Recap
We have now covered the full width of the Django testing landscape. Article 1 established the mental model: the test pyramid, types of tests, and what makes tests worth having. Articles 2 and 3 built and migrated a unit test suite. Article 4 moved up to integration tests: views, APIs, serializers, URL routing, and middleware. Article 5 covered every auth and permission boundary. Article 6 built a complete feature from scratch using TDD, red-green-refactor, one cycle at a time.
Across all six articles, one pattern has appeared constantly: test setup. Creating users, customers, products, orders, coupons, and reviews before every test. We used objects.create() directly, and it worked. But as a test suite grows to hundreds of tests, that approach develops cracks.
This article fixes those cracks. We introduce factory_boy, the standard library for test data in Python, go deep on pytest fixture scoping, and establish the discipline of hermetic test isolation: tests that never depend on each other, never share mutable state, and never fail for reasons unrelated to the code they test.
The problem with manual setup
Consider what happens when you create test data manually across a growing suite:
# This appears in 30 different test files
user = User.objects.create_user(username="alice", password="pass")
customer = Customer.objects.create(name="Alice", email="alice@example.com", user=user)
product = Product.objects.create(name="Widget", price=Decimal("25.00"), stock=10)
order = Order.objects.create(customer=customer, status="pending")Now the Customer model gains a required phone field. Every one of those 30 test files breaks. You spend an hour fixing setup code that has nothing to do with the logic being tested.
Or the User model enforces a minimum password length. Every test that creates a user with password="pass" silently fails validation, and the failures cascade in confusing ways.
Manual setup also produces data that is coupled to the test that creates it. When two tests need slightly different data, each duplicates the full setup with one field changed. The duplication spreads, the setup grows longer than the assertions, and reading a test file becomes archaeology.
There are three problems: fragility (model changes break unrelated tests), duplication (the same setup repeated everywhere), and noise (setup drowns out the logic being tested).
factory_boy solves all three.
Introducing factory_boy
factory_boy is a Python library for creating test objects. You define a factory once for each model, specifying sensible defaults for every field. Tests create objects through the factory, overriding only the fields that matter for that specific test.
When the model changes, you update the factory. The change propagates to every test automatically, because tests do not hard-code field values they do not care about.
pip install factory-boyBasic factories
A factory is a class that extends factory.django.DjangoModelFactory. The Meta inner class points to the model. Each field on the factory is a default value.
import factory
from factory.django import DjangoModelFactory
from django.contrib.auth import get_user_model
from decimal import Decimal
from .models import Customer, Product, Order, Coupon
from django.utils import timezone
from datetime import timedelta
User = get_user_model()
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")
password = factory.PostGenerationMethodCall("set_password", "testpass123")
class CustomerFactory(DjangoModelFactory):
class Meta:
model = Customer
user = factory.SubFactory(UserFactory)
name = factory.LazyAttribute(lambda obj: obj.user.username.capitalize())
email = factory.LazyAttribute(lambda obj: obj.user.email)
is_loyalty_member = False
class ProductFactory(DjangoModelFactory):
class Meta:
model = Product
name = factory.Sequence(lambda n: f"Product {n}")
price = Decimal("25.00")
stock = 10
class CouponFactory(DjangoModelFactory):
class Meta:
model = Coupon
code = factory.Sequence(lambda n: f"COUPON{n}")
discount_percent = 10
expires_at = factory.LazyFunction(lambda: timezone.now() + timedelta(days=7))
class OrderFactory(DjangoModelFactory):
class Meta:
model = Order
customer = factory.SubFactory(CustomerFactory)
status = "pending"
subtotal = Decimal("0.00")With these factories, creating test objects becomes one line:
# Creates a User, Customer, and Order in one call
order = OrderFactory()
# Creates a Product with specific price
product = ProductFactory(price=Decimal("50.00"))
# Creates a confirmed order
confirmed_order = OrderFactory(status="confirmed")
# Creates an object in memory without saving (useful for unit tests)
product = ProductFactory.build(name="Widget")create() saves to the database. build() creates an unsaved instance. build_batch(n) and create_batch(n) produce a list of n objects.
Sequences
A sequence is a counter that increments each time a factory creates an object. Sequences solve the most common problem with test data: unique constraint violations when creating multiple objects of the same type.
class UserFactory(DjangoModelFactory):
class Meta:
model = User
# factory.Sequence passes an auto-incrementing integer n to the lambda
username = factory.Sequence(lambda n: f"user{n}")
email = factory.Sequence(lambda n: f"user{n}@example.com")user_a = UserFactory() # username="user0", email="user0@example.com"
user_b = UserFactory() # username="user1", email="user1@example.com"
user_c = UserFactory() # username="user2", email="user2@example.com"Without sequences, creating multiple users would fail because username and email are unique. With sequences, every object gets a unique value automatically, and you never think about it again.
Note
Sequences reset between test runs but not between tests
The sequence counter starts at 0 when the test process starts and increments globally across all tests in that run. Tests should never assert on the exact value of a sequenced field (like asserting username == 'user0') because the counter value depends on test execution order. Assert on structure, not on generated values.
Lazy attributes
A LazyAttribute is computed when the object is created, not when the factory class is defined. This lets you derive one field from another.
class CustomerFactory(DjangoModelFactory):
class Meta:
model = Customer
user = factory.SubFactory(UserFactory)
# name is derived from user.username at creation time
name = factory.LazyAttribute(lambda obj: obj.user.username.capitalize())
# email mirrors the user's email
email = factory.LazyAttribute(lambda obj: obj.user.email)LazyFunction is similar but does not receive the object under construction. Use it for values that depend on external state (like the current time) rather than other fields on the object.
import factory
from django.utils import timezone
from datetime import timedelta
class CouponFactory(DjangoModelFactory):
class Meta:
model = Coupon
# LazyFunction: computed fresh each time, no access to obj
expires_at = factory.LazyFunction(lambda: timezone.now() + timedelta(days=7))✦ Tip
Use LazyFunction for time-dependent defaults
If you used a plain attribute (expires_at = timezone.now() + timedelta(days=7)), the timestamp would be evaluated once when the class is defined, not when each object is created. LazyFunction ensures a fresh timestamp on every factory call.
Related models
The most powerful feature of factory_boy is how it handles related models. A SubFactory creates a related object automatically when the parent is created. The relationship is built up the graph: creating an Order automatically creates a Customer, which automatically creates a User.
# Creates User -> Customer -> Order in one call
order = OrderFactory()
print(order.customer.name) # "User0" (from CustomerFactory default)
print(order.customer.user.email) # "user0@example.com" (from UserFactory default)
# Override the customer
specific_customer = CustomerFactory(name="Alice")
order = OrderFactory(customer=specific_customer)
# Override a field on the subfactory using double underscore notation
order = OrderFactory(customer__name="Bob")
print(order.customer.name) # "Bob"The double-underscore notation (customer__name="Bob") is one of factory_boy's most useful features. It lets you reach into a nested subfactory and override a specific field without creating the customer manually. The factory creates everything, wires it together, and applies your override.
# Create a loyalty member's order in one line
order = OrderFactory(customer__is_loyalty_member=True)
# Create an order with a specific product price (if products are related)
# You can go arbitrarily deep
order = OrderFactory(customer__user__email="specific@example.com")Traits
Traits are named presets that flip a group of fields at once. Instead of repeating a set of overrides in every test that needs a "loyalty member" or an "expired coupon", you define the preset once in the factory and activate it by name.
import factory
from factory.django import DjangoModelFactory
from django.utils import timezone
from datetime import timedelta
from .models import Customer, Coupon, Order
class CustomerFactory(DjangoModelFactory):
class Meta:
model = Customer
user = factory.SubFactory(UserFactory)
name = factory.LazyAttribute(lambda obj: obj.user.username.capitalize())
email = factory.LazyAttribute(lambda obj: obj.user.email)
is_loyalty_member = False
class Params:
loyalty = factory.Trait(is_loyalty_member=True)
class CouponFactory(DjangoModelFactory):
class Meta:
model = Coupon
code = factory.Sequence(lambda n: f"COUPON{n}")
discount_percent = 10
expires_at = factory.LazyFunction(lambda: timezone.now() + timedelta(days=7))
class Params:
expired = factory.Trait(
expires_at=factory.LazyFunction(
lambda: timezone.now() - timedelta(seconds=1)
)
)
high_value = factory.Trait(discount_percent=50)
class OrderFactory(DjangoModelFactory):
class Meta:
model = Order
customer = factory.SubFactory(CustomerFactory)
status = "pending"
subtotal = factory.LazyFunction(lambda: __import__("decimal").Decimal("0.00"))
class Params:
confirmed = factory.Trait(status="confirmed")
shipped = factory.Trait(status="shipped")
cancelled = factory.Trait(status="cancelled")Traits make test setup self-documenting. The code reads like the scenario it represents:
# Without traits — noise
customer = CustomerFactory(is_loyalty_member=True)
coupon = CouponFactory(expires_at=timezone.now() - timedelta(seconds=1))
order = OrderFactory(status="shipped")
# With traits — signal
customer = CustomerFactory(loyalty=True)
coupon = CouponFactory(expired=True)
order = OrderFactory(shipped=True)
# Traits compose
coupon = CouponFactory(expired=True, high_value=True)SubFactories and deep graphs
Some features require deep object graphs: an order that belongs to a loyalty member, reviewed by a specific user, with a coupon that has not expired. Building these graphs manually takes ten lines of setup. With subfactories and traits, it takes one or two.
import factory
from factory.django import DjangoModelFactory
from orders.factories import ProductFactory, UserFactory
from .models import Review
class ReviewFactory(DjangoModelFactory):
class Meta:
model = Review
user = factory.SubFactory(UserFactory)
product = factory.SubFactory(ProductFactory)
rating = 4
body = ""
class Params:
positive = factory.Trait(rating=5, body="Excellent product.")
negative = factory.Trait(rating=1, body="Very disappointed.")# A realistic test scenario in two lines of setup
loyalty_order = OrderFactory(customer__loyalty=True, shipped=True)
positive_review = ReviewFactory(user=loyalty_order.customer.user, positive=True)This is the payoff. Test setup that used to be 8 lines of objects.create() calls with careful ordering of related objects is now 2 lines. The factory handles the graph. The test handles the assertion.
Using factories in pytest
Factories do not replace fixtures. They complement them. The best pattern is to define a small set of fixtures that use factories for the common cases, and call factories directly inside tests for scenarios that are specific to that test.
import pytest
from orders.factories import UserFactory, CustomerFactory, ProductFactory, OrderFactory, CouponFactory
from reviews.factories import ReviewFactory
@pytest.fixture
def user(db):
return UserFactory()
@pytest.fixture
def other_user(db):
return UserFactory()
@pytest.fixture
def customer(db, user):
return CustomerFactory(user=user)
@pytest.fixture
def loyalty_customer(db):
return CustomerFactory(loyalty=True)
@pytest.fixture
def product(db):
return ProductFactory()
@pytest.fixture
def order(db, customer):
return OrderFactory(customer=customer)
@pytest.fixture
def valid_coupon(db):
return CouponFactory()
@pytest.fixture
def expired_coupon(db):
return CouponFactory(expired=True)For tests that need many objects of the same type (pagination tests, aggregation tests, filter tests), call create_batch directly inside the test body rather than in a fixture. Fixtures should provide the objects that most tests in a file need. Bulk data is test-specific.
@pytest.mark.django_db
def test_pagination_returns_ten_results_per_page(authenticated_api_client, customer):
# Direct factory call inside the test — this data is specific to this test
OrderFactory.create_batch(15, customer=customer)
response = authenticated_api_client.get(reverse("api:order-list"))
assert response.data["count"] == 15
assert len(response.data["results"]) == 10
@pytest.mark.django_db
def test_average_rating_across_many_reviews(product):
from reviews.factories import ReviewFactory
ReviewFactory.create_batch(5, product=product, rating=4)
ReviewFactory.create_batch(5, product=product, rating=2)
assert product.average_rating == 3.0Fixture scoping strategies
pytest fixtures run at a configurable scope: function, class, module, or session. The scope determines how often the fixture is created and how long it lives. Choosing the right scope is one of the most important decisions for keeping a test suite both fast and reliable.
Function scope (the default)
The fixture runs before each test function and is discarded after. This is the safest scope: each test gets a completely fresh object with no contamination from previous tests.
Use function scope for any fixture that creates mutable state, especially database objects. Django's TestCase transaction wrapping and pytest-django's db fixture both operate at function scope, rolling back after each test.
@pytest.fixture(scope="function") # default, usually omitted
def order(db, customer):
return OrderFactory(customer=customer)Class scope
The fixture runs once per test class and is shared across all methods in that class. Useful when you are grouping related tests that all need the same object and none of them mutate it.
@pytest.fixture(scope="class")
def product_with_reviews(django_db_setup, db):
product = ProductFactory()
ReviewFactory.create_batch(5, product=product, rating=4)
return product
class TestProductRating:
def test_average_is_correct(self, product_with_reviews):
assert product_with_reviews.average_rating == 4.0
def test_review_count_is_correct(self, product_with_reviews):
assert product_with_reviews.reviews.count() == 5Module scope
The fixture runs once per test module (file). Use this for expensive read-only setup that all tests in a file need, such as a large set of seed data used by many independent tests. The same cautions about mutation apply: a test that modifies the shared object will corrupt subsequent tests.
@pytest.fixture(scope="module")
def product_catalog(django_db_setup, db):
return ProductFactory.create_batch(50)Session scope
The fixture runs once for the entire test session. The most common use case is django_db_setup itself, which creates the test database once. Avoid session-scoped fixtures for anything your tests might modify. The contamination risk is too high.
⚠ Gotcha
Wider scope requires explicit database access
Function-scoped fixtures get database access automatically through the db or django_db_setup fixture. Class, module, and session-scoped fixtures must explicitly declare django_db_setup as a dependency, because the default db fixture only covers function scope.
# Wrong: class-scoped fixture without explicit db access
@pytest.fixture(scope="class")
def shared_product():
return ProductFactory() # RuntimeError: database access not allowed
# Correct
@pytest.fixture(scope="class")
def shared_product(django_db_setup, db):
return ProductFactory()Shared state bugs
A shared state bug is when one test modifies an object that another test expects to be in its original state. These bugs are intermittent (they depend on test execution order), hard to diagnose (the failing test looks correct in isolation), and silently introduced by widening fixture scope.
# shared_order is class-scoped — created once for the whole class
@pytest.fixture(scope="class")
def shared_order(django_db_setup, db):
return OrderFactory(status="pending")
class TestOrderCancellation:
def test_cancel_pending_order(self, shared_order):
# This test mutates shared_order
cancel_order(shared_order)
shared_order.refresh_from_db()
assert shared_order.status == "cancelled" # passes
def test_pending_order_is_cancellable(self, shared_order):
# Now shared_order.status is "cancelled" — this test fails for the wrong reason
assert shared_order.is_cancellable # FAILSThe fix is straightforward: use function scope so each test gets its own fresh object, or never mutate the shared object (fetch a fresh copy from the database instead).
@pytest.fixture # function scope (default)
def order(db):
return OrderFactory(status="pending")
def test_cancel_pending_order(order):
cancel_order(order)
order.refresh_from_db()
assert order.status == "cancelled"
def test_pending_order_is_cancellable(order):
# Fresh order for this test — status is "pending" as expected
assert order.is_cancellableIf you genuinely need class scope for performance reasons, fetch a fresh database copy inside the test instead of using the shared object directly:
class TestOrderCancellation:
def test_cancel_pending_order(self, shared_order):
# Fetch a fresh copy rather than mutating the shared fixture
order = Order.objects.get(pk=shared_order.pk)
cancel_order(order)
order.refresh_from_db()
assert order.status == "cancelled"
def test_pending_order_is_cancellable(self, shared_order):
# shared_order is untouched — safe to read
assert shared_order.is_cancellableDatabase isolation
Django's TestCase and pytest-django's db fixture both wrap each test in a transaction that is rolled back after the test completes. This means database writes in one test are never visible to another. This isolation is automatic and requires no cleanup code in your tests.
However, there are two situations where this isolation breaks down.
TransactionTestCase
Some tests require real transactions: testing atomic() blocks, testing behaviour that depends on the on_commit hook, or testing code that uses multiple database connections. For these, use Django's TransactionTestCase or pytest-django's django_db(transaction=True) mark.
TransactionTestCase does not use transaction rollback for cleanup. It truncates all tables after each test, which is significantly slower. Use it only when you specifically need transaction semantics.
@pytest.mark.django_db(transaction=True)
def test_on_commit_hook_fires_after_transaction(order):
with mock.patch("orders.tasks.send_confirmation_email") as mock_send:
with transaction.atomic():
order.status = "confirmed"
order.save()
# on_commit hooks only fire when the outer transaction commits
mock_send.assert_called_once()setUpTestData and class-level isolation
Django's setUpTestData creates objects once for the entire test class inside a savepoint, then restores them between tests. Tests that try to modify these objects will see their changes rolled back before the next test runs, so the shared objects are always in their original state.
In pytest terms, the equivalent is a class-scoped fixture that wraps the setup in TestCase.setUpTestData semantics. The simplest approach in pytest is to use a function-scoped fixture with factory_boy, which is fast enough for most suites.
Avoiding the database in unit tests
The fastest test is one that never touches the database. Every test that creates a database record spends time on I/O: the insert, the transaction overhead, and the rollback. For unit tests that only need to call a method on a model, use factory.build() or construct the object directly with Model().
# Slow: creates a database record just to test a method
@pytest.mark.django_db
def test_order_total_applies_tax(db):
order = OrderFactory(subtotal=Decimal("100.00"), tax_rate=Decimal("0.16"))
assert order.total == Decimal("116.00")
# Fast: no database involved
def test_order_total_applies_tax():
order = OrderFactory.build(subtotal=Decimal("100.00"), tax_rate=Decimal("0.16"))
assert order.total == Decimal("116.00")
# Even faster: plain constructor, no factory overhead
def test_order_total_applies_tax():
from decimal import Decimal
from orders.models import Order
order = Order(subtotal=Decimal("100.00"), tax_rate=Decimal("0.16"))
assert order.total == Decimal("116.00")The rule is simple: if a test does not assert on database state, it should not touch the database. A model property test, a method test, a utility function test, and a permission class test all fall into this category.
When you use factory.build(), factory_boy calls SubFactory references with build() automatically. The entire object graph is built in memory without a single database call.
# All SubFactories are also built (not saved) — zero database calls
order = OrderFactory.build(subtotal=Decimal("100.00"))
print(order.customer) # Customer instance in memory
print(order.customer.user) # User instance in memory
print(order.pk) # None — not savedTest doubles and mocking
Some dependencies cannot be controlled through factories: external HTTP APIs, email services, third-party payment gateways, Celery tasks, and the filesystem. For these, use test doubles: objects that stand in for the real dependency and return controlled responses.
The standard library provides unittest.mock. The two tools you will use most are Mock (a generic double) and patch (which replaces a name in a module temporarily).
from unittest.mock import patch, MagicMock
@pytest.mark.django_db
def test_order_confirmation_email_is_sent_on_create(authenticated_api_client, customer):
with patch("orders.tasks.send_confirmation_email") as mock_send:
authenticated_api_client.post(
reverse("api:order-list"),
data={"subtotal": "100.00"},
format="json",
)
mock_send.assert_called_once()
@pytest.mark.django_db
def test_payment_failure_returns_402(authenticated_api_client, order):
with patch("orders.services.payment_gateway.charge") as mock_charge:
mock_charge.side_effect = Exception("Card declined")
response = authenticated_api_client.post(
reverse("api:charge-order", kwargs={"pk": order.pk})
)
assert response.status_code == 402The scope of a patch context manager is exactly one test. The real function is restored when the with block exits. This is hermetic: the mock never leaks into another test.
If you use pytest-mock (covered in Article 3), the mocker fixture achieves the same result with less nesting:
@pytest.mark.django_db
def test_order_confirmation_email_is_sent_on_create(
authenticated_api_client, customer, mocker
):
mock_send = mocker.patch("orders.tasks.send_confirmation_email")
authenticated_api_client.post(
reverse("api:order-list"),
data={"subtotal": "100.00"},
format="json",
)
mock_send.assert_called_once()✦ Tip
Patch at the point of use, not the point of definition
If orders.views imports send_confirmation_email from orders.tasks, patch orders.views.send_confirmation_email, not orders.tasks.send_confirmation_email. The view already holds a reference to the original function. Patching the source module after the import has no effect on the reference the view holds.
# Wrong: patching the definition after the view has already imported it
mocker.patch("orders.tasks.send_confirmation_email")
# Correct: patching the name as the view sees it
mocker.patch("orders.views.send_confirmation_email")Summary
- Manual
objects.create()setup in every test produces three problems: fragility (model changes break unrelated tests), duplication (the same setup repeated everywhere), and noise (setup drowns out the assertions). factory_boysolves all three. Define defaults once per model, override only what matters per test, and let the factory manage the object graph.- Use
factory.Sequencefor fields with unique constraints. The counter increments automatically and you never hit a uniqueness error. - Use
factory.LazyAttributeto derive one field from another at creation time. Usefactory.LazyFunctionfor values that depend on external state like the current time. SubFactorybuilds related objects automatically. Override nested fields with double-underscore notation:OrderFactory(customer__loyalty=True).- Traits are named presets that group related field overrides. They make test setup self-documenting:
CouponFactory(expired=True)is clearer than repeating the expiry timestamp calculation everywhere. - Use function scope (the default) for database fixtures. Widen scope only when there is a measurable performance reason and only for objects that tests will not mutate.
- A shared state bug is when one test mutates a shared fixture and corrupts subsequent tests. The symptom is a test that passes in isolation but fails in the full suite. The fix is function scope or fetching a fresh database copy inside the test.
- Use
factory.build()or plainModel()constructors for unit tests that do not assert on database state. Zero I/O means maximum speed. - Patch at the point of use, not the point of definition. A name imported by a module must be patched on that module, not on the source module it came from.
In the final article we pull everything together: measuring and interpreting coverage, running the suite in GitHub Actions CI, making the suite fast enough that nobody skips it, and knowing when the suite is good enough to ship on.