[Pytest]fixture와 scope

Jay·2024년 2월 7일
1
post-thumbnail

개요

pytest를 사용하여 Rest API의 테스트 코드를 작성하는 작업을 진행하였습니다. pytest에서 제공하는 fixture를 사용해서 테스트에 필요한 DB 환경을 구성하는 도중, 마주했던 문제를 정리해보고자 합니다.

Fixture

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'

nameplanePizza 객체가 필요한 테스트가 있을때, 위와 같이 해당 객체를 생성하고 반환하는 fixture를 구현하여 테스트 함수의 인자로 전달하여 사용할 수 있습니다. 또한 하나의 fixture는 다른 fixture의 인자로 전달되어 재사용할 수 있어 테스트에 필요한 데이터를 관리하는데 용이하게 사용할 수 있습니다. 이와 같은 장점으로 데이터베이스의 구조가 복잡할 수록 이에 따라 fixture를 설계하여 테스트코드에 사용할 수 있으며, 테스트 수가 늘어남에도 반복적으로 해당 객체를 생성하는 코드 대신 fixture를 사용하여 코드의 수를 줄일 수 있습니다.


scope

fixture의 인자 중 scope가 존재합니다. scope는 해당 fixture의 수명을 나타내는 것으로 fixture가 생성되고 삭제되는 빈도를 정의하게 됩니다. scope에는 총 4가지가 존재하며 수명은 아래와 같습니다.

  1. function - 각 테스트 함수가 실행될때마다 생성되고 삭제됩니다.
  2. class - 각 테스트 클래스가 실행될때마다 생성되고 삭제됩니다.
  3. module - 각 테스트 모듈/파일이 실행될 때마다 생성되고 삭제됩니다.
  4. session - 각 테스트 세션이 실행될 때마다 생성되고 삭제됩니다.

scopefixture가 얼마나 자주 생성되고 삭제되는지를 제어하게 됩니다. 기본 설정값은 function이며, 각각의 테스트 함수가 실행될때마다 fixture가 생성되고 삭제되는 것을 기본으로 동작합니다. 따라서 fixture의 특성이나 소모되는 자원 비용에 따라서 scope를 다르게 설정해주어 테스트의 효율성을 높일 필요가 있습니다.



Issue

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에 접근하기 위해서는 pytestdjango_dbdb 마크 어노테이션을 사용하거나 transactional_db fixture를 사용하라는 것이었습니다. 하지만 위의 authenticated_user함수 부분에 django_db 어노테이션을 사용하였지만 DB에 접근이 안되는 것입니다.
관련 이슈를 검색하던 도중, DB에 접근하는 fixturescopefunction으로 제한되어 있다는 것을 알게되었고, 위의 코드를 아래와 같이 수정한 후에 테스트를 정상적으로 실행할 수 있었습니다.

@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 결정

테스트에 사용할 fixture별로 올바른 scope를 설정하는 것은 테스트를 올바른 수준의 독립과 효율성을 보장하기 위해서 매우 중요합니다. 따라서 적절한 scope를 결정하기 위해서는 테스트 독립성, 성능, 의존성, 사이드이펙트, 데이터 무결성, 효율성 등을 고려하며, 올바른 scope를 결정하는데 몇가지 추천되는 규칙이 존재합니다.

Function Scope

각 테스트별로 데이터가 독립되어야 하는 fixture 데이터에서 사용합니다. fixture가 생성되고 제거되는데 많은 자원이 소모되지 않고 사이드 이펙트가 존재하지 않아야하는 fixture에 사용합니다.

Class Scope

테스트 클래스 안의 테스트 메소드들이 서로 같은 데이터를 공유할 필요가 있는 경우 사용합니다. 클래스 내부의 많은 수들의 테스트가 실행하기에 필요한 fixture를 생성하고 삭제하는데 필요한 자원을 줄일 수가 있으며, 클래스 내부의 테스트 메소드들이 서로 사이드이펙트를 가지지 않도록 주의해야합니다.

Module Scope

하나의 모듈 안의 모든 테스트 함수가 공유해야하는 fixture 데이터에 사용합니다. class scope와 마찬가지로 테스트에 필요한 fixture를 생성하고 삭제하는데 필요한 자원을 줄일 수 있습니다.

Session Scope

모든 테스트가 공유해야하는 자원을 생성하는데 session scope를 사용합니다. 시간이 많이 소모되는 데이터 세팅 시나리오나, 모든 자원이 공유하는 네트워크나 DB를 다루는 fixture에 사용하는 것이 좋습니다.



reference

0개의 댓글