troubleshooting2024-06-10·8 min·294/348

pytest Fixture 스코프 문제 해결 가이드

pytest fixture의 스코프를 올바르게 설정하고 테스트 간 상태 공유 문제를 해결하는 방법을 알아봅니다.

pytest Fixture 스코프 문제 해결 가이드

pytest fixture는 테스트 코드에서 공통 리소스를 관리하는 강력한 도구입니다. 하지만 fixture 스코프를 잘못 설정하면 테스트 간 의존성 문제가 발생할 수 있습니다.

Environment

$ python --version
Python 3.11.5

$ pip show pytest
Name: pytest
Version: 7.4.3

$ pip show pytest-randomly
Name: pytest-randomly
Version: 3.15.0

Problem: 테스트 간 상태 공유 문제

# conftest.py
import pytest

@pytest.fixture
def database():
    # This creates a new connection for EACH test
    db = create_connection()
    yield db
    db.close()

# test_user.py
def test_create_user(database):
    user = database.create_user("joel")
    assert user.name == "joel"

def test_get_user(database):
    # Expects to find user from previous test, but fixture creates new connection
    user = database.get_user("joel")
    assert user is None  # FAILS - database is empty!

실행 결과:

$ pytest test_user.py -v
test_user.py::test_create_user PASSED
test_user.py::test_get_user FAILED

Analysis: Fixture 스코프 이해

# pytest fixture scopes:
# function (default) - runs once per test function
# class - runs once per test class
# module - runs once per module
# session - runs once per test session

# Display fixture scope information
$ pytest --fixtures -v | grep -A 2 "database"

# Check fixture call count
import pytest

@pytest.fixture(scope="function")
def counted_fixture():
    return object()

def test1(counted_fixture):
    pass

def test2(counted_fixture):
    pass

def test3(counted_fixture):
    pass

Solution: Fixture 스코프 설정

function 스코프 (기본값)

# conftest.py
@pytest.fixture(scope="function")
def fresh_database():
    """Each test gets a fresh database"""
    db = Database()
    db.create_tables()
    yield db
    db.drop_tables()
    db.close()

# 테스트 간 독립적
def test_user_creation(fresh_database):
    fresh_database.create_user("joel")
    # Fresh DB in next test

def test_user_query(fresh_database):
    # DB is empty - no user from previous test
    assert fresh_database.get_user_count() == 0

class 스코프

@pytest.fixture(scope="class")
def shared_database():
    """All tests in class share same database"""
    db = Database()
    db.create_tables()
    yield db
    db.drop_tables()

class TestUser:
    def test_create(self, shared_database):
        shared_database.create_user("joel")
    
    def test_get(self, shared_database):
        # User exists from test_create
        user = shared_database.get_user("joel")
        assert user is not None

session 스코프

@pytest.fixture(scope="session")
def test_database():
    """All tests share same database"""
    db = Database()
    db.create_tables()
    yield db
    db.drop_tables()

# test_setup.py
def test_system_initialization(test_database):
    test_database.initialize()

###autouse fixture

@pytest.fixture(autouse=True, scope="session")
def setup_test_environment():
    """Automatically runs for all tests"""
    os.environ["TESTING"] = "1"
    yield
    del os.environ["TESTING"]

@pytest.fixture(autouse=True)
def reset_state():
    """Runs before each test"""
    yield
    # Cleanup after each test
    cache.clear()

Advanced: 의존성 있는 Fixture

@pytest.fixture(scope="session")
def db_config():
    return {"host": "localhost", "port": 5432}

@pytest.fixture(scope="session")
def database(db_config):
    db = connect(db_config)
    yield db
    db.close()

@pytest.fixture(scope="function")
def db_session(database):
    """Transaction that rolls back after each test"""
    transaction = database.begin()
    yield database
    transaction.rollback()

# 테스트에서 사용
def test_user(db_session):
    db_session.execute("INSERT INTO users ...")
    # Automatically rolled back after test

Fixture 간 의존성 관리

@pytest.fixture
def base_url():
    return "http://localhost:8000"

@pytest.fixture
def api_client(base_url):
    return APIClient(base_url)

@pytest.fixture
def authenticated_client(api_client):
    api_client.login("admin", "password")
    return api_client

# Usage
def test_api(authenticated_client):
    response = authenticated_client.get("/api/data")
    assert response.status_code == 200

Troubleshooting

# 1. Fixture 재사용 확인
$ pytest --fixtures -v

# 2. Fixture 호출 횟수 확인
$ pytest -v --tb=short

# 3. Fixture 스코프 정보 출력
import pytest
pytest.fixture(autouse=True)(lambda: None)  # Debug fixture

# 4. Fixture 정의 위치 확인
$ pytest --co -q

# 5. Fixture 에러 추적
$ pytest -v --setup-show

Lessons Learned

  1. 스코프 선택: 테스트 간 상태 공유가 필요 없으면 function 스코프, 공유가 필요하면 class 또는 session 스코프를 사용하세요.

  2. 테스트 독립성: 테스트는 서로 독립적이어야 합니다. 공유 상태는 의도치 않은 테스트 실패를 초래합니다.

  3. autouse 주의: autouse fixture는 모든 테스트에 영향을 주므로 신중하게 사용하세요.

  4. 의존성 관리: Fixture 간 의존성이 복잡해지면, 각 fixture가 독립적으로 동작하도록 리팩토링하세요.

  5. 정리 로직: yield fixture의 teardown 부분에서 리소스를 정리하는 것을 잊지 마세요.


This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.