pytest의 fixture는 테스트에 필요한 준비물(의존성/리소스) 을 함수에 정의해두고 테스트 함수의 인자를 주입하는 형태로 재사용하는 기능이다.
(중복되는 setup/teardown 코드를 줄이기 위해 사용)
pytest @pytest.fixture로 등록된 함수는 테스트 함수의 파라미터 이름을 기준으로 매칭된다.
즉, pytest는 test_... 함수의 인자 목록을 보고 동일한 이름의 fixture를 찾아 실행한 뒤 반환값(또는 yield 값)을 인자로 주입한다.
import pytest
@pytest.fixture
def user():
return {"id": 1, "name": "test_user"}
def test_user_name(user):
assert user["name"] == "test_user"
위 코드에서 test_user_name는 user()를 직접 호출하지 않아도 pytest가 test_user_name(user)의 user 파라미터를 보고 user fixture를 실행한 결과를 주입해준다.
fixture는 scope 단위로 결과를 캐시하고, 같은 scope 안에서는 재사용한다.
(즉, scope="function"이면 테스트 함수마다 새로 만들고, 그 함수 안에서는 동일 fixture를 여러 번 요청해도 재사용된다.)
@pytest.fixture(scope="function")
def fx():
...
스코프의 종류는 다음과 같다
function: 테스트 함수마다 새로 생성class: 클래스 단위로 1번 생성module: 파일(모듈) 단위로 1번 생성package: 패키지 단위로 1번 생성session: 전체 테스트 실행 동안 1번 생성teardown은 테스트에서 사용한 리소스를 정리(cleanup)하기 위한 코드이며,
pytest fixture에서 yield를 기준으로 setup/teardown을 한 함수에서 구현할 수 있다.
@pytest.fixture(scope="function")
def db_session():
session = SessionLocal()
yield session # setup 완료 후 테스트에 session 전달
session.close() # teardown: fixture scope 종료 시 정리
def test_db_connection(db_session):
...
test_db_connection 함수가 호출되면 다음의 흐름으로 진행된다.
1. yield 이전: DB 세션 생성 (setup)
2. yield 지점: session을 pytest가 받아서 테스트 함수 인자로 주입
3. test_db_connection 실행 시작
4. test_db_connection 실행 종료
5. yield 이후: DB 세션 종료 (teardown)
pytest는 fixture 함수에 대해 generator 객체를 next()로 재개(resume) 해서 teardown을 수행하는 구조로 되어있다.
# pytest/src/_pytest/fixtures.py
def call_fixture_func(
fixturefunc: _FixtureFunc[FixtureValue], request: FixtureRequest, kwargs
) -> FixtureValue:
...
generator = fixturefunc(**kwargs) # 제너레이터 객체 생성
fixture_result = next(generator) # yield 까지 실행: setup 수행
# scope가 끝날 때 teardown을 호출하는 콜백 등록
finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator)
request.addfinalizer(finalizer)
...
return fixture_result # yield 값 반환
def _teardown_yield_fixture(fixturefunc, it) -> None:
try:
next(it) # teardown 수행
except StopIteration:
pass # 정상 종료
else:
fs, lineno = getfslineno(fixturefunc) # yield가 2개 이상이라면 에러 반환
fail(
f"fixture function has more than one 'yield':\n\n"
f"{Source(fixturefunc).indent()}\n"
f"{fs}:{lineno + 1}",
pytrace=False,
)
pytest는 첫 next(generator)로 setup을 수행하고 yield 값을 받아 테스트에 주입한다.
그리고 teardown을 즉시 실행하지 않고 _teardown_yield_fixture(..., generator)를 finalizer로 등록한다.
이후 테스트 scope가 끝나는 시점에 next(generator)를 다시 호출하여 teardown을 진행한다.
(teardown을 위한 fixture을 사용 시, yield를 정확히 한 번만 사용해야 된다는 것도 알 수 있다.)