[Backend] pytest로 테스트 코드 작성하기

alirz-pixel·2025년 12월 12일

backend

목록 보기
9/11

Fixture

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_nameuser()를 직접 호출하지 않아도 pytest가 test_user_name(user)user 파라미터를 보고 user fixture를 실행한 결과를 주입해준다.

Fixture Scope

fixture는 scope 단위로 결과를 캐시하고, 같은 scope 안에서는 재사용한다.
(즉, scope="function"이면 테스트 함수마다 새로 만들고, 그 함수 안에서는 동일 fixture를 여러 번 요청해도 재사용된다.)

@pytest.fixture(scope="function")
def fx():
	...

스코프의 종류는 다음과 같다

  • function: 테스트 함수마다 새로 생성
  • class: 클래스 단위로 1번 생성
  • module: 파일(모듈) 단위로 1번 생성
  • package: 패키지 단위로 1번 생성
  • session: 전체 테스트 실행 동안 1번 생성

teardown

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 관련 코드 링크

# 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를 정확히 한 번만 사용해야 된다는 것도 알 수 있다.)

0개의 댓글