Fixtures & Factories#
Test fixtures provide reusable test data and setup code. FairDM uses factory-boy for complex objects and pytest fixtures for composition and lifecycle management.
Overview#
FairDM uses two complementary approaches for test data:
For creating model instances
Database objects with sensible defaults
Easy field overrides
Support for relationships and inheritance
Located in
tests/fixtures/factories.py
For composition and lifecycle
Combine factories into scenarios
Manage setup/teardown
Scope control (function, module, session)
Located in
conftest.pyfiles
Factory-Boy Basics#
What is Factory-Boy?#
Factory-boy generates test objects with sensible defaults, allowing selective field overrides.
Without factory-boy:
# Repetitive, brittle, hard to maintain
@pytest.mark.django_db
def test_something():
user = User.objects.create(
username="testuser",
email="test@example.com",
first_name="Test",
last_name="User"
)
project = Project.objects.create(
owner=user,
title="Test Project",
slug="test-project",
description="Test description"
)
With factory-boy:
# Concise, flexible, maintainable
@pytest.mark.django_db
def test_something():
project = ProjectFactory() # All fields have sensible defaults
Creating Your First Factory#
Step 1: Define the factory in tests/fixtures/factories.py
import factory
from django.contrib.auth import get_user_model
User = get_user_model()
class UserFactory(factory.django.DjangoModelFactory):
"""
Factory for creating User instances.
Usage:
# Create with defaults
user = UserFactory()
# Override specific fields
user = UserFactory(username="custom_user")
# Build without saving to database
user = UserFactory.build()
"""
class Meta:
model = User
username = factory.Sequence(lambda n: f"user{n}")
email = factory.LazyAttribute(lambda obj: f"{obj.username}@example.com")
first_name = "Test"
last_name = "User"
Step 2: Use the factory in tests
from fairdm.factories import UserFactory
@pytest.mark.django_db
def test_user_creation():
user = UserFactory(username="alice")
assert user.username == "alice"
assert user.email == "alice@example.com"
Factory Patterns#
Pattern 1: Sequences#
Generate unique values:
class ProjectFactory(factory.django.DjangoModelFactory):
class Meta:
model = Project
title = factory.Sequence(lambda n: f"Project {n}")
slug = factory.Sequence(lambda n: f"project-{n}")
Usage:
p1 = ProjectFactory() # title="Project 0", slug="project-0"
p2 = ProjectFactory() # title="Project 1", slug="project-1"
Pattern 2: LazyAttribute#
Compute field from other fields:
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
username = factory.Sequence(lambda n: f"user{n}")
email = factory.LazyAttribute(lambda obj: f"{obj.username}@example.com")
Pattern 3: SubFactory (ForeignKey)#
Create related objects automatically:
class ProjectFactory(factory.django.DjangoModelFactory):
class Meta:
model = Project
owner = factory.SubFactory(UserFactory)
title = "Default Project"
slug = factory.Sequence(lambda n: f"project-{n}")
Usage:
# Automatically creates User
project = ProjectFactory()
assert project.owner is not None
# Use existing user
user = UserFactory()
project = ProjectFactory(owner=user)
Pattern 5: Traits#
Create variants with different characteristics:
class ProjectFactory(factory.django.DjangoModelFactory):
class Meta:
model = Project
title = "Default Project"
visibility = Project.Visibility.PUBLIC
class Params:
private = factory.Trait(
visibility=Project.Visibility.PRIVATE
)
with_datasets = factory.Trait(
dataset1=factory.RelatedFactory(DatasetFactory, 'project'),
dataset2=factory.RelatedFactory(DatasetFactory, 'project'),
)
Usage:
public_project = ProjectFactory()
private_project = ProjectFactory(private=True)
project_with_data = ProjectFactory(with_datasets=True)
Pattern 6: Build vs Create#
Control database persistence:
# Create: Saves to database
user = UserFactory() # user.pk is set
user = UserFactory.create() # Explicit
# Build: In-memory only (for unit tests)
user = UserFactory.build() # user.pk is None
When to use build:
Unit tests that don’t need database
Testing object initialization
Faster test execution
When to use create:
Integration tests
Testing database constraints
Testing relationships
Polymorphic Factories#
FairDM uses polymorphic models for Samples and Measurements. Factory-boy supports this:
Base Polymorphic Factory#
from fairdm.core.models import Sample
class SampleFactory(factory.django.DjangoModelFactory):
"""
Base factory for Sample model.
Use child factories (RockSampleFactory, etc.) for specific types.
"""
class Meta:
model = Sample
dataset = factory.SubFactory(DatasetFactory)
name = factory.Sequence(lambda n: f"Sample {n}")
Child Factory with Inheritance#
from fairdm.factories import SampleFactory
class RockSampleFactory(SampleFactory):
"""
Factory for RockSample (inherits from Sample).
Usage:
rock = RockSampleFactory(rock_type="Granite")
"""
class Meta:
model = RockSample
# RockSample-specific fields
rock_type = "Igneous"
mineral_composition = "Quartz, Feldspar"
Usage:
@pytest.mark.django_db
def test_polymorphic_sample():
rock = RockSampleFactory(rock_type="Granite")
# Can query as Sample
assert Sample.objects.count() == 1
# Can query as RockSample
assert RockSample.objects.count() == 1
# Polymorphic query returns correct type
sample = Sample.objects.first()
assert isinstance(sample, RockSample)
Pytest Fixtures#
Use pytest fixtures to compose factories into reusable scenarios.
Simple Fixture#
# tests/conftest.py
import pytest
from fairdm.factories import UserFactory
@pytest.fixture
def user():
"""Create a test user."""
return UserFactory()
Composed Fixture#
# tests/conftest.py
import pytest
from fairdm.factories import ProjectFactory, DatasetFactory
@pytest.fixture
def project_with_datasets():
"""
Create a project with 3 datasets.
Returns tuple: (project, [dataset1, dataset2, dataset3])
"""
project = ProjectFactory()
datasets = DatasetFactory.create_batch(3, project=project)
return project, datasets
Usage:
@pytest.mark.django_db
def test_something(project_with_datasets):
project, datasets = project_with_datasets
assert project.datasets.count() == 3
Fixture Scopes#
Control when fixtures are created:
# Function scope (default): New instance per test
@pytest.fixture
def user():
return UserFactory()
# Module scope: Shared across tests in one file
@pytest.fixture(scope="module")
def shared_user():
return UserFactory()
# Session scope: Shared across entire test run
@pytest.fixture(scope="session")
def reference_data():
# Load reference data once
return load_reference_vocabularies()
Best Practices#
1. Factories in tests/fixtures/factories.py#
tests/fixtures/
├── __init__.py
├── factories.py # All factory-boy factories
└── pytest_fixtures.py # Complex pytest fixtures
2. Comprehensive Docstrings#
class ProjectFactory(factory.django.DjangoModelFactory):
"""
Factory for creating Project instances.
Default behavior:
- Creates a unique owner (UserFactory)
- Generates unique title and slug
- Sets visibility to PUBLIC
Usage:
# Create with defaults
project = ProjectFactory()
# Override fields
project = ProjectFactory(title="Custom Title")
# Use existing user
user = UserFactory()
project = ProjectFactory(owner=user)
# Build without saving
project = ProjectFactory.build()
"""
...
3. Sensible Defaults#
# ✅ Good: Sensible defaults, easy to override
class ProjectFactory(factory.django.DjangoModelFactory):
title = factory.Sequence(lambda n: f"Project {n}")
visibility = Project.Visibility.PUBLIC
# ❌ Bad: No defaults, requires manual setup
class ProjectFactory(factory.django.DjangoModelFactory):
pass # All fields must be provided
4. Explicit Over Implicit#
# ✅ Good: Explicit relationships
@pytest.mark.django_db
def test_project_with_datasets():
project = ProjectFactory()
datasets = DatasetFactory.create_batch(3, project=project)
# ❌ Bad: Hidden RelatedFactory magic
@pytest.mark.django_db
def test_project_with_datasets():
project = ProjectFactory(with_datasets=True) # What datasets? How many?
Common Pitfalls#
Factories that call .create() need database access.
Fix: Add @pytest.mark.django_db decorator to test.
Using same value for unique fields causes conflicts.
Fix: Use factory.Sequence for unique values.
Too many RelatedFactory or Traits makes tests confusing.
Fix: Keep factories simple, compose in pytest fixtures.
Creating database objects in unit tests slows them down.
Fix: Use .build() for in-memory objects in unit tests.
Next Steps#
See also
Review Database Strategy for transaction management in integration tests
See Fixture Factory Example for complete working examples
Follow FairDM testing conventions for factory patterns
Read Test Layers to understand when to use
.build()vs.create()Learn Running Tests CLI options for fixture-related debugging