파이썬 테스트 #3 parametrize와 마커: 케이스를 늘리고 골라 돌리기

5 분 소요

#1 pytest 시작에서 첫 테스트를 돌렸고, #2 픽스처에서 준비 코드를 정리했습니다. 이번 글은 테스트 본문의 반복을 줄이는 차례입니다. 같은 함수를 입력만 바꿔 가며 검증하느라 거의 똑같은 테스트를 복사해 본 경험이 있다면, @pytest.mark.parametrize가 그 반복을 없애 줍니다. 후반부에서는 마커로 테스트에 라벨을 붙여 원하는 것만 골라 돌리는 방법까지 다루겠습니다.

같은 테스트를 세 번 복사하고 있다면 #

🚫 입력만 다른 테스트 세 개
def test_add_positive():
    assert add(1, 2) == 3

def test_add_negative():
    assert add(-1, -2) == -3

def test_add_zero():
    assert add(0, 0) == 0

로직은 전부 assert add(a, b) == expected 한 줄입니다. 다른 건 숫자뿐인데 함수가 세 개입니다. 케이스를 추가할 때마다 함수 이름을 새로 짓고 본문을 복사해야 하고, 검증 방식이 바뀌면 세 군데를 같이 고쳐야 합니다.

@pytest.mark.parametrize: 케이스 표가 곧 명세 #

✅ 케이스를 표로
import pytest
from calculator import add

@pytest.mark.parametrize("a, b, expected", [
    (1, 2, 3),
    (-1, -2, -3),
    (0, 0, 0),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

첫 인자는 파라미터 이름이고, 둘째 인자는 케이스 목록입니다. pytest가 케이스마다 테스트를 한 번씩 돌려서, 함수는 하나지만 pytest -v로 보면 test_add[1-2-3], test_add[-1--2--3], test_add[0-0-0] 세 건으로 집계됩니다. 케이스 목록 자체가 “이 함수는 이런 입력에서 이런 결과를 낸다"는 명세 역할을 하고, 새 케이스 추가는 튜플 한 줄 추가로 끝납니다.

[-1--2--3] 같은 자동 ID는 읽기 어렵습니다. 케이스 목록 뒤에 ids=["positive", "negative", "zero"]를 더하면 결과가 test_add[negative]처럼 표시되어, 실패했을 때 어느 케이스가 깨졌는지 바로 보입니다. 케이스가 늘어날수록 효과가 큽니다.

parametrize 겹치기: 조합 자동 생성 #

데코레이터를 두 번 겹치면 모든 조합이 만들어집니다.

조합 생성
@pytest.mark.parametrize("role", ["admin", "member"])
@pytest.mark.parametrize("active", [True, False])
def test_permission(role, active):
    assert can_login(role, active) == active

2×2로 4개 케이스가 자동으로 생깁니다. 다만 파라미터가 늘면 케이스 수가 곱으로 늘어나니, 정말 모든 조합을 검증해야 하는지 따져 보고 쓰는 편이 좋습니다.

에러 케이스도 표로: pytest.raises와 결합 #

경계값과 잘못된 입력이야말로 parametrize가 가장 빛나는 영역입니다.

에러 케이스 모음
@pytest.mark.parametrize("bad_input", ["", "abc", "-1", "200"])
def test_parse_age_rejects(bad_input):
    with pytest.raises(ValueError):
        parse_age(bad_input)

정상 케이스 표와 에러 케이스 표를 나란히 두면, 함수가 무엇을 받고 무엇을 거부하는지 테스트 파일만 봐도 드러납니다.

skip, skipif, xfail: 세 마커의 의미 구분 #

마커는 테스트에 붙이는 라벨입니다. pytest에 내장된 세 가지부터 구분하겠습니다.

내장 마커 세 가지
@pytest.mark.skip(reason="결제 모듈 교체 작업 중")
def test_legacy_payment():
    ...

@pytest.mark.skipif(sys.platform == "win32", reason="유닉스 전용 기능")
def test_unix_socket():
    ...

@pytest.mark.xfail(reason="이슈 #142: 음수 금액 버그", strict=True)
def test_negative_amount():
    assert charge(-1000) is None
마커의미
skip지금은 돌리지 않습니다. 이유를 reason에 남깁니다
skipif조건이 참일 때만 건너뜁니다 (OS, 파이썬 버전 등)
xfail실패할 것을 알고 있습니다. 알려진 버그의 문서화입니다

xfail이 특히 유용합니다. 버그를 발견했지만 당장 고칠 수 없을 때, 테스트를 지우는 대신 xfail로 남겨 두면 버그가 기록으로 남습니다. strict=True를 켜 두면 버그가 고쳐져 테스트가 통과하는 순간 XPASS 실패로 알려 주므로, 마커를 떼는 시점도 놓치지 않습니다.

커스텀 마커: 등록하고 -m으로 골라 돌리기 #

마커는 직접 정의할 수도 있습니다. 먼저 pyproject.toml에 등록합니다. 등록하지 않은 마커는 경고가 나오고, --strict-markers 옵션을 켜면 에러가 됩니다.

pyproject.toml
[tool.pytest.ini_options]
markers = [
    "slow: 오래 걸리는 테스트",
    "integration: 외부 서비스가 필요한 테스트",
]
마커 부착
@pytest.mark.slow
def test_full_report():
    ...

실행할 때 -m-k 옵션으로 고릅니다.

골라 실행
pytest -m slow                        # slow 마커만
pytest -m "not slow"                  # slow 제외
pytest -m "integration and not slow"  # and, or, not 조합 가능
pytest -k parse                       # 이름에 parse가 들어간 테스트만

-m은 마커로, -k테스트 이름으로 고릅니다. -k는 마커를 붙이지 않았어도 동작하고, pytest -k "parse and not rejects"처럼 조합도 됩니다.

slow 마커 운영 패턴 #

커스텀 마커의 가장 흔한 활용은 느린 테스트 분리입니다. 전체 스위트가 느려지면 개발 중에 테스트를 안 돌리게 되고, 그 순간부터 테스트의 가치가 떨어집니다.

  • 1초 이상 걸리는 테스트에 @pytest.mark.slow를 붙입니다
  • 개발 중에는 pytest -m "not slow"로 빠르게 돌립니다
  • 커밋 전이나 CI에서는 pytest로 전체를 돌립니다

이 구분만으로 “자주 도는 빠른 스위트"와 “가끔 도는 전체 스위트"라는 두 층이 생깁니다. CI 쪽 운영은 7편에서 마저 다루겠습니다.

정리 #

  • @pytest.mark.parametrize: 입력만 다른 테스트를 케이스 표 하나로 합칩니다
  • ids: 케이스에 읽기 좋은 이름을 붙입니다
  • 데코레이터 겹치기: 파라미터 조합을 자동 생성합니다
  • pytest.raises와 결합: 에러 케이스와 경계값도 표로 명세합니다
  • skip/skipif/xfail: 건너뛰기, 조건부 건너뛰기, 알려진 버그 문서화
  • 커스텀 마커 + -m, 이름 기반 -k: 원하는 테스트만 골라 실행합니다
  • slow 마커: 빠른 스위트와 전체 스위트를 분리해 운영합니다

다음 글(#4 mock과 monkeypatch)에서는 외부에 의존하는 코드를 테스트하는 도구를 다루겠습니다. 시간, 환경 변수, 네트워크 호출처럼 테스트가 통제할 수 없는 것을 가짜로 바꾸는 방법입니다.

X