Read a failing test and the first question is always the same: which of these twenty setup lines is the one the assertion depends on? Test data factories answer it by inverting the default. Instead of constructing every field explicitly, the test states only what makes this case different, and the factory supplies the rest. A test that reads CustomerFactory(vat_exempt=True) tells the reader exactly what it is about; the same test with fifteen keyword arguments tells them nothing.
Prerequisites
factory_boy >= 3.3, or a hand-rolled builder — the patterns here apply to both.- A transactional database fixture, since factories must write inside the test's transaction: see database fixtures and transactional tests.
Faker >= 25if generated values are wanted; it ships withfactory_boyasfactory.Faker.pytest >= 8.0.
Core concept: defaults carry the noise, arguments carry the meaning
A factory is a function from "what this test cares about" to "a valid object". Everything not passed explicitly is noise the test does not depend on, and moving that noise out of the test body is the entire benefit.
Step-by-step implementation
1. One factory per model, with valid defaults
import factory
from myapp.models import Customer
class CustomerFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = Customer
sqlalchemy_session_persistence = "flush" # never commit; see below
# Sequence, not Faker: uniqueness must be guaranteed, not probable.
email = factory.Sequence(lambda n: f"customer-{n}@example.test")
name = factory.Faker("name") # variety with no assertion on it
country = "GB"
vat_exempt = False
sqlalchemy_session_persistence = "flush" is the setting that makes factories compatible with a rolled-back test. flush sends the INSERT so primary keys are populated and subsequent queries see the row, without ending the transaction the fixture will roll back. commit would end it, and the test's isolation with it.
2. Bind the session once
import pytest
@pytest.fixture(autouse=True)
def bind_factories(db_session):
# Every factory writes through the test's transactional session.
for factory_class in (CustomerFactory, OrderFactory, OrderLineFactory):
factory_class._meta.sqlalchemy_session = db_session
yield
A factory that opens its own session is the single most common way rows escape a transactional fixture. Binding them in one autouse fixture makes the mistake impossible rather than merely discouraged.
3. Name the recurring variations as traits
import datetime as dt
import factory
class SubscriptionFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = Subscription
sqlalchemy_session_persistence = "flush"
status = "active"
expires_at = factory.LazyFunction(
lambda: dt.datetime(2027, 1, 1, tzinfo=dt.timezone.utc)
)
class Params:
# A trait is a named bundle of overrides — the vocabulary of the domain,
# rather than a repeated pair of keyword arguments in forty tests.
lapsed = factory.Trait(
status="expired",
expires_at=dt.datetime(2020, 1, 1, tzinfo=dt.timezone.utc),
)
def test_lapsed_subscription_blocks_access(db_session):
subscription = SubscriptionFactory(lapsed=True)
assert not subscription.grants_access()
Traits are where factories stop being boilerplate reduction and start being documentation. lapsed=True is a term from the domain; status="expired", expires_at=datetime(2020, 1, 1) is an implementation detail that will need updating in forty places when the model changes.
4. Keep the object graph shallow by default
import factory
class OrderFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = Order
sqlalchemy_session_persistence = "flush"
customer = factory.SubFactory(CustomerFactory) # required for validity
placed_at = factory.LazyFunction(
lambda: dt.datetime(2026, 6, 1, tzinfo=dt.timezone.utc)
)
@factory.post_generation
def lines(self, create, extracted, **kwargs):
# Lines are OPT-IN: OrderFactory() makes one row, not four.
if not create or not extracted:
return
for amount in extracted:
OrderLineFactory(order=self, amount=amount)
def test_total_sums_line_amounts(db_session):
order = OrderFactory(lines=[100, 250, 75]) # exactly the rows this test needs
assert order.total() == 425
A SubFactory on every relation is how a factory that looks innocent inserts fifteen rows. Required relations get a SubFactory; optional ones get a post_generation hook that does nothing unless asked.
5. Seed the randomness
import pytest
from factory.random import reseed_random
@pytest.fixture(autouse=True, scope="session")
def deterministic_factories(request):
seed = request.config.getoption("--factory-seed", default=None) or 20260918
reseed_random(seed) # factory_boy's Faker and fuzzy attributes
print(f"\nfactory seed: {seed}")
Printing the seed is the point. A failure caused by a generated value is reproducible with --factory-seed=<printed value>, and irreproducible without it.
Verification
Factories are correct when two consecutive runs produce identical data and no rows survive a test. Both are checkable directly:
def test_factories_are_deterministic(db_session):
first = CustomerFactory()
assert first.email == "customer-0@example.test" # sequence starts at 0 per run
def test_factory_rows_do_not_leak(db_session):
# Run after any test that created customers; the rollback must have undone it.
assert db_session.query(Customer).count() == 0
pytest tests/ -p no:randomly -q && pytest tests/ -p no:randomly -q # identical output
Two runs with identical output is the standard to hold. A suite where the second run differs has non-determinism somewhere, and generated data is the usual source.
Troubleshooting
| Symptom | Root cause | Fix |
|---|---|---|
IntegrityError: duplicate key | Faker used for a unique column | Use factory.Sequence |
| Rows survive between tests | Factory bound to its own session, or persistence set to commit | Bind the test session; use flush |
| One test creates dozens of rows | SubFactory chains on optional relations | Convert to post_generation opt-ins |
| Test fails one run in a hundred | Unseeded random data hitting a boundary | Seed the generator and print the seed |
AttributeError on a factory attribute | Model field renamed, factory not updated | Keep factories beside the models in review |
| Identifiers differ between runs | Sequences shared across an xdist session | Prefix sequences with the worker id |
Builders when a factory is the wrong shape
factory_boy is optimised for persisted models. For value objects, API payloads or configuration, a plain builder is often clearer and has no dependency at all:
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class OrderRequest:
customer_id: str = "cus_test"
currency: str = "GBP"
amount: int = 1000
idempotency_key: str = "key-1"
def an_order(**overrides) -> OrderRequest:
"""A builder: defaults plus the fields this test is about."""
return replace(OrderRequest(), **overrides)
def test_zero_amount_is_rejected(api):
response = api.post("/orders", json=an_order(amount=0).__dict__)
assert response.status_code == 422
dataclasses.replace gives immutable overrides in one line, which covers most payload-building needs. The pattern scales down better than a factory class and, because the defaults are a frozen dataclass, a typo in an override is a TypeError at the call site rather than a silently ignored keyword.
The choice between the two is about persistence. Anything that must be inserted, related and flushed benefits from factory_boy's session handling; anything that is just a value is better as a function.
Where factories go wrong at scale
Three failure modes account for most of the pain teams report after a year of factory use.
The god factory. One UserFactory grows parameters for every scenario in the suite — with_subscription, with_orders, verified, admin, suspended — until it is a small program nobody can change safely. Traits help, but the real remedy is composition: separate factories that a scenario fixture assembles, so no single factory knows about every test.
Assertions on generated values. assert customer.name == "John Smith" passes until Faker's word list changes. The rule is that a value produced by the factory is never asserted on directly; the test asserts on values it supplied or on relationships it established.
Factories reaching into the application. A factory that calls a service method to create its object is running application logic during setup, so an unrelated change to that method breaks hundreds of tests at once. Factories write to the model layer; they do not invoke use cases.
Factories under parallel execution
Sequences are per-process, so two pytest-xdist workers both start at zero and both try to insert customer-0@example.test. With per-worker databases the collision is invisible; with a shared database it is an IntegrityError that appears only under -n auto and looks like a race in the application.
import os
import factory
WORKER = os.environ.get("PYTEST_XDIST_WORKER", "gw0")
class CustomerFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = Customer
# Worker-prefixed: unique across processes, still deterministic within one.
email = factory.Sequence(lambda n: f"customer-{WORKER}-{n}@example.test")
The prefix keeps the value deterministic per worker, so a failure is still reproducible by running that worker's tests alone. An alternative — using a UUID — removes the collision and the determinism together, which trades a rare crash for permanently unreadable test data.
Two further details matter under parallelism. Session-scoped factory state, such as a cached "current tenant", is created once per worker rather than once per run, so any assumption about there being exactly one of something is wrong. And the --factory-seed above should incorporate the worker id, or every worker generates the same "random" names and any uniqueness constraint on a Faker-generated column fails immediately.
Seeding a realistic baseline without coupling tests to it
Some suites need more than the rows a single test creates: a catalogue of products, a tax table, a set of feature flags. The temptation is a large seed fixture that every test inherits, and it is a trap — within a year tests depend on rows they never mention, and nobody can change the seed without breaking things at random.
The workable split is between reference data, which is immutable and genuinely global, and scenario data, which belongs to the test that needs it.
import pytest
@pytest.fixture(scope="session", autouse=True)
def reference_data(engine):
"""Rows the domain cannot function without: currencies, countries, tax bands.
Committed once, outside any test transaction, and never modified."""
with Session(engine) as session:
session.add_all([
Currency(code="GBP", minor_units=2),
Currency(code="JPY", minor_units=0), # a deliberate edge case
TaxBand(country="GB", rate=Decimal("0.20")),
])
session.commit()
Two rules keep this safe. Reference data is small enough to read in one screen — if it is not, some of it is scenario data in disguise. And no test may modify it: a test that needs a different tax rate creates its own TaxBand row rather than mutating the shared one, because mutation of committed reference data survives the rollback and poisons everything after it.
The edge case in that fixture is deliberate. Including a zero-decimal currency alongside the ordinary one means every test that touches money has a chance of exercising the rounding path, and the cost is one extra row. Reference data chosen this way does real work; reference data that is just "the happy-path values" only adds setup.
Where a suite genuinely needs a large, realistic dataset — search relevance, reporting, migration performance — load it from a fixture file into a dedicated database, mark those tests, and keep them out of the default run. A hundred thousand rows restored once per session is fine; a hundred thousand rows every test is a different kind of suite.
One consequence of this split is worth stating explicitly, because it comes up in review repeatedly: a test that needs a variant of reference data — a currency with three decimal places, a tax band that has not come into effect yet — creates its own row rather than editing the shared one. The extra row costs nothing, rolls back with everything else, and makes the test self-contained enough to read without knowing what the session fixture committed.
Reviewing factories like production code
Factories accumulate quietly, because a change to one is rarely the point of the change it appears in. A few review habits keep them from drifting.
A new field needs a default in the factory, in the same change. Otherwise the next hundred tests to run fail on a NOT NULL violation and somebody adds the field to fifty call sites instead of one factory.
A new trait must be named for the domain, not the implementation. lapsed survives a schema change; status_expired_and_date_in_past does not.
A SubFactory added to an existing factory is a performance change. It multiplies the rows every test in the suite inserts, and the reviewer should ask whether the relation is required for validity or merely convenient for one test.
Assertions on factory-supplied values are a bug in the test. When review catches assert order.currency == "GBP" where nothing passed a currency, the fix is to pass it explicitly — the test evidently does depend on it.
# A cheap audit: which factories are used most, and which are never used at all?
grep -rho "[A-Za-z]*Factory(" tests/ | sort | uniq -c | sort -rn | head -20
The unused ones are worth deleting; the heavily used ones are worth reading carefully, because a default in a factory used by four hundred tests is effectively a global constant of the suite. Treating that number as a signal — anything above a hundred uses gets the same scrutiny as a public function signature — is what stops the god factory from forming in the first place. A useful supplement is to run the audit again after any large refactor: a factory whose usage count halves overnight usually means somebody copied it rather than extended it, and two near-identical factories drift apart far faster than one factory with a trait.
A last habit is to delete aggressively. Factories for models that no longer exist, traits whose scenario was removed, builders left behind by a refactor: all of them still get read by whoever is trying to understand the suite, and all of them still have to be updated when a shared base class changes. The audit command above finds the unused ones in a second, and removing them costs nothing.
Finally, keep factories in the same review as the model they build. A reviewer looking at a migration that adds a column and at the factory that now supplies it can see in one place whether the default is sensible; the same two changes split across two pull requests produce a week of red builds in between.
Frequently Asked Questions
Should factories use random data or fixed defaults? Fixed or sequential defaults for anything the test might assert on, random only where variety genuinely helps and the value is never asserted. Random names and addresses are fine; random amounts, dates and identifiers produce tests that fail once a month with no way to reproduce. Where randomness is wanted deliberately, seed it and print the seed.
What is the difference between a factory and a fixture? A fixture is a value pytest provides before the test runs; a factory is a callable the test invokes with its own arguments. Fixtures are right for shared infrastructure such as a session or a client, factories for the rows a specific test needs, because only the test knows which fields matter to it.
How do I avoid factories that create half the database?
Make sub-objects lazy. factory_boy's SubFactory creates a related object on every call, so a three-level chain inserts a dozen rows per test. Use a trait or an explicit argument to opt into the deeper graph, and let the default build only what the object requires to be valid.
Do factories belong in the package or in the test directory? In the test directory by default, because they encode test conventions rather than product behaviour. Publish them as a package extra only when downstream consumers — another service's test suite, a plugin — genuinely need to build your models, and treat their signatures as public API from that point.
How do factories interact with a transactional test fixture?
They must use the same session and must flush rather than commit. A factory that opens its own session writes outside the test's transaction and leaks rows; one that calls commit ends the savepoint. Bind the factory's session in a fixture and use flush to populate identifiers.
Related guides
- Compare the two approaches directly in factory_boy versus plain fixture builders.
- Keep generated values reproducible with generating reproducible fake data with Faker.
- Make sure the rows disappear afterwards using database fixtures and transactional tests.
- When the goal is exploring the input space rather than filling fields, move to property-based and fuzz testing strategies.
- Apply the same "defaults plus one override" idea to dependencies in wiring test doubles through a factory function.
← Back to Integration, Database & Service Testing