홈으로 돌아가기

Django pytest 테스트에서 factory_boy 팩토리

이 기사는 pytest 테스트에서 관련 Django 객체 생성을 자동화하기 위한 factory_boy 사용법을 보여줍니다. ForeignKey를 위한 SubFactory, ManyToMany를 위한 post_generation, fixtures와의 통합을 다룹니다. 예제는 전형적인 라이브러리 모델 테스트 시나리오를 다룹니다.

factory_boy: 안정적인 Django 테스트를 위한 팩토리
Advertisement 728x90

factory_boy와 pytest를 활용한 Django 모델 테스트: 실무 가이드

factory_boy 팩토리는 테스트에서 관련된 Django 모델 인스턴스를 간편하게 생성해 줍니다. ForeignKey나 ManyToMany 관계를 가진 모델을 수십 개씩 수동으로 만들 필요 없이, Faker를 활용해 현실감 있고 다양성 있는 테스트 데이터를 자동으로 생성합니다. 이 방식은 테스트 작성 속도를 높이고, 중복되는 부가 코드를 제거하며, 모델 구조가 변경될 때 발생하는 테스트 붕괴를 최소화합니다.

예시로 도서관 도메인을 사용하겠습니다: Genre(장르), Language(언어), Author(저자), Book(도서) — 그리고 이들 간의 관계입니다.

class Genre(models.Model):
    name = models.CharField(max_length=200)

class Language(models.Model):
    name = models.CharField(max_length=20)

class Author(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    date_of_birth = models.DateField(null=True, blank=True)
    date_of_death = models.DateField(null=True, blank=True)

class Book(models.Model):
    title = models.CharField(max_length=200)
    genre = models.ForeignKey('Genre', on_delete=models.CASCADE, null=True)
    language = models.ForeignKey('Language', on_delete=models.CASCADE, null=True)
    author = models.ManyToManyField('Author')
    summary = models.TextField(max_length=1000)
    isbn = models.CharField(max_length=13)

반복적 수동 생성: 피해야 할 패턴

관련 객체를 수동으로 인스턴스화하면 심각한 유지보수 문제가 발생합니다:

Google AdInline article slot
  • 모든 테스트에 걸쳐 반복되는 부가 코드
  • 동적이고 현실적인 데이터 생성 대신 하드코딩된 값 사용
  • 테스트 데이터 설정 로직과 검증 로직의 경계 흐림
  • 모델에 사소한 변경만 있어도 테스트가 즉시 실패함

문제 있는 코드 예시:

def test_book_creation():
    genre = Genre.objects.create(name="공상과학소설")
    language = Language.objects.create(name="러시아어")
    author = Author.objects.create(
        first_name="아르카디",
        last_name="스트루가츠키",
        date_of_birth="1925-08-28"
    )
    book = Book.objects.create(
        title="로드사이드 피크닉",
        genre=genre,
        language=language,
        summary="가장 평판 높은 중편 소설 중 하나...",
        isbn="9785171180975"
    )
    book.author.add(author)
    assert book.display_author() == "스트루가츠키"

이 방식은 확장성이 없습니다. 수십 개의 복잡하게 얽힌 객체가 필요한 상황에서는 관리가 어려워지고 오류 발생 가능성이 급격히 증가합니다.

Faker 기반 기본 팩토리

factory_boy는 Django와 Faker와 원활하게 통합되어, 일관되고 현실적인 테스트 데이터를 생성합니다:

Google AdInline article slot
class AuthorFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Author

    first_name = factory.Faker("first_name")
    last_name = factory.Faker("last_name")
    date_of_birth = factory.Faker("date_time")
    date_of_death = factory.Faker("date_time")

이제 저자 생성은 단 한 줄로 완료됩니다:

author = AuthorFactory.create()

다른 모델에도 동일한 패턴을 적용할 수 있습니다:

class GenreFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Genre
    name = factory.Faker("name")

class LanguageFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Language
    name = factory.Faker("name")

관련 모델 처리 방법

ForeignKey 필드에는 SubFactory를 사용하세요:

Google AdInline article slot
class BookFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Book

    title = factory.Faker("sentence")
    genre = factory.SubFactory(GenreFactory)
    language = factory.SubFactory(LanguageFactory)
    summary = factory.Faker("paragraph")
    isbn = str(random.randint(1000000000000, 9999999999999))

ManyToManyField의 경우 @factory.post_generation 데코레이터를 활용하세요:

@factory.post_generation
def author(self, create, extracted, **kwargs):
    if not create or not extracted:
        return
    self.author.add(*extracted)

깔끔하고 읽기 쉬운 테스트 예시:

def test_book_creation():
    author1 = AuthorFactory.create()
    author2 = AuthorFactory.create()
    book = BookFactory.create(author=[author1, author2])
    assert len(book.author.all()) == 2

pytest 고정값(fixtures)을 통한 통합

conftest.py 파일에서 재사용 가능한 고정값을 정의하세요:

@pytest.fixture(scope='function')
def test_book_factory() -> Book:
    author1: Author = AuthorFactory.create()
    author2: Author = AuthorFactory.create()
    book_create: Book = BookFactory.create(author=[author1, author2])
    return book_create

그리고 test_models.py에서 다음과 같이 사용합니다:

@pytest.mark.django_db
def test_model_book(test_book_factory: Book) -> None:
    title: str = test_book_factory.title
    genre: Genre = test_book_factory.genre
    language: Language = test_book_factory.language
    summary: str = test_book_factory.summary
    isbn: str = test_book_factory.isbn
    authors: QuerySet = test_book_factory.author.all()

    assert len(summary) <= 1000
    assert len(title) <= 200
    assert len(isbn) <= 13
    assert isinstance(title, str)
    assert isinstance(genre, Genre)
    assert isinstance(language, Language)
    assert isinstance(summary, str)
    assert isinstance(isbn, str)

    for author in authors:
        assert isinstance(author, Author)

@pytest.mark.django_db 데코레이터는 각 테스트마다 격리된 임시 데이터베이스를 생성하여, 테스트 간 독립성을 완전히 보장합니다.

핵심 요약

  • SubFactory: ForeignKey 관련 객체를 필요 시 자동 생성
  • @post_generation: ManyToManyField 관계를 처리하며, 미리 생성된 인스턴스 목록을 받아 처리
  • pytest 고정값은 데이터 준비검증 로직을 명확히 분리해 코드 가독성과 유지보수성을 높임
  • Faker는 다양한 실제 데이터를 생성해 엣지 케이스 커버리지를 향상시킴
  • 모델 변경 시 팩토리 파일 하나만 수정하면 되며, 수십 개의 산발된 테스트 파일을 모두 고칠 필요 없음

— Editorial Team

Advertisement 728x90

다음 읽기