구현하는 API 수가 증가하며, 테스트의 복잡성도 점차 증가해져 갔습니다. 특정 조건에서의 API 호출 결과를 테스트하기 위해 특정 관계의 객체들 혹은 같은 클래스의 객체가 여러개 필요한 경우가 생겼는데, 이를 관리하기 위해 fixture
를 설계하는 패턴에 관한 글을 읽게되었습니다. 해당 게시글의 일부를 정리해보고 적용해보고자 합니다.
현재 개발중인 서비스는 호스트 사용자가 운영중인 경기에 다른 사용자가 참여를 신청하는 기능을 제공합니다. 따라서 여러개의 사용자 객체가 필요하며, 경기 객체와의 매핑을 통해 참여 기록을 생성하였습니다. 하지만 고정된 객체만 반환하는 fixture
로 인하여 여러개의 사용자 객체를 생성하기 위해서는 fixture
또한 여러개를 구현했어야 했습니다.
import pytest
@pytest.fixture(scope='function')
def authenticated_user_data():
data = {
'email':'mitiuser1@miti.com',
'nickname':'mitiuser1',
'password':'Mitiuser1234!',
'name':'김미티',
'phone':'01032323232',
'is_authenticated':True,
'birthday': '2000-01-01'
}
return data
@pytest.fixture(scope='function')
@pytest.mark.django_db
def authenticated_user_object(authenticated_user_data):
user = get_user_model().objects.create_user(
**authenticated_user_data
)
return user
위와 같은 fixture
로는 여러개의 객체를 생성하고 관계를 형성하기 위한 fixture
를 별도로 생성했어야 했기에, 테스트하기 위한 특정 DB 상태를 구현하기 위해서는 작성해야하는 코드의 양또한 비례하여 증가하였습니다. 이를 해결하기 위해 여러 fixture pattern
중에서 Factory Pattern과 Composing Pattern을 적용해보고자 합니다.
팩토리 패턴은 fixture
를 사용할때, 인자를 통해 개발자가 원하는 fixture
를 생성할 수 있도록 도와줍니다. 기존의 코드에서는 하드코딩를 통해 고정된 데이터를 지니는 객체를 생성해서 반환한 반면, 팩토리 패턴에서는 인자를 통해 데이터를 전달하여 fixture
를 생성할 수 있습니다.
@pytest.fixture(scope='function')
def authenticated_user_data():
data = {
'email':'mitiuser1@miti.com',
'nickname':'mitiuser1',
'password':'Mitiuser1234!',
'name':'김미티',
'phone':'01032323232',
'is_authenticated':True,
'birthday': '2000-01-01'
}
return data
위와 같이 정의된 fixture
는 하드 코딩된 데이터를 고정적으로 반환합니다.하지만 아래와 같이 수정할 수 있습니다.
@pytest.fixture(scope='function')
def make_authenticated_user():
def make(
email='mitiuser1@miti.com',
nickname='mitiuser1',
password='Mitiuser1234!',
name='김미티',
is_authenticated=True,
birthday='2000-01-01'
):
return {
'email': email,
'nickname': nickname,
'password': password,
'name': name.
'is_authenticated': is_authenticated,
'birthday': birthday
}
return make
def test(make_authenticated_user):
user1 = make_authenticated_user(email='testuser1@miti.com', ...)
user2 = make_authenticated_user(email='testuser2@miti.com', ...)
make_authenticated_user
는 객체를 반환하는 대신 함수를 make
로 정의된 함수를 반환합니다. 이 함수는 궁극적으로 객체를 생성하는 역할을 수행하며 생성된 객체를 반환하는 역할을 수행합니다. 이와 같은 Factory pattern
을 통해 여러개의 객체를 생성하기 위해 여러 fixture
를 정의하지 않아도 되고, 사용자가 원하는 데이터를 인자로 전달하여 특정 객체를 생성하여 사용할 수 있습니다.
관계를 가지는 객체를 생성할 필요가 있는 경우가 있습니다. 아래의 경우, Sale
모델 객체는 Customer
모델 객체를 참조하고 있으며, fixture
함수 내부에서 관계를 매핑하여 객체가 생성되고 있습니다.
@pytest.fixture
def make_transaction():
def make(amount, sku, ...):
customer = Customer(...)
sale = Sale(amount=amount, sku=sku, customer=customer, ...)
transaction = Transaction(sale=sale, ... )
return transaction
return make
하지만 이러한 fixture
의 경우, 위와 마찬가지로 고정된 관계를 지니는 객체만 생성할 수 있으며, 테스트에 필요한 여러 관계들을 다양하게 만들기 위해서는 여러개의 fixture
를 구현해야 합니다. 이러한 문제를 해결하는데 사용할 수 있는 패턴으로는 Composing Pattern
이 있습니다.
@pytest.fixture
def make_transaction(make_customer, make_sale):
def make(transaction_id, customer=None, sale=None):
if customer is None:
customer = make_customer()
if sale is None:
sale = make_sale()
transaction = Transaction(
transaction_id=transaction_id,
customer=customer,
sale=sale,
)
return transaction
return make
Composing Pattern
은 Factory Pattern
과 마찬가지로 객체를 반환하지 않고 함수를 반환합니다. 반환된 함수를 사용하여 사용자가 원하는 객체끼리의 관계를 가지는 객체를 생성할 수 있으며, 특정 관계를 지니는 객체를 생성하기 위한 fixture
를 별도로 구현하지 않아도 됩니다. 또한 인자로 전달받은 Factory Pattern fixture
를 사용해서 기본값을 지니는 객체를 지정하여 생성할 수 있습니다.
시스템의 복잡성이 낮은 경우에는 간단한 fixture
의 경우 위의 패턴을 사용하기 보다는 하드코딩된 fixture
를 구현하여 사용하는 것이 빠를 수 있습니다. 하지만 시스템의 기능이 증가하고, 테스트의 범위가 증가할수록 fixture
를 관리할 수 있는 별도의 구조를 사용하는 것이 용이하며, 위와 같은 패턴들이 사용될 수 있습니다. 복잡해지는 테스트 코드를 구조적으로 관리하고 편리하게 사용하기 위해서 위와 같은 패턴들을 적용해보는 것이 좋은 경험일것 같습니다.