[Pytest]테스트를 위한 Fixture patterns

Jay·2024년 2월 26일
0
post-thumbnail

개요

구현하는 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을 적용해보고자 합니다.



Factory 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를 정의하지 않아도 되고, 사용자가 원하는 데이터를 인자로 전달하여 특정 객체를 생성하여 사용할 수 있습니다.



Composing Pattern

관계를 가지는 객체를 생성할 필요가 있는 경우가 있습니다. 아래의 경우, 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 PatternFactory Pattern과 마찬가지로 객체를 반환하지 않고 함수를 반환합니다. 반환된 함수를 사용하여 사용자가 원하는 객체끼리의 관계를 가지는 객체를 생성할 수 있으며, 특정 관계를 지니는 객체를 생성하기 위한 fixture를 별도로 구현하지 않아도 됩니다. 또한 인자로 전달받은 Factory Pattern fixture를 사용해서 기본값을 지니는 객체를 지정하여 생성할 수 있습니다.



Summary

시스템의 복잡성이 낮은 경우에는 간단한 fixture의 경우 위의 패턴을 사용하기 보다는 하드코딩된 fixture를 구현하여 사용하는 것이 빠를 수 있습니다. 하지만 시스템의 기능이 증가하고, 테스트의 범위가 증가할수록 fixture를 관리할 수 있는 별도의 구조를 사용하는 것이 용이하며, 위와 같은 패턴들이 사용될 수 있습니다. 복잡해지는 테스트 코드를 구조적으로 관리하고 편리하게 사용하기 위해서 위와 같은 패턴들을 적용해보는 것이 좋은 경험일것 같습니다.



reference

0개의 댓글