pytest
를 사용하여 Rest API의 테스트 코드를 작성하는 작업을 진행하였습니다. pytest
에서 제공하는 fixture
를 사용해서 테스트에 필요한 DB 환경을 구성하는 도중, 마주했던 문제를 정리해보고자 합니다.
fixture
는 테스트 환경(데이터 베이스나 데이터셋, 설정 파일, 환경 변수 등)을 특정 상태로 유지하여 일관적인 테스트 환경을 제공하기 위해 기능입니다. fixture
를 통해서 복잡한 시스템이나 테스트 구조에서 안정적으로 테스트를 설계하고 작성할 수 있으며, 파라미터화하여 테스트에서 간편하게 사용하는 것이 가능해집니다.
import pytest
from django.db import models
class Pizza(models.Model):
name = models.CharField(max_length=64)
@pytest.mark.django_db
@pytest.fixture
def plane_pizza():
return Pizza.objects.create(name='plane')
def test_get_plane_pizza(plane_pizza):
assert plane_pizza.name == 'plane'
name
이 plane
인 Pizza
객체가 필요한 테스트가 있을때, 위와 같이 해당 객체를 생성하고 반환하는 fixture
를 구현하여 테스트 함수의 인자로 전달하여 사용할 수 있습니다. 또한 하나의 fixture
는 다른 fixture
의 인자로 전달되어 재사용할 수 있어 테스트에 필요한 데이터를 관리하는데 용이하게 사용할 수 있습니다. 이와 같은 장점으로 데이터베이스의 구조가 복잡할 수록 이에 따라 fixture
를 설계하여 테스트코드에 사용할 수 있으며, 테스트 수가 늘어남에도 반복적으로 해당 객체를 생성하는 코드 대신 fixture
를 사용하여 코드의 수를 줄일 수 있습니다.
fixture
의 인자 중 scope
가 존재합니다. scope
는 해당 fixture
의 수명을 나타내는 것으로 fixture
가 생성되고 삭제되는 빈도를 정의하게 됩니다. scope
에는 총 4가지가 존재하며 수명은 아래와 같습니다.
- function - 각 테스트 함수가 실행될때마다 생성되고 삭제됩니다.
- class - 각 테스트 클래스가 실행될때마다 생성되고 삭제됩니다.
- module - 각 테스트 모듈/파일이 실행될 때마다 생성되고 삭제됩니다.
- session - 각 테스트 세션이 실행될 때마다 생성되고 삭제됩니다.
scope
는 fixture
가 얼마나 자주 생성되고 삭제되는지를 제어하게 됩니다. 기본 설정값은 function
이며, 각각의 테스트 함수가 실행될때마다 fixture
가 생성되고 삭제되는 것을 기본으로 동작합니다. 따라서 fixture
의 특성이나 소모되는 자원 비용에 따라서 scope
를 다르게 설정해주어 테스트의 효율성을 높일 필요가 있습니다.
import pytest
from django.contrib.auth import get_user_model
@pytest.mark.django_db
@pytest.fixture(scope='module')
def authenticated_user():
data = {...}
return get_user_model().objects.create_user(**data)
필자의 경우 유저 인증/인가와 관련된 API의 테스트 코드를 작성하기위해, 인증된 사용자 객체를 생성하는 fixture
를 사용하기 위해 위와 같이 코드를 작성하였습니다. 하지만 아래와 같은 런타임에러 메세지를 마주하며 테스트는 정상적으로 동작하지 않았습니다.
RuntimeError: Database access not allowed, use the "django_db" mark, or the "db" or "transactional_db" fixtures to enable it.
에러 메세지에 따르면 DB에 접근하기 위해서는 pytest
의 django_db
나 db
마크 어노테이션을 사용하거나 transactional_db fixture
를 사용하라는 것이었습니다. 하지만 위의 authenticated_user
함수 부분에 django_db
어노테이션을 사용하였지만 DB에 접근이 안되는 것입니다.
관련 이슈를 검색하던 도중, DB에 접근하는 fixture
는 scope
가 function
으로 제한되어 있다는 것을 알게되었고, 위의 코드를 아래와 같이 수정한 후에 테스트를 정상적으로 실행할 수 있었습니다.
@pytest.mark.django_db
@pytest.fixture # or pytest.fixture(scope='function')
def authenticated_user():
data = {...}
return get_user_model().objects.create_user(**data)
테스트에 사용할 fixture
별로 올바른 scope
를 설정하는 것은 테스트를 올바른 수준의 독립과 효율성을 보장하기 위해서 매우 중요합니다. 따라서 적절한 scope
를 결정하기 위해서는 테스트 독립성, 성능, 의존성, 사이드이펙트, 데이터 무결성, 효율성 등을 고려하며, 올바른 scope
를 결정하는데 몇가지 추천되는 규칙이 존재합니다.
각 테스트별로 데이터가 독립되어야 하는 fixture
데이터에서 사용합니다. fixture
가 생성되고 제거되는데 많은 자원이 소모되지 않고 사이드 이펙트가 존재하지 않아야하는 fixture
에 사용합니다.
테스트 클래스 안의 테스트 메소드들이 서로 같은 데이터를 공유할 필요가 있는 경우 사용합니다. 클래스 내부의 많은 수들의 테스트가 실행하기에 필요한 fixture
를 생성하고 삭제하는데 필요한 자원을 줄일 수가 있으며, 클래스 내부의 테스트 메소드들이 서로 사이드이펙트를 가지지 않도록 주의해야합니다.
하나의 모듈 안의 모든 테스트 함수가 공유해야하는 fixture
데이터에 사용합니다. class scope
와 마찬가지로 테스트에 필요한 fixture
를 생성하고 삭제하는데 필요한 자원을 줄일 수 있습니다.
모든 테스트가 공유해야하는 자원을 생성하는데 session scope
를 사용합니다. 시간이 많이 소모되는 데이터 세팅 시나리오나, 모든 자원이 공유하는 네트워크나 DB를 다루는 fixture
에 사용하는 것이 좋습니다.