factory_boy Advanced: Traits, Sub-Factories, Batch Creation, and Django Integration
factory_boy is Python's most widely used test object factory library. The basics — factory.Factory and factory.django.DjangoModelFactory — are well documented, but the advanced features are what make large test suites maintainable. This guide covers traits, sub-factories, batch creation, lazy attributes, and the patterns that keep factories composable as your models grow.
Setup
pip install factory-boyFor Django:
# In conftest.py or a dedicated factories.py file
import factory
from factory.django import DjangoModelFactory
from myapp.models import User, Profile, PostTraits
Traits are named flag configurations that toggle sets of fields. They replace the need for multiple separate factory subclasses.
class UserFactory(DjangoModelFactory):
class Meta:
model = User
username = factory.Sequence(lambda n: f'user_{n}')
email = factory.LazyAttribute(lambda obj: f'{obj.username}@example.com')
is_active = True
is_staff = False
is_superuser = False
class Params:
# Defining traits
admin = factory.Trait(
is_staff=True,
is_superuser=True,
)
inactive = factory.Trait(
is_active=False,
)
staff = factory.Trait(
is_staff=True,
)Usage:
user = UserFactory() # regular active user
admin = UserFactory(admin=True) # staff + superuser
inactive_admin = UserFactory(admin=True, inactive=True) # both traitsTraits can reference each other and set any number of fields. They compose cleanly:
class PostFactory(DjangoModelFactory):
class Meta:
model = Post
title = factory.Sequence(lambda n: f'Post {n}')
author = factory.SubFactory(UserFactory)
status = 'draft'
published_at = None
class Params:
published = factory.Trait(
status='published',
published_at=factory.LazyFunction(timezone.now),
)
featured = factory.Trait(
is_featured=True,
)Lazy Attributes
factory.LazyAttribute computes a field value from other fields on the same object:
class ProfileFactory(DjangoModelFactory):
class Meta:
model = Profile
user = factory.SubFactory(UserFactory)
display_name = factory.LazyAttribute(lambda obj: obj.user.username.title())
bio = factory.LazyAttribute(lambda obj: f'Hello, I am {obj.display_name}')
avatar_url = factory.LazyAttribute(
lambda obj: f'https://avatars.example.com/{obj.user.username}.jpg'
)factory.LazyFunction is for callables that don't need access to the object:
class EventFactory(DjangoModelFactory):
class Meta:
model = Event
name = factory.Sequence(lambda n: f'Event {n}')
start_date = factory.LazyFunction(timezone.now)
end_date = factory.LazyAttribute(
lambda obj: obj.start_date + timedelta(hours=2)
)Sub-Factories
factory.SubFactory creates related objects automatically:
class CommentFactory(DjangoModelFactory):
class Meta:
model = Comment
post = factory.SubFactory(PostFactory)
author = factory.SubFactory(UserFactory)
body = factory.Sequence(lambda n: f'Comment {n} body text')
created_at = factory.LazyFunction(timezone.now)Building a comment automatically creates a post and a user. Override at any level:
# Use an existing post
post = PostFactory()
comment = CommentFactory(post=post)
# Override the sub-factory's fields
comment = CommentFactory(post__title='Custom Title', author__username='alice')The double-underscore syntax (post__title) passes overrides into the sub-factory.
Related Factory and M2M
For reverse relations (one-to-many) and many-to-many, use factory.RelatedFactory and factory.post_generation:
class UserFactory(DjangoModelFactory):
class Meta:
model = User
username = factory.Sequence(lambda n: f'user_{n}')
# Creates a Profile after the User is saved
profile = factory.RelatedFactory(
'myapp.tests.factories.ProfileFactory',
factory_related_name='user',
)For M2M:
class PostFactory(DjangoModelFactory):
class Meta:
model = Post
title = factory.Sequence(lambda n: f'Post {n}')
@factory.post_generation
def tags(self, create, extracted, **kwargs):
if not create:
return # In build mode, skip M2M
if extracted:
for tag in extracted:
self.tags.add(tag)Usage:
tag1 = TagFactory()
tag2 = TagFactory()
post = PostFactory(tags=[tag1, tag2])Batch Creation
factory_boy has built-in batch helpers:
# Build (no DB) a list of 10 users
users = UserFactory.build_batch(10)
# Create (DB) a list of 5 posts with a shared author
author = UserFactory()
posts = PostFactory.create_batch(5, author=author)
# Create with a trait
admins = UserFactory.create_batch(3, admin=True)build_batch and create_batch accept the same overrides as build() and create().
Sequences
factory.Sequence provides auto-incrementing values:
class ProductFactory(DjangoModelFactory):
class Meta:
model = Product
sku = factory.Sequence(lambda n: f'SKU-{n:04d}')
name = factory.Sequence(lambda n: f'Product {n}')
price = factory.Sequence(lambda n: Decimal(f'{n * 10}.99'))Reset sequences between tests if you need predictable values:
# In pytest conftest.py
@pytest.fixture(autouse=True)
def reset_sequences():
factory.reset_sequence()Django Model Factory Patterns
Avoiding Database Hits in Unit Tests
Use build() (not create()) when your unit tests don't need database-saved objects:
def test_display_name_formatting():
user = UserFactory.build(username='alice_w')
profile = ProfileFactory.build(user=user)
assert profile.display_name == 'Alice_W'build() creates the object in memory, populates all fields, and runs LazyAttribute and SubFactory in build mode (sub-factories also use build()).
Using create_strategy
If you need a factory that always uses build() mode:
class InMemoryUserFactory(UserFactory):
class Meta:
strategy = factory.BUILD_STRATEGYexclude for Transient Fields
Use exclude for fields that affect factory logic but shouldn't be passed to the model:
class OrderFactory(DjangoModelFactory):
class Meta:
model = Order
exclude = ['with_items']
class Params:
with_items = False
total = factory.LazyAttribute(
lambda obj: Decimal('99.99') if obj.with_items else Decimal('0.00')
)
@factory.post_generation
def items(self, create, extracted, **kwargs):
if not create:
return
if self.with_items or extracted:
items = extracted or [OrderItemFactory(order=self)]
for item in items:
self.items.add(item)pytest Integration
# conftest.py
import pytest
from myapp.tests.factories import UserFactory, PostFactory
@pytest.fixture
def user(db):
return UserFactory()
@pytest.fixture
def admin_user(db):
return UserFactory(admin=True)
@pytest.fixture
def published_posts(db, user):
return PostFactory.create_batch(3, author=user, published=True)Using db fixture (from pytest-django) ensures the factory runs inside a transaction that rolls back after each test.
Key Points
- Traits (
class Paramswithfactory.Trait) replace factory subclasses for common field variations factory.LazyAttributecomputes fields from other fields on the same object;factory.LazyFunctionfor callables that don't need the objectfactory.SubFactorycreates related objects automatically; override nested fields withfield__nested_field=valuefactory.RelatedFactoryand@factory.post_generationhandle reverse relations and M2Mcreate_batch(n)creates multiple database records with shared overrides- Use
build()in unit tests to avoid database overhead; usecreate()in integration tests - Reset sequences with
factory.reset_sequence()for predictable values in ordered assertions