들어가며
테스트 코드를 작성하다 보면 반복을 자주 만납니다. pytest는 이 두 반복을 줄여 주는 도구를 갖고 있습니다.
이 글에서는 parametrize와 fixture를 중심으로 pytest의 핵심 기능을 하나씩 살펴보겠습니다.
1. parametrize
덧셈 함수를 테스트한다고 해보겠습니다. 입력 조합이 여러 개라면 보통 테스트 함수를 여러 개 만들게 됩니다. 같은 코드를 입력만 바꿔 반복하는 셈입니다.
@pytest.mark.parametrize는 이 반복을 없애 줍니다. 입력과 기대값의 목록을 한 번에 넘기면 됩니다. 그러면 pytest가 목록의 각 줄을 별도 테스트로 실행합니다.
import pytest
@pytest.mark.parametrize("a, b, expected", [(1, 2, 3), (2, 3, 5), (3, 5, 8)])
def test_addition(a, b, expected):
assert a + b == expected
테스트 함수는 하나지만 실제로는 세 번 실행됩니다. 어떤 입력에서 실패했는지도 따로 표시됩니다.
2. 예외가 잘 발생하는지 확인 - pytest.raises
테스트는 정상 동작만 확인하지 않습니다. 잘못된 입력에서 예외가 제대로 발생하는지도 확인해야 합니다. pytest.raises는 이때 씁니다. 지정한 예외가 블록 안에서 발생하면 테스트가 통과합니다.
import pytest
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
1 / 0
예외가 발생하지 않으면 이 테스트는 실패합니다. "예외가 발생해야 정상"인 상황을 검증하는 방법입니다.
3. 반복되는 준비 작업 묶기 - fixture
테스트마다 같은 데이터나 객체를 미리 만들어야 할 때가 있습니다. 이 준비 작업을 매 테스트에 복사해 넣으면 코드가 지저분해집니다.
fixture는 이 준비 작업을 함수 하나로 분리하는 방법입니다. @pytest.fixture를 붙여 함수를 만듭니다. 그리고 테스트 함수의 인자 이름에 그 fixture 이름을 적습니다. 그러면 pytest가 fixture를 먼저 실행하고 그 결과를 테스트에 넘겨 줍니다.
import pytest
@pytest.fixture
def sample_list():
return [1, 2, 3, 4, 5]
def test_list_sum(sample_list):
assert sum(sample_list) == 15
test_list_sum은 sample_list를 인자로 받습니다. pytest가 sample_list()를 실행해 반환값 [1, 2, 3, 4, 5]를 넣어 줍니다.
4. fixture의 생명주기 - scope
fixture는 기본적으로 테스트 함수마다 새로 실행됩니다. 그런데 데이터베이스 연결처럼 만드는 비용이 큰 자원은 매번 새로 만들면 낭비입니다.
scope 매개변수로 fixture를 얼마나 자주 만들지 정합니다. 값은 function, class, module, session 네 가지입니다. 뒤로 갈수록 더 넓은 범위에서 한 번만 만들어 재사용합니다.
@pytest.fixture(scope="module")
def db_connection():
conn = create_db_connection()
yield conn
conn.close()
scope="module"은 모듈 하나에서 연결을 한 번만 만듭니다. 그 모듈의 모든 테스트가 같은 연결을 함께 씁니다.
fixture는 다른 fixture를 인자로 받아 조합할 수도 있습니다.
@pytest.fixture
def user_data():
return {"name": "John", "age": 30}
@pytest.fixture
def user(user_data):
return User(**user_data)
def test_user_age(user):
assert user.age == 30
user fixture가 user_data fixture를 받아 씁니다. pytest가 의존 순서를 파악해 차례대로 실행합니다.
5. 정리 작업 자동화 - yield와 teardown
테스트가 끝난 뒤에는 정리가 필요할 때가 있습니다. 만든 임시 유저를 지우거나 파일을 닫는 작업입니다.
fixture 안에서 return 대신 yield를 쓰면 정리 작업을 붙일 수 있습니다. yield까지가 준비 작업입니다. yield 다음 줄부터가 정리 작업입니다. pytest는 테스트가 끝난 뒤 yield 다음 부분을 이어서 실행합니다.
아래는 메일 송수신을 테스트하는 예제입니다. 유저를 만듭니다. 테스트가 끝나면 유저를 삭제합니다.
@pytest.fixture
def sending_user(mail_admin):
user = mail_admin.create_user()
yield user
mail_admin.delete_user(user) # teardown
@pytest.fixture
def receiving_user(mail_admin):
user = mail_admin.create_user()
yield user
user.clear_mailbox() # teardown
mail_admin.delete_user(user)
def test_email_received(sending_user, receiving_user):
email = Email(subject="Hey!", body="How's it going?")
sending_user.send_email(email, receiving_user)
assert email in receiving_user.inbox
실행 순서는 다음과 같습니다.
sending_user실행 → 유저 생성 후yield에서 멈춤receiving_user실행 → 유저 생성 후yield에서 멈춤test_email_received본문 실행receiving_user의yield이후 실행 → 메일박스 비우고 유저 삭제sending_user의yield이후 실행 → 유저 삭제
정리는 만든 순서의 반대로 진행됩니다. 나중에 만든 fixture가 먼저 정리됩니다.
6. 테스트 분류하고 골라 실행 - 마커
테스트가 많아지면 종류별로 골라 실행할 필요가 생깁니다. 느린 테스트만 제외하거나 데이터베이스 테스트만 실행하는 경우입니다.
마커는 테스트에 붙이는 이름표입니다. 먼저 pytest.ini나 pyproject.toml에 쓸 마커를 정의합니다.
markers =
slow: 느린 테스트
db: 데이터베이스 관련 테스트
그다음 테스트 함수에 @pytest.mark.<이름>을 붙입니다.
@pytest.mark.slow
def test_large_computation():
assert large_computation() == expected_value
이제 -m 옵션으로 특정 마커만 골라 실행합니다.
pytest -m slow
7. 실행 범위 지정과 실패 메시지
테스트 파일이 여러 개면 실행 대상을 지정할 수 있습니다.
pytest test_*.py # 와일드카드로 파일 묶음 지정
pytest mymodule # 특정 모듈 지정
pytest ./ # 현재 디렉터리 전체
실패했을 때 보여줄 메시지도 직접 붙일 수 있습니다. assert 문 뒤에 쉼표로 메시지를 적습니다.
def test_even():
a = 11
assert a % 2 == 0, "value was odd, should be even"
이 테스트가 실패하면 "value was odd, should be even"이 함께 출력됩니다. 왜 실패했는지 바로 알 수 있습니다.
8. 자주 쓰는 플러그인
pytest는 플러그인으로 기능을 확장합니다. 자주 쓰는 두 가지를 소개합니다.
pytest-cov는 테스트가 전체 코드 중 얼마나 실행하는지를 측정합니다. 이 비율을 커버리지라고 합니다.
pip install pytest-cov
pytest --cov=my_module
pytest-xdist는 테스트를 여러 코어로 나눠 병렬 실행합니다. 테스트가 많을 때 전체 시간이 줄어듭니다.
pip install pytest-xdist
pytest -n auto
정리
| 기능 | 무엇을 하는가 | 핵심 |
|---|---|---|
| parametrize | 같은 테스트를 여러 입력으로 | @pytest.mark.parametrize |
| pytest.raises | 예외 발생 검증 | with pytest.raises(예외): |
| fixture | 준비 작업을 함수로 분리 | 인자 이름으로 주입 |
| scope | fixture 재사용 범위 | function / class / module / session |
| yield teardown | 테스트 후 정리 자동화 | yield 이후가 정리 |
| 마커 | 테스트 분류·선택 실행 | @pytest.mark + -m |
| 플러그인 | 기능 확장 | pytest-cov, pytest-xdist |
pytest의 핵심은 반복을 줄이는 데 있습니다. parametrize는 입력 반복을 줄입니다. fixture는 준비 작업 반복을 줄입니다. 여기에 마커와 플러그인을 더하면 테스트를 골라 실행하고 측정까지 할 수 있습니다.