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)
반복적 수동 생성: 피해야 할 패턴
관련 객체를 수동으로 인스턴스화하면 심각한 유지보수 문제가 발생합니다:
- 모든 테스트에 걸쳐 반복되는 부가 코드
- 동적이고 현실적인 데이터 생성 대신 하드코딩된 값 사용
- 테스트 데이터 설정 로직과 검증 로직의 경계 흐림
- 모델에 사소한 변경만 있어도 테스트가 즉시 실패함
문제 있는 코드 예시:
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와 원활하게 통합되어, 일관되고 현실적인 테스트 데이터를 생성합니다:
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를 사용하세요:
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
아직 댓글이 없습니다.