Back to Home

Factories factory_boy in Django pytest tests

The article demonstrates the use of factory_boy for automating the creation of related Django objects in pytest tests. It covers SubFactory for ForeignKey, post_generation for ManyToMany, and integration with fixtures. Examples cover typical library model testing scenarios.

Factory_boy: factories for reliable Django tests
Advertisement 728x90

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:

Google AdInline article slot
  • 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:

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")

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:

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))

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

  • SubFactory automatically creates associated ForeignKey objects on demand
  • @post_generation handles ManyToManyField relationships, 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

Advertisement 728x90

Read Next