Selenium + Python 추천 라이브러리 6선 | 테스트 자동화 필수 도구

테스트 자동화

@pytest.mark.parametrize는 하나의 테스트 함수를 여러 패턴으로 실행할 수 있는 강력한 기능입니다. API 테스트에서 인증 패턴 전환, UI 테스트에서 역할별 접근 권한 검증 등 실무에서 빠질 수 없는 장면이 많습니다. pytest.param·indirect·fixture params와의 사용 구분까지 이해하면 데이터 기반 테스트의 폭이 크게 넓어집니다.

📌 이 글의 대상 독자

@pytest.mark.parametrize의 기본은 알지만 더 활용하고 싶은 분
pytest.paramid·marks 사용법을 알고 싶은 분
indirect를 활용한 fixture 연동 방법을 배우고 싶은 분
✅ fixture params와의 사용 구분을 정리하고 싶은 분

📖 이 글에서 배울 수 있는 내용

✔ 복수 인수·복수 데코레이터를 활용한 파라미터 조합 방법
pytest.param으로 id·marks(skip·xfail 포함)를 붙여 관리성을 높이는 방법
indirect=True로 fixture와 파라미터를 연동하는 방법
✔ fixture params와의 사용 구분 기준 (실무 빈도 포함 비교표)
✔ 조합 폭발 대처법 (페어와이즈·경계값·리스크 기반)
pytest-xdist와 조합해 병렬 실행을 고속화하는 방법

필자는 QA 엔지니어로 15년 이상 API 테스트·기능 테스트 자동화를 담당해 왔습니다.
인증 토큰을 여러 패턴으로 전환하면서 API 테스트를 돌리거나, 역할별 접근 권한을 일괄 검증하는 등의 장면에서 파라미터화 테스트를 많이 활용해 온 경험을 바탕으로 실제 현장에서의 실패 사례와 개선 패턴을 함께 소개합니다.

✅ 이 글의 핵심 정리

  • pytest.param(id=..., marks=...)으로 패턴에 이름과 속성을 붙여 관리성을 높인다
  • indirect=True로 파라미터를 fixture에 전달해 셋업도 함께 전환한다
  • 단일 테스트에 대한 입력 변형에는 parametrize, 여러 테스트에 공통 적용할 때는 fixture params를 사용한다

@pytest.mark.parametrize 기본 복습

응용 패턴에 들어가기 전에 최소 구성을 확인해 둡니다.

import pytest

# 인수명과 패턴 리스트를 지정하기만 하면 됨
@pytest.mark.parametrize("value, expected", [
    (1,  True),
    (0,  False),
    (-1, False),
])
def test_is_positive(value: int, expected: bool) -> None:
    assert (value > 0) == expected

이 테스트는 3가지 패턴으로 실행됩니다. 출력은 test_is_positive[1-True]처럼 자동으로 이름이 붙습니다.

응용 패턴 5가지

1. pytest.param으로 id·marks를 붙여 관리성 높이기

패턴이 늘어나면 “어떤 케이스가 실패했는지” 파악하기 어려워집니다.
pytest.param으로 id(테스트명)와 marks(속성)를 붙이면 가독성이 크게 향상됩니다.

import pytest
import requests

@pytest.mark.parametrize("payload, expected_status", [
    pytest.param(
        {"username": "admin", "password": "correct"},
        200,
        id="valid_credentials",
    ),
    pytest.param(
        {"username": "admin", "password": "wrong"},
        401,
        id="wrong_password",
    ),
    # smoke 마크로 CI에서 기본 확인만 먼저 실행 (pytest -m smoke)
    pytest.param(
        {"username": "", "password": ""},
        400,
        id="empty_credentials",
        marks=pytest.mark.smoke,
    ),
    # 기지 버그로 인해 일시 스킵
    pytest.param(
        {"username": "admin", "password": None},
        400,
        id="null_password",
        marks=pytest.mark.skip(reason="Issue #123 수정 대기 중"),
    ),
    # 향후 수정 예정인 실패 패턴 (xfail)
    pytest.param(
        {"username": " ", "password": "pass"},
        400,
        id="whitespace_username",
        marks=pytest.mark.xfail(reason="공백만 있는 사용자명이 현재 통과됨"),
    ),
])
def test_login(payload: dict, expected_status: int) -> None:
    response = requests.post(
        "https://example.com/api/auth/login",  # 샘플 URL
        json=payload,
        timeout=(3, 10),
    )
    assert response.status_code == expected_status

id를 붙이면 test_login[valid_credentials]처럼 표시됩니다.
id가 없으면 test_login[payload0-200]같은 자동 생성명이 되어 패턴이 많아질수록 특정하기 어려워집니다.

marks 종류용도실무에서의 활용 장면
pytest.mark.smoke커스텀 분류CI에서 기본 확인만 먼저 실행하고 싶을 때
pytest.mark.skip일시 스킵기지 버그·환경 의존 패턴 제외
pytest.mark.xfail실패가 예상됨수정 예정 버그를 테스트에 남겨두고 싶을 때
⚠️ 커스텀 마크는 pytest.ini 등록이 필요
pytest.mark.smoke 등의 커스텀 마크는 pytest.ini(또는 pyproject.toml)에 등록하지 않으면 PytestUnknownMarkWarning이 발생합니다. skip·xfail은 내장 마크이므로 등록이 필요 없습니다.

# pytest.ini
[pytest]
markers =
    smoke: 스모크 테스트 (기본 동작 확인)
    slow: 실행 시간이 긴 테스트

2. 복수의 parametrize를 쌓아 조합 생성하기

@pytest.mark.parametrize를 여러 개 쌓으면 모든 패턴의 직적(조합)이 자동으로 생성됩니다.

import pytest

# 2 × 3 = 6가지 패턴 자동 생성
@pytest.mark.parametrize("lang", ["ja", "en", "ko"])
@pytest.mark.parametrize("role", ["admin", "user"])
def test_dashboard_access(lang: str, role: str) -> None:
    # 실무 예: 언어 × 역할의 모든 조합으로 접근 권한 검증
    assert lang in ("ja", "en", "ko")
    assert role in ("admin", "user")
⚠️ 조합 폭발 대처법
쌓을수록 패턴 수가 곱셈으로 늘어납니다 (3×3×3=27, 4×4×4=64). 실행 시간이 급증하므로 아래 방법으로 패턴을 줄이세요.

  • 경계값 분석: 최솟값·최댓값·경계 ±1만 선택
  • 페어와이즈 테스트: 임의의 두 인수 조합이 반드시 한 번은 포함되도록 줄이기 (allpairspy 등 활용 가능)
  • 리스크 기반 선택: 장애 발생 시 영향이 큰 조합을 우선하고 저위험 조합은 줄이기
  • pytest.param + skip: 불필요한 조합에만 marks=pytest.mark.skip을 붙여 제외

3. indirect=True로 fixture에 파라미터 전달하기

📎 fixture가 아직 익숙하지 않은 분은 먼저 이쪽을
pytest 픽스처 입문 가이드 — yield·scope·conftest.py 실무 관점 해설

indirect=True를 사용하면 파라미터 값이 테스트 함수에 직접 전달되지 않고, 같은 이름의 fixture에 전달된 후 테스트 함수에 주입됩니다.
셋업 처리를 파라미터에 따라 전환하고 싶을 때 편리합니다.

import pytest
import requests

# 파라미터를 받아 인증된 세션을 반환하는 fixture
@pytest.fixture
def authed_session(request: pytest.FixtureRequest) -> requests.Session:
    role: str = request.param  # ← parametrize의 값이 여기에 들어옴
    credentials = {
        "admin": {"username": "admin_user", "password": "admin_pass"},
        "user":  {"username": "normal_user", "password": "user_pass"},
    }
    cred = credentials[role]
    session = requests.Session()
    res = session.post(
        "https://example.com/api/auth/login",  # 샘플 URL
        json=cred,
        timeout=(3, 10),
    )
    res.raise_for_status()
    session.headers.update({"Authorization": f"Bearer {res.json()['token']}"})
    yield session
    session.close()


# indirect=True로 authed_session fixture에 파라미터를 전달
@pytest.mark.parametrize("authed_session", ["admin", "user"], indirect=True)
def test_profile_access(authed_session: requests.Session) -> None:
    res = authed_session.get(
        "https://example.com/api/profile",  # 샘플 URL
        timeout=(3, 10),
    )
    assert res.status_code == 200

4. 일부만 indirect로 지정하기 (부분 indirect)

import pytest
import requests

@pytest.fixture
def authed_session(request: pytest.FixtureRequest) -> requests.Session:
    role: str = request.param
    session = requests.Session()
    # ... 로그인 처리 ...
    yield session
    session.close()


@pytest.mark.parametrize(
    "authed_session, endpoint",
    [
        pytest.param("admin", "/api/admin/users",   id="admin_users"),
        pytest.param("admin", "/api/admin/reports", id="admin_reports"),
        pytest.param("user",  "/api/profile",       id="user_profile"),
    ],
    indirect=["authed_session"],  # ← 리스트로 대상 인수를 지정
)
def test_endpoint_access(authed_session: requests.Session, endpoint: str) -> None:
    res = authed_session.get(
        f"https://example.com{endpoint}",  # 샘플 URL
        timeout=(3, 10),
    )
    assert res.status_code == 200

5. pytest-xdist와 조합해 병렬 실행 고속화하기

pip install pytest-xdist
# CPU 코어 수에 맞게 자동 병렬화
pytest -n auto

# 코어 수를 직접 지정
pytest -n 4
⚠️ 병렬 실행 시 테스트 간 간섭 주의
DB 쓰기나 공유 리소스 접근을 포함하는 fixture를 파라미터화하면 병렬 실행 시 간섭이 발생할 수 있습니다.
읽기 전용 데이터나 독립 리소스를 사용하는 fixture라면 session·module 스코프에서도 문제가 생기기 어렵습니다.

parametrize와 fixture params의 사용 구분

fixture params 코드 예시

import pytest
import requests

@pytest.fixture(params=[
    pytest.param("admin",  id="admin_role"),
    pytest.param("editor", id="editor_role"),
    pytest.param("viewer", id="viewer_role"),
])
def role(request: pytest.FixtureRequest) -> str:
    return request.param


def test_can_access_dashboard(role: str) -> None:
    res = requests.get(
        "https://example.com/api/dashboard",  # 샘플 URL
        headers={"X-Role": role},
        timeout=(3, 10),
    )
    assert res.status_code == 200


def test_can_view_profile(role: str) -> None:
    res = requests.get(
        "https://example.com/api/profile",  # 샘플 URL
        headers={"X-Role": role},
        timeout=(3, 10),
    )
    assert res.status_code == 200

비교표

비교 항목@pytest.mark.parametrizefixture params실무 빈도
단일 테스트에 대한 입력 변형★★★
여러 테스트 함수에 공통 패턴 적용★★☆
셋업 처리도 함께 파라미터화★★☆
조합 테스트 (직적)★★☆
id·marks로 패턴 관리★★★
초보자 가독성
하나의 테스트 함수에 여러 입력 패턴을 전달하고 싶다→ @pytest.mark.parametrize
여러 테스트 함수에 같은 패턴을 재사용하고 싶다→ fixture params
파라미터에 따라 셋업도 바꾸고 싶다→ fixture params 또는 indirect
파라미터의 조합(직적)을 자동 생성하고 싶다→ @pytest.mark.parametrize 중첩

테스트 데이터를 별도 파일로 관리해 공유하기

# tests/test_data.py
import pytest

VALID_USER_PAYLOADS = [
    pytest.param({"username": "admin",  "password": "admin123"}, id="admin"),
    pytest.param({"username": "editor", "password": "edit456"},  id="editor"),
]

INVALID_USER_PAYLOADS = [
    pytest.param({"username": "", "password": "pass"}, id="empty_username"),
    pytest.param({},                                    id="empty_body"),
]
# tests/test_login.py
import pytest
import requests
from tests.test_data import VALID_USER_PAYLOADS, INVALID_USER_PAYLOADS


@pytest.mark.parametrize("payload", VALID_USER_PAYLOADS)
def test_login_success(payload: dict) -> None:
    res = requests.post(
        "https://example.com/api/auth/login",  # 샘플 URL
        json=payload,
        timeout=(3, 10),
    )
    assert res.status_code == 200


@pytest.mark.parametrize("payload", INVALID_USER_PAYLOADS)
def test_login_failure(payload: dict) -> None:
    res = requests.post(
        "https://example.com/api/auth/login",  # 샘플 URL
        json=payload,
        timeout=(3, 10),
    )
    assert res.status_code in (400, 422)

Playwright에서의 파라미터화 예시

import pytest
from playwright.sync_api import Page

@pytest.mark.parametrize("username, password, expected_url", [
    pytest.param("admin", "correct", "/dashboard", id="admin_login"),
    pytest.param("user",  "correct", "/home",      id="user_login"),
    pytest.param("admin", "wrong",   "/login",     id="invalid_password"),
    pytest.param("",      "",        "/login",     id="empty_credentials"),
])
def test_login_redirect(
    page: Page,
    username: str,
    password: str,
    expected_url: str,
) -> None:
    page.goto("https://example.com/login")  # 샘플 URL
    page.fill("[data-testid='username']", username)
    page.fill("[data-testid='password']", password)
    page.click("[data-testid='submit']")
    page.wait_for_url(f"**{expected_url}")
    assert expected_url in page.url

실무에서 자주 겪는 함정 7가지

⚠️ pytest parametrize에서 주의해야 할 포인트

  1. 테스트 함수명에 test_ 접두사가 없음
    pytest는 기본적으로 test_로 시작하는 함수만 수집합니다. 파라미터화해도 함수명이 잘못되면 전혀 실행되지 않습니다.
  2. conftest.py의 위치가 잘못됨
    공유 fixture를 conftest.py에 작성하는 경우, 테스트 파일과 같은 디렉터리 또는 상위 디렉터리에 두어야 합니다.
  3. 상태를 보유하는 fixture를 파라미터화함
    DB 쓰기나 공유 리소스를 포함하는 fixture를 파라미터화하면 병렬 실행 시 간섭이 발생할 수 있습니다. 읽기 전용·독립 리소스라면 session·module 스코프에서도 문제가 생기기 어렵습니다.
  4. 커스텀 마크를 pytest.ini에 등록하지 않음
    pytest.ini에 마크 등록이 없으면 PytestUnknownMarkWarning이 발생합니다. skip·xfail은 내장 마크이므로 등록 불필요입니다.
  5. print() 출력이 보이지 않음
    pytest -s를 붙이거나 logging 모듈을 사용하세요.
  6. 셋업 중 예외 발생으로 ERROR가 됨
    fixture나 indirect 처리 내 예외는 FAILED가 아닌 ERROR로 표시됩니다. 파라미터의 데이터 타입을 확인하세요.
  7. 루트 디렉터리 이외에서 실행해 ModuleNotFoundError 발생
    from tests.test_data import ...는 프로젝트 루트에서 실행하지 않으면 경로가 해결되지 않습니다.

자주 묻는 질문 (FAQ)

Q. id는 필수인가요?

필수는 아니지만 5가지 이상의 패턴이 되면 붙이는 것을 권장합니다. id가 없으면 자동 생성명이 되어 CI 로그에서 실패 패턴을 특정하기 어려워집니다.

Q. fixture params와 indirect 중 어느 것을 사용해야 하나요?

여러 테스트 함수에 같은 패턴을 자동 적용하고 싶을 때는 fixture params가 자연스럽습니다. 특정 테스트 함수에서만 셋업을 파라미터에 따라 바꾸고 싶을 때는 indirect가 적합합니다. 위의 판단 흐름 표를 참고하세요.

Q. indirect=Trueindirect=["인수명"]의 차이는?

indirect=True는 모든 인수를 fixture 경유로 만듭니다. 리스트 형식은 특정 인수만 fixture 경유로 지정할 수 있습니다.

Q. 특정 패턴만 스킵하고 싶은 경우는?

pytest.param(..., marks=pytest.mark.skip(reason="..."))으로 그 패턴만 스킵할 수 있습니다. pytest.mark.xfail을 사용하면 실패가 예상되는 패턴으로 기록할 수 있습니다.

Q. 파라미터 수가 너무 많아졌을 때는?

CSV·JSON·YAML 파일에서 읽어오거나, pytest_generate_tests 훅으로 동적 제어도 가능합니다(중급자 대상). 우선 경계값·대표값·이상값의 핵심 패턴에 집중하는 것이 선결입니다.

Q. @pytest.mark.parametrize는 클래스 기반 테스트에서도 사용할 수 있나요?

사용할 수 있습니다. 메서드에 붙이는 것도, 클래스 전체에 붙이는 것도 가능합니다.

정리

핵심 포인트내용
pytest.param으로 id·marks관리성·가독성 향상. skip·xfail도 활용
복수 데코레이터로 직적조합 자동 생성. 폭발 대책은 페어와이즈·경계값·리스크 기반으로
indirect=True파라미터를 fixture에 전달해 셋업도 전환
부분 indirect리스트로 일부 인수만 fixture 경유로
pytest-xdist 조합병렬 실행으로 고속화. 상태 간섭이 있는 fixture에는 주의
test_data.py로 공유conftest.py가 아닌 전용 파일로 분리해 안전하게 import
fixture params와의 구분단일 테스트 입력 변화 → parametrize, 여러 테스트 공통 적용 → fixture params

@pytest.mark.parametrize의 응용 패턴을 활용하면 테스트 케이스의 증가를 코드량 증가 없이 실현할 수 있습니다.
먼저 기존 테스트의 pytest.paramid를 붙이는 것부터 시작해 보세요.

패턴 관리가 정돈되면 케이스 추가·삭제 작업이 훨씬 수월해지고, CI의 실패 메시지도 한눈에 파악할 수 있게 됩니다.

제목과 URL을 복사했습니다