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¶
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¶
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:
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}")
3. SubFactory — Related Objects¶
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:
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¶
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¶
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:
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:
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:
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))
from pytest_factoryboy import register
from .factories import AuthorFactory, BookFactory
register(AuthorFactory)
register(BookFactory)
register(AuthorFactory, "prolific_author", prolific=True)
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()vscreate(): Usebuild()in unit tests that don't need DB access — it's much faster. Usecreate()(the default forDjangoModelFactory) 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 byDjangoModelFactory.- Naming conflicts:
pytest-factoryboyraises an error early if a factory name conflicts with a model name. Use the_nameparameter inregister()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.