Test Data Fixtures and Factories
Shared fixtures rot and duplicated setup hides intent. Factories with sensible defaults, building only what the test is about, and making the interesting value obvious.
Shared fixtures rot and duplicated setup hides intent. Factories with sensible defaults, building only what the test is about, and making the interesting value obvious.
Every test needs objects to exist before it can do anything, and how you build them decides two things: whether a reader can tell what a test is about, and whether adding a test breaks four others.
The two patterns are fixtures and factories, and they are not alternatives so much as different answers to "how much of this setup is the same every time". By the end of this lesson you will know when each is right, how to build a factory that keeps tests readable, and why a shared fixture that grows is the beginning of an unmaintainable suite.
Setup grows faster than tests do. A test about email formatting needs a user; the user needs an account; the account needs a plan; the plan needs a price. Four objects for a test about a string.
Written inline, that is eight lines of noise before the one line that matters:
def test_the_receipt_shows_the_discounted_total():
plan = Plan(name="Pro", price=Decimal("20.00"))
account = Account(name="Acme", plan=plan, currency="GBP")
user = User(
email="ada@example.com",
name="Ada",
account=account,
verified=True,
role="admin",
)
order = Order(user=user, total=Decimal("45.00"))
assert receipt_for(order).total_line == "Total: £40.50"Nine of ten lines are setup, and none of them is what the test is named
for. Repeat that across two hundred tests and every change to the User
constructor is a two-hundred-file edit.
A fixture is named, reusable setup. Its defining property is that every test asking for it gets the same thing — freshly built, but the same shape.
@pytest.fixture
def pro_account(database):
plan = create_plan(database, name="Pro", price=Decimal("20.00"))
return create_account(database, name="Acme", plan=plan)
@pytest.fixture
def admin_user(database, pro_account):
return create_user(
database, email="ada@example.com", account=pro_account,
role="admin", verified=True,
)Fixtures compose — admin_user asks for pro_account, which asks for
database — and the framework resolves the chain. That composition is
their real strength, and it means expensive setup can be built once and
depended on widely.
Where fixtures are the right answer:
Infrastructure. A database session, an HTTP client, a running server, a temporary directory. There is one right way to build these and every test wants it.
Expensive setup shared at session scope. One container, one migration run, one loaded model.
A genuinely standard object that no test modifies — a read-only reference record, a currency table.
@pytest.fixture(scope="session") # once for the whole run
def engine():
with PostgresContainer("postgres:16") as postgres:
engine = create_engine(postgres.get_connection_url())
run_migrations(engine)
yield engine
@pytest.fixture # once per test, the default
def database(engine):
connection = engine.connect()
transaction = connection.begin()
yield Session(bind=connection)
transaction.rollback()
connection.close()A factory builds objects on demand, with sensible defaults for everything and an override for anything.
Now a test says exactly what it depends on and nothing else:
Three properties make this work, and all three are worth insisting on.
Sensible defaults, so a test only states what it cares about. Every default must produce a valid object — a factory that returns something the application would reject is a factory that makes every test a puzzle.
Unique values for anything unique. The uuid4() in the email is what
lets these tests run in parallel and repeatedly, which was the
independence requirement from earlier.
Overrides for everything. The moment a test needs something the factory cannot express, someone builds an object by hand and the pattern starts eroding.
Established libraries do this well — factory_boy for Python,
Fabricate and Fishery for JavaScript, FactoryBot for Ruby — and they
add useful things like sequences, sub-factories and traits. A dictionary
with an override merge, as above, is enough to start.
Whatever the test is about goes in the test. Everything else goes in setup.
That is the same rule from the structuring lesson, and factories are what make it achievable — because a fixture can only hide all the values, while a factory hides the irrelevant ones and exposes the relevant one.
Bad — a shared fixture the tests then mutate:
Good — a factory, and each test builds the case it names:
The bad version has a specific failure mode beyond ugliness. Mutating a
fixture works until the object has behaviour on construction — a
verified flag that also sets a timestamp, a role that also grants
permissions — at which point setting the attribute afterwards produces an
object in a state the application can never actually be in. The test then
passes or fails for reasons that have nothing to do with real behaviour.
It also invites the next person to add a field to the shared fixture, and that is how a fixture used by forty tests is born.
Real data has relationships, and a factory should create them lazily — only what is needed:
The test creates one user and two orders, and says nothing about accounts or plans — which exist, because the factories built them, and are irrelevant, because the test is not about them.
Two extras worth having once a factory is in use:
Traits for recurring combinations, so common cases stay short:
Awkward defaults built in. The foundations course argued for data that misbehaves. A factory is where that becomes free — make one trait produce a name with an apostrophe, another an account with ten thousand records — and any test can opt in.
One distinction worth adopting early: some tests need a saved object and some only need an object.
In memory. Microseconds.
No database, no transaction, no cleanup. Everything a pure
unit test of can_invite(user) actually needs.
Saved. Milliseconds.
Right when the thing under test reads it back, joins against it, or enforces a database constraint.
A unit test of can_invite(user) needs no database at all, and giving it
one turns a microsecond test into a millisecond one for no benefit.
Multiply by a thousand tests and it is the difference between a suite you
run on save and one you do not.
Factories build the data. The next lesson keeps it from leaking between tests: transactions, truncation and per-test schemas, and why "passes alone, fails together" is always a state problem rather than a mystery.
Before that, find the most-used fixture in your own suite and count its fields. If it has grown past what any single test needs, converting it to a factory is a contained change that makes every future test easier to read.
# The rule
# whatever the test is ABOUT goes in the test
# everything else goes in setup
# a fixture hides all the values; a factory hides only the
# irrelevant ones — which is why factories scale
# FIXTURES — named, reusable, the same thing every time
use for infrastructure: database session, client, temp dir
expensive session-scoped setup: container, migrations
a standard object that NO test modifies
they compose: admin_user -> pro_account -> database
scope: session for expensive IMMUTABLE things only
function (default) for anything a test can change
# FACTORIES — build on demand, defaults plus overrides
def make_user(database, **overrides):
attributes = {
"email": f"user-{uuid4()}@example.com", # unique!
"role": "standard",
"verified": True,
} | overrides
return create_user(database, **attributes)
three requirements
sensible defaults, and every default produces a VALID object
unique values for anything unique — parallel safety
overrides for everything, or people build objects by hand
# The signal to switch from fixture to factory
# a test's first line reaches in to mutate the fixture
# a fixture grows a field because ONE new test needed it
# Why mutating a fixture is worse than ugly
# objects with construction behaviour end up in states the
# application can never produce, so the test proves nothing
# Object graphs: build lazily
attributes.setdefault("user", make_user(database))
# the test says "two orders for one user" and nothing about plans
# Extras worth having
traits make_locked_admin(...) for recurring combinations
awkward defaults a trait for O'Brien, one for 10,000 records
# build vs create
build_user() in memory, microseconds — unit tests
create_user(database) saved, milliseconds — integration tests
# giving a unit test a database is a 1000x cost for no benefit
# Libraries
# factory_boy (Python), Fishery / Fabricate (JS), FactoryBot (Ruby)def make_user(database, **overrides):
"""A valid, verified standard user. Override anything."""
attributes = {
"email": f"user-{uuid4()}@example.com",
"name": "Test User",
"role": "standard",
"verified": True,
"account": make_account(database),
} | overrides
return create_user(database, **attributes)@pytest.fixture
def user_factory(database):
def factory(**overrides):
return make_user(database, **overrides)
return factorydef test_an_unverified_user_cannot_invite_colleagues(user_factory):
user = user_factory(verified=False)
assert not can_invite(user)
def test_an_admin_can_invite_colleagues(user_factory):
user = user_factory(role="admin")
assert can_invite(user)@pytest.fixture
def user(database):
return create_user(
database, email="ada@example.com", role="standard",
verified=True, account=create_account(database),
)
def test_an_unverified_user_cannot_invite(user):
user.verified = False # reaching in to change it
assert not can_invite(user)
def test_an_admin_can_invite(user):
user.role = "admin" # and again
assert can_invite(user)def test_an_unverified_user_cannot_invite(user_factory):
assert not can_invite(user_factory(verified=False))
def test_an_admin_can_invite(user_factory):
assert can_invite(user_factory(role="admin"))def make_order(database, **overrides):
attributes = {
"reference": f"ORD-{uuid4().hex[:8]}",
"total": Decimal("45.00"),
"status": "pending",
} | overrides
# only build a user if the test did not supply one
attributes.setdefault("user", make_user(database))
return create_order(database, **attributes)def test_a_users_orders_are_listed_newest_first(user_factory,
order_factory):
user = user_factory()
order_factory(user=user, reference="ORD-1")
order_factory(user=user, reference="ORD-2")
references = [o.reference for o in orders_for(user)]
assert references == ["ORD-2", "ORD-1"]def make_locked_admin(database, **overrides):
return make_user(
database, role="admin", locked=True,
failed_logins=5, **overrides,
)def build_user(**overrides):
"""In memory. No database. Microseconds."""
return User(**({"email": f"u-{uuid4()}@example.com"} | overrides))
def create_user(database, **overrides):
"""Saved. Needs a database. Milliseconds."""
user = build_user(**overrides)
database.add(user)
database.flush()
return user