Skip to content

Testing with factory_boy and pytest

Stop writing brittle test fixtures by hand. factory_boy lets you declare rich, realistic test objects once and reuse them everywhere — and pytest-factoryboy turns those factories directly into pytest fixtures with zero boilerplate.


Prerequisites

pip install factory-boy pytest-factoryboy faker

Why factory_boy?

Static Django fixtures (JSON/YAML files) break silently when your schema changes, are painful to maintain, and make tests hard to read.
factory_boy is a fixtures replacement: you declare a factory class once, and it constructs objects (saved or unsaved) on demand, with sensible defaults you can override per-test.


Core Concepts

1. Basic Factory

tests/factories.py
import factory
from myapp.models import User

class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User

    first_name = "Jane"
    last_name  = "Doe"
    email      = factory.LazyAttribute(
        lambda obj: f"{obj.first_name}.{obj.last_name}@example.com".lower()
    )
    is_active  = True

Instantiation strategies:

user = UserFactory.build()    # Not saved to DB
user = UserFactory.create()   # Saved to DB
user = UserFactory()          # Shortcut for create()
user = UserFactory.stub()     # Lightweight object, no model involved

Override any attribute inline:

admin = UserFactory(first_name="Admin", is_staff=True)

2. Lazy Values

Declaration When to use
factory.LazyAttribute(lambda obj: ...) Value depends on other fields of the same object
factory.LazyFunction(callable) Value is computed fresh each time, no object access needed
factory.Sequence(lambda n: ...) Unique, auto-incrementing values
factory.Faker("email") Realistic fake data via the Faker library
import factory

class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User

    username   = factory.Sequence(lambda n: f"user_{n}")
    email      = factory.Faker("safe_email")
    date_joined = factory.LazyFunction(datetime.now)
    full_name   = factory.LazyAttribute(lambda obj: f"{obj.first_name} {obj.last_name}")

class AuthorFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Author

    name = factory.Faker("name")

class BookFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Book

    title  = factory.Faker("sentence", nb_words=4)
    author = factory.SubFactory(AuthorFactory)  # Auto-creates an Author

When you call BookFactory(), it automatically creates and saves the related Author too.


4. Traits — Preset Variations

Traits are opt-in bundles of overrides. They keep your factory DRY instead of spawning AdminUserFactory, InactiveUserFactory, etc.

class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model   = User
        exclude = ["is_superuser_trait"]  # keep internal flags off the model

    is_active = True
    is_staff  = False

    class Params:
        admin = factory.Trait(
            is_staff    = True,
            is_superuser = True,
        )
        inactive = factory.Trait(
            is_active = False,
        )

Usage:

regular = UserFactory()
admin   = UserFactory(admin=True)
banned  = UserFactory(inactive=True)

Integrating with pytest via pytest-factoryboy

pytest-factoryboy bridges factories and pytest fixtures. Call register() in conftest.py and your factory becomes a fixture — automatically.

Registration

tests/conftest.py
from pytest_factoryboy import register
from .factories import AuthorFactory, BookFactory

register(AuthorFactory)  # → fixtures: author, author_factory
register(BookFactory)    # → fixtures: book, book_factory

Two fixtures are created per registration:

Fixture What it provides
author A pre-built Author model instance
author_factory The factory class, so you can call it yourself

Basic Test

tests/test_models.py
import pytest

@pytest.mark.django_db
def test_book_has_author(book):
    assert book.author is not None
    assert book.author.name  # populated by Faker

No imports. No setup. Just ask for book and it arrives fully formed.


Overriding Attributes

Via @pytest.mark.parametrize

@pytest.mark.parametrize("author__name", ["Stephen King"])
@pytest.mark.django_db
def test_author_name(author):
    assert author.name == "Stephen King"

The author__name syntax follows factory_boy's double-underscore convention and overrides the attribute fixture.

Via a Fixture Override

@pytest.fixture
def author__name():
    return "Agatha Christie"

@pytest.mark.django_db
def test_named_author(author):
    assert author.name == "Agatha Christie"

Multiple Registrations (Fixture Flavors)

Register the same factory under different names with preset overrides:

tests/conftest.py
from pytest_factoryboy import register
from .factories import UserFactory

register(UserFactory, "regular_user")
register(UserFactory, "admin_user", is_staff=True, is_superuser=True)
register(UserFactory, "inactive_user", is_active=False)
@pytest.mark.django_db
def test_admin_can_access_admin_panel(admin_user, client):
    client.force_login(admin_user)
    response = client.get("/admin/")
    assert response.status_code == 200

Using the Factory Fixture Directly

When you need multiple instances or more control, use *_factory:

@pytest.mark.django_db
def test_book_listing(book_factory):
    books = [book_factory(title=f"Book {i}") for i in range(5)]
    assert len(books) == 5

LazyFixture — Injecting Fixtures as Factory Attributes

Sometimes you want a factory attribute to be another pytest fixture (not just a new object). Use LazyFixture:

tests/conftest.py
from pytest_factoryboy import register, LazyFixture
from .factories import BookFactory, AuthorFactory

register(AuthorFactory, "shared_author")
register(BookFactory, "book_with_shared_author",
         author=LazyFixture("shared_author"))

Or inline in parametrize:

import pytest
from pytest_factoryboy import LazyFixture

@pytest.mark.parametrize("book__author", [LazyFixture("shared_author")])
@pytest.mark.django_db
def test_book_uses_shared_author(book, shared_author):
    assert book.author == shared_author

Real-World Example

A complete setup for a blog app:

tests/factories.py
import factory
from faker import Factory as FakerFactory
from myapp.models import Author, Book

faker = FakerFactory.create()

class AuthorFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Author

    name  = factory.LazyFunction(lambda: faker.name())
    bio   = factory.LazyFunction(lambda: faker.paragraph())
    email = factory.LazyAttribute(
        lambda obj: f"{obj.name.lower().replace(' ', '.')}@example.com"
    )

    class Params:
        prolific = factory.Trait(bio="Wrote over 100 books.")

class BookFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Book

    title       = factory.LazyFunction(lambda: faker.sentence(nb_words=4))
    author      = factory.SubFactory(AuthorFactory)
    published   = True
    page_count  = factory.LazyFunction(lambda: faker.random_int(min=100, max=900))
tests/conftest.py
from pytest_factoryboy import register
from .factories import AuthorFactory, BookFactory

register(AuthorFactory)
register(BookFactory)
register(AuthorFactory, "prolific_author", prolific=True)
tests/test_books.py
import pytest

@pytest.mark.django_db
def test_book_is_published(book):
    assert book.published is True

@pytest.mark.parametrize("book__page_count", [42])
@pytest.mark.django_db
def test_short_book(book):
    assert book.page_count == 42

@pytest.mark.django_db
def test_prolific_author_bio(prolific_author):
    assert "100 books" in prolific_author.bio

@pytest.mark.django_db
def test_bulk_books(book_factory):
    books = [book_factory() for _ in range(10)]
    assert len(books) == 10
    assert all(b.author_id for b in books)

Quick Reference

register(MyFactory)
  → my_model          # instance fixture
  → my_model_factory  # factory class fixture

Override attribute in test:
  @pytest.mark.parametrize("my_model__field", [value])

Override attribute as fixture:
  @pytest.fixture
  def my_model__field():
      return value

Named registration:
  register(MyFactory, "variant_name", field=value)
  → variant_name, variant_name_factory

Cross-fixture dependency:
  register(MyFactory, author=LazyFixture("other_fixture"))

Tips & Gotchas

  • build() vs create(): Use build() in unit tests that don't need DB access — it's much faster. Use create() (the default for DjangoModelFactory) for integration tests.
  • Sequence resets: Sequences increment globally per test session. If you rely on specific sequence values, reset with factory.reset_sequence() in a fixture.
  • @pytest.mark.django_db: Don't forget this on any test that touches registered factories backed by DjangoModelFactory.
  • Naming conflicts: pytest-factoryboy raises an error early if a factory name conflicts with a model name. Use the _name parameter in register() to resolve.
  • Post-generation hooks: Post-generation fixtures expose only the extracted value, not the result of the hook itself. Defer logic that depends on post-generation results until the test runs.