Django Model Testing with factory_boy and pytest
factory_boy factories simplify generating related Django model instances in tests. Instead of manually creating dozens of models with ForeignKey and ManyToMany relationships, factories leverage Faker to auto-generate realistic, varied test data. This accelerates test writing, eliminates boilerplate duplication, and minimizes breakage when models evolve.
Let’s use a library domain: Genre, Language, Author, and Book (with relationships).
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)
The Anti-Pattern: Manual Object Creation
Manually instantiating related objects introduces serious maintainability issues:
- Repetitive boilerplate across every test
- Hardcoded values instead of dynamic, realistic data generation
- Blurred separation between test data setup and assertion logic
- Fragile tests that break with even minor model changes
Example of problematic code:
def test_book_creation():
genre = Genre.objects.create(name="Science Fiction")
language = Language.objects.create(name="Russian")
author = Author.objects.create(
first_name="Arkady",
last_name="Strugatsky",
date_of_birth="1925-08-28"
)
book = Book.objects.create(
title="Roadside Picnic",
genre=genre,
language=language,
summary="One of the most acclaimed novellas...",
isbn="9785171180975"
)
book.author.add(author)
assert book.display_author() == "Strugatsky"
This approach doesn’t scale — it becomes unwieldy and error-prone with complex scenarios involving dozens of interrelated objects.
Basic Factories with Faker
factory_boy integrates seamlessly with Django and Faker to generate consistent, realistic test data:
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")
Creating an author is now one clean line:
author = AuthorFactory.create()
Same pattern applies to other models:
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")
Handling Related Models
For ForeignKey fields, use 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))
For ManyToManyField, use @factory.post_generation:
@factory.post_generation
def author(self, create, extracted, **kwargs):
if not create or not extracted:
return
self.author.add(*extracted)
A clean, readable test:
def test_book_creation():
author1 = AuthorFactory.create()
author2 = AuthorFactory.create()
book = BookFactory.create(author=[author1, author2])
assert len(book.author.all()) == 2
Integrating with pytest via Fixtures
In conftest.py, define a reusable fixture:
@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
Then in 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)
The @pytest.mark.django_db decorator spins up an isolated, temporary database for each test — ensuring full test independence.
Key Takeaways
SubFactoryautomatically creates associatedForeignKeyobjects on demand@post_generationhandlesManyToManyFieldrelationships, accepting a list of pre-created instances- pytest fixtures cleanly separate data setup from assertion logic
- Faker generates diverse, realistic data — improving edge-case coverage
- Model changes require updates only in factories — not across dozens of scattered test files
— Editorial Team
No comments yet.