pytest 픽스처로 테스트 셋업·해체를 깔끔하게 관리하기

pytest 픽스처로 테스트 셋업·해체를 깔끔하게 관리하기

테스트를 작성하다 보면 "각 테스트가 시작하기 전에 데이터를 준비하고, 끝나면 깨끗이 정리해야 하는" 일이 반복돼요. 그 준비 코드를 매 테스트마다 복사해 붙여 넣는 대신, pytest는 픽스처(fixture) 라는 재사용 가능한 셋업 함수를 제공해요. 픽스처는 pytest가 자동으로 실행 시점과 해제 시점을 관리해 주는 함수라서, 코드가 훨씬 짧아지고 테스트 간에 상태가 섞이는 문제도 막아줘요. 여기서는 픽스처를 요청하고, 범위(scope)를 지정하고, 해제(teardown) 코드까지 얹는 방법을 순서대로 살펴볼게요.

출처: pytest 공식 문서 - How to use fixtures

픽스처 "요청하기": 함수 시그니처로 연결

가장 기본적인 방식은 테스트 함수의 매개변수 이름으로 픽스처를 요청하는 거예요. pytest는 테스트 함수의 시그니처를 보고, 그 이름과 같은 픽스처를 찾아서 실행한 뒤 반환값을 인자로 넘겨줘요.

# content of test_append.py
import pytest

# Arrange
@pytest.fixture
def first_entry():
    return "a"

# Arrange
@pytest.fixture
def order(first_entry):
    return [first_entry]

def test_string(order):
    # Act
    order.append("b")

    # Assert
    assert order == ["a", "b"]

위 예시에서 order 픽스처가 다시 first_entry 픽스처를 요청하고 있어요. 이렇게 픽스처도 테스트처럼 다른 픽스처를 요청할 수 있고, 모든 요청 규칙이 그대로 적용돼요.

픽스처는 재사용 가능하고 결과는 캐시돼요

서로 다른 두 테스트가 같은 픽스처를 요청하면, pytest는 각 테스트에 픽스처의 자체 실행 결과를 따로 넘겨줘요. 덕분에 한 테스트가 데이터를 바꿔도 다른 테스트에 영향이 없어요. 그래서 각 테스트가 항상 깨끗한 상태에서 시작하도록 보장할 수 있어요.

한 가지 알아둘 점은, 같은 테스트 안에서 픽스처를 여러 번 요청해도 pytest가 그 픽스처를 다시 실행하지 않는다는 거예요. 한 번 실행된 반환값은 같은 테스트 동안 캐시되어 같은 객체를 공유하죠.

autouse 픽스처: 요청 없이도 자동 적용

모든 테스트가 필요로 하는 공통 셋업이 있다면, 일일이 요청하는 대신 autouse=True를 붙일 수 있어요. 이러면 테스트가 픽스처를 요청하지 않아도 pytest가 자동으로 적용해요.

@pytest.fixture(autouse=True)
def append_first(order, first_entry):
    return order.append(first_entry)

이렇게 하면 두 테스트 모두 이 픽스처의 영향을 받아요. 물론 명시적으로 요청하는 것도 여전히 가능하고, 단지 "요청하지 않아도 된다"는 뜻이에요.

scope: 클래스·모듈·패키지·세션 단위로 공유

기본적으로 픽스처는 함수 단위(function) 로 매 테스트마다 새로 만들어져요. 그런데 네트워크 연결처럼 만드는 비용이 큰 자원은 테스트마다 다시 만들면 낭비예요. 이때 scope를 지정해 재사용 범위를 넓힐 수 있어요. 가능한 값은 function, class, module, package, session이에요.

# content of conftest.py
import smtplib

import pytest

@pytest.fixture(scope="module")
def smtp_connection():
    return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)

scope="module"로 두면 같은 모듈의 여러 테스트가 같은 smtp_connection 인스턴스를 공유해서, 연결을 한 번만 만드는 이득을 볼 수 있어요.

픽스처 파라미터화: 같은 셋업을 여러 설정으로

픽스처에 params를 지정하면, 각 파라미터 값마다 픽스처 함수가 실행되고 그에 의존하는 테스트도 함께 반복 실행돼요. 각 값은 request.param으로 접근할 수 있어요.

# content of conftest.py
import smtplib

import pytest

@pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"])
def smtp_connection(request):
    smtp_connection = smtplib.SMTP(request.param, 587, timeout=5)
    yield smtp_connection
    print(f"finalizing {smtp_connection}")
    smtp_connection.close()

이렇게 하면 이 픽스처를 쓰는 모든 테스트가 두 호스트에 대해 각각 실행돼요. 테스트 함수 코드는 전혀 바꿀 필요가 없어요.

픽스처 팩토리: 하나의 테스트에서 여러 결과 만들기

테스트 안에서 같은 셋업을 여러 번 호출해야 한다면, 데이터를 직접 반환하는 대신 데이터를 생성하는 함수를 반환하는 "팩토리(factory)" 패턴을 쓸 수 있어요.

@pytest.fixture
def make_customer_record():
    def _make_customer_record(name):
        return {"name": name, "orders": []}

    return _make_customer_record


def test_customer_records(make_customer_record):
    customer_1 = make_customer_record("Lisa")
    customer_2 = make_customer_record("Mike")
    customer_3 = make_customer_record("Meredith")

생성한 데이터를 정리해야 한다면, yield로 생성 함수를 넘기고 그 뒤에 해제 코드를 두는 것도 가능해요.

usefixtures: 픽스처 객체까지 필요 없을 때

테스트가 픽스처의 반환값은 필요 없지만 그 픽스처의 부수 효과는 필요할 때가 있어요. 대표적으로 "현재 작업 디렉토리를 비운 채 테스트를 시작하라" 같은 것들이죠. 이때 @pytest.mark.usefixtures를 쓰면 함수 인자로 요청하지 않아도 픽스처를 적용할 수 있어요.

@pytest.mark.usefixtures("cleandir")
class TestDirectoryInit:
    def test_cwd_starts_empty(self):
        assert os.listdir(os.getcwd()) == []

여러 픽스처를 지정하거나 모듈 전체에 적용하고 싶다면 아래처럼 쓸 수 있어요.

@pytest.mark.usefixtures("cleandir", "anotherfixture")
def test(): ...

pytestmark = pytest.mark.usefixtures("cleandir")

한 가지 주의할 점은, @pytest.mark.usefixtures픽스처 함수에는 적용할 수 없다는 거예요. 실수로 픽스처에 붙이면 오류가 나요.

계층별로 픽스처 덮어쓰기

테스트 스위트가 커지면 특정 디렉토리나 모듈에서 픽스처의 동작을 바꾸고 싶을 수 있어요. pytest는 conftest.py(디렉토리 단위), 테스트 모듈, 그리고 테스트의 직접 파라미터화 등 여러 수준에서 픽스처를 덮어쓰기(override)할 수 있게 지원해요. 한쪽의 픽스처를 그보다 좁은 범위에서 다시 정의하면, 그 범위의 테스트는 새 정의를 사용하게 되죠.

더 알아보기 (Learn more)