parametrize: 하나의 테스트를 여러 입력으로 펼치기
@pytest.mark.parametrize로 같은 검증 로직을 여러 입력에 자동 적용하는 법, 루프와의 차이, id와 fixture 조합까지 완전 정복합니다.
지난 글에서 fixture로 테스트 준비물을 깔끔하게 주입하는 법을 익혔다. 이번에는 다른 종류의 반복을 다룬다. 같은 함수를 여러 입력값으로 검증하고 싶을 때다. 예를 들어 square(2)==4, square(3)==9, square(-1)==1처럼 입력만 다르고 검증 구조는 똑같은 케이스가 줄줄이 있다고 하자. 이걸 매번 별도 함수로 복사하면 지루하고, 함수 안에서 for 루프로 도는 것은 더 나쁜 함정이 있다. pytest의 parametrize가 정답이다.
케이스 목록을 데코레이터로
@pytest.mark.parametrize는 인자 이름들과 케이스 목록을 받아, 케이스 개수만큼 독립적인 테스트를 자동 생성한다.
import pytest
def square(n):
return n * n
@pytest.mark.parametrize("n, expected", [
(2, 4),
(3, 9),
(-1, 1),
(0, 0),
])
def test_square(n, expected):
assert square(n) == expected
첫 번째 인자 "n, expected"는 테스트 함수가 받을 파라미터 이름이고, 두 번째 인자는 그 값들의 튜플 목록이다. pytest는 이 함수를 네 번, 각각 다른 (n, expected) 조합으로 실행한다. 출력에는 test_square[2-4], test_square[3-9]처럼 입력값이 박힌 이름으로 나타나, 어떤 케이스가 통과·실패했는지 한눈에 보인다.
왜 함수 안의 for 루프는 안 되나
같은 일을 함수 하나 안에서 for 루프로 처리할 수도 있다. 하지만 그렇게 하면 두 가지를 잃는다.
첫째, assert는 첫 실패에서 즉시 멈춘다. 루프 안에서 세 번째 입력이 실패하면 나머지 입력은 아예 검증되지 않는다. 둘째, 실패 메시지에 어떤 입력이 문제였는지 드러나지 않아 디버깅이 어렵다. parametrize는 각 케이스가 별도 테스트이므로 하나가 실패해도 나머지는 끝까지 돌고, 실패한 입력이 테스트 이름에 그대로 박혀 나온다.
# 권장하지 않는 방식 — 첫 실패에서 멈추고 입력을 가린다
def test_square_loop():
for n, expected in [(2, 4), (3, 8), (4, 16)]:
assert square(n) == expected # (3, 8)에서 멈춤, (4,16)은 미검증
id로 케이스에 이름 붙이기
입력이 복잡한 객체이거나 의미를 분명히 하고 싶을 때는 ids로 각 케이스에 읽기 좋은 이름을 줄 수 있다.
@pytest.mark.parametrize(
"value, is_valid",
[
("user@example.com", True),
("no-at-sign", False),
("", False),
],
ids=["정상이메일", "골뱅이없음", "빈문자열"],
)
def test_email_validation(value, is_valid):
assert validate_email(value) == is_valid
이제 출력에 test_email_validation[정상이메일]처럼 의미가 담긴 이름이 나온다. 실패 보고만 봐도 어떤 시나리오가 깨졌는지 즉시 파악된다.
fixture와 함께 쓰기, 그리고 곱집합
parametrize는 fixture와 자연스럽게 섞인다. 또 데코레이터를 여러 개 쌓으면 입력의 모든 조합(곱집합)이 자동으로 만들어진다.
@pytest.mark.parametrize("base", [2, 10])
@pytest.mark.parametrize("exp", [0, 1, 2])
def test_power(base, exp):
# (2,0) (2,1) (2,2) (10,0) (10,1) (10,2) → 6개 테스트
assert pow(base, exp) == base ** exp
base 2개와 exp 3개가 곱해져 6개의 테스트가 생성된다. 다만 조합이 기하급수적으로 늘 수 있으니, 정말 모든 조합이 의미 있을 때만 쌓는 것이 좋다.
parametrize는 “테스트 코드는 그대로 두고 입력만 데이터로 분리한다”는 발상이다. 새 경계값이 떠오르면 목록에 한 줄 추가하기만 하면 된다. 다음 글에서는 외부 의존성을 가짜로 바꿔치기하는 mock과 monkeypatch를 다룬다.
지난 글: pytest fixture: 테스트 준비물을 우아하게 주입하기
다음 글: mock과 monkeypatch: 의존성을 가짜로 바꾸기
읽어주셔서 감사합니다. 😊