A shared fixture file defines the users, accounts, and orders every test uses. It started with five records and now has two hundred. Nobody adds to it without checking what breaks, and nobody removes anything at all.
Changing one record's status breaks eleven tests in four files, none of which mention that record's status in their assertions. The fixture has become a global variable that every test depends on invisibly.
What changed in 2026
- Factories displaced fixture files. Building objects programmatically with defaults and overrides became the dominant pattern.
- Transaction rollback isolation became standard. Wrapping each test in a rolled-back transaction became the default with real database testing — see testcontainers.
- Test-local data became a review expectation. Tests depending on shared state started being flagged in review.
- Builder patterns spread. Chained construction expressing only the relevant attributes became common.
Why shared fixtures rot
Three mechanisms, all compounding.
Invisible coupling. A test asserting that a query returns three results depends on the fixture containing exactly three matching records. Nothing in the test says so. Add a fourth and the test fails for reasons unrelated to what it tests.
Growth without removal. Adding a record is safe; removing one might break something. So fixtures only grow, and eventually loading them is itself slow.
Unreadable tests. A test referencing "user three" tells you nothing about what makes that user relevant. You must open the fixture file and cross-reference — and the reader of a test should not need to.
|
Shared fixtures |
Test-local factories |
| Coupling between tests |
High |
None |
| Readability |
Requires cross-referencing |
Self-contained |
| Setup cost |
Once, then shared |
Per test |
| Changing data |
Breaks unrelated tests |
Affects one test |
| Growth over time |
Only grows |
Stays proportional |
Factories with explicit overrides
The pattern that works: a factory function creating a valid object with sensible defaults, and the test overriding only what matters to it.
A test about expired subscriptions creates a user with an expired subscription, stating that explicitly. Everything else — name, email, identifiers — comes from defaults the test does not care about and does not mention.
Two properties follow. The test is readable, because the only attributes mentioned are the ones that matter. And it is independent, because it created its own data and nothing else depends on it.
Defaults should produce a valid, unremarkable object. Uniqueness matters — factories generating a fixed email will violate unique constraints on the second call, so sequences or randomness are needed for unique fields.
Associations deserve care: a factory creating an order should create its customer unless one is supplied, and it should not create a whole graph of unrelated objects by default, which is how factory-based setup becomes slow.
Isolation
Tests must not see each other's data. Two approaches.
Transaction rollback. Wrap each test in a transaction and roll it back afterwards. Extremely fast, and the database returns to its prior state with no cleanup. The limitation is code managing its own transactions, or tests that need to verify commit behaviour.
Truncation. Empty the affected tables between tests. Slower, and it works for cases rollback cannot handle.
Rollback should be the default, with truncation as the exception. Both keep tests independent, which is what allows them to run in any order and in parallel.
Parallel execution needs separate schemas or databases per worker; sharing one with rollback isolation still contends on the same rows.
Common mistakes
- A large shared fixture set. Becomes untouchable.
- Tests relying on unstated defaults. Unreadable and fragile.
- Factories creating deep object graphs by default. Slow setup.
- Fixed values in factories. Unique constraint violations.
- Cleaning up by deleting in teardown. Slower than rollback and error-prone.
- Tests depending on execution order. Breaks under parallelism.
- Reusing development seed data in tests. Different requirements — see database seeding strategies.
FAQ
Are fixtures ever appropriate?
For genuinely static reference data — country codes, currency lists, permission definitions — a shared fixture is reasonable, because it is the same everywhere and nobody varies it. The problem is entity data that tests want to differ.
Do factories make tests slow?
They can, if they create more than needed. Keeping default associations minimal and letting tests opt into extra objects keeps setup proportional.
How do I test with large datasets?
Separately from unit tests. Performance behaviour needs volume, and that belongs in a dedicated test rather than in every test's setup.
Should factories hit the database?
Depends on the test. Persisted objects for anything exercising queries; in-memory construction for pure logic. Most factory libraries support both.
Where to go next
For running tests against a real database, read testcontainers. For development data rather than test data, database seeding strategies, and for output-comparison testing, golden file testing.