Integration & Data

Test Data Factories & Builders

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 >= 25 if generated values are wanted; it ships with factory_boy as factory.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.

Explicit construction versus a factory call Two panels show the same test precondition. On the left, eleven explicit keyword arguments with one of them highlighted as the meaningful field. On the right, a single factory call with only that one field passed, and the remaining values supplied by defaults the reader never has to scan. The reader has to find the one field that matters explicit construction ← the point factory call CustomerFactory(vat_exempt=True) every other field has a default that keeps the object valid nothing to scan past
Both produce the same row. Only one of them survives being read six months later by somebody debugging a failure.

Step-by-step implementation

1. One factory per model, with valid defaults

Python
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

Python
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

Python
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),
        )
Python
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

Python
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)
Python
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

Python
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:

Python
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
Bash
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

SymptomRoot causeFix
IntegrityError: duplicate keyFaker used for a unique columnUse factory.Sequence
Rows survive between testsFactory bound to its own session, or persistence set to commitBind the test session; use flush
One test creates dozens of rowsSubFactory chains on optional relationsConvert to post_generation opt-ins
Test fails one run in a hundredUnseeded random data hitting a boundarySeed the generator and print the seed
AttributeError on a factory attributeModel field renamed, factory not updatedKeep factories beside the models in review
Identifiers differ between runsSequences shared across an xdist sessionPrefix 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:

Python
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)
Python
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.

Three ways factories decay, and the correction for each Three cards. The god factory accumulates parameters for every scenario and is corrected by composing small factories in scenario fixtures. Assertions on generated values break when the generator changes and are corrected by asserting only on supplied values. Factories that call application services couple setup to product code and are corrected by writing directly to the model layer. How factories decay after a year the god factory one class, thirty params every scenario bolted on nobody dares change it fix: compose small ones in scenario fixtures asserting on defaults assert name == "John" passes until the word list is updated upstream fix: assert only on values you supplied calling the app factory invokes a service to build its object one change breaks 300 tests fix: write to the model layer directly
All three decay modes share a cause: the factory acquired responsibilities that belong to the test or to the application.

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.

Python
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.

Python
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.

Reference data outside the transaction, scenario data inside it A session boundary contains a committed reference-data layer of currencies and tax bands that survives every test. Inside it, each test opens a transaction in which factories create scenario rows, and the transaction is rolled back at the end so only the reference layer remains. Two layers with different lifetimes reference data — committed once per session currencies · countries · tax bands · feature flags immutable by convention; survives every rollback test 1 CustomerFactory() OrderFactory(lines=[100]) rolled back test 2 SubscriptionFactory(lapsed=True) sees the same currencies rolled back test 3 an_order(amount=0) no rows at all nothing to undo
The upper band is committed and shared; everything in the lower bands disappears. A test that mutates the upper band breaks that arrangement silently.

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.

Bash
# 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.

← Back to Integration, Database & Service Testing