Test Data
Date: 2026-08-17
The data a test needs to run. Get it wrong and tests depend on each other, fail in a different order, and pass locally while failing in CI — which is most flakiness, arriving through the back door.
Test data is the state a test requires: database rows, fixtures, API responses, files. The requirement is that each test controls its own, and doesn’t inherit or leave state.
Fixtures versus factories
FIXTURE — a fixed dataset, loaded
before tests
+ realistic, shared, one setup cost
− opaque: which row makes THIS test
pass?
− brittle: changing it breaks
unrelated tests
− grows forever
FACTORY — a function creating what a
test needs
+ explicit — the test states its
own preconditions
+ isolated
− a little more setup code
Factories are the better default, because the test becomes readable on its own:
const order = makeOrder({
status: 'paid',
items: [makeItem({ price: 4999 })],
})Everything not stated has a sensible default, and everything relevant to the test is visible in the test. With a shared fixture, the reader has to go and find which of 200 rows this assertion depends on.
Say only what matters
// BAD — which field is the test about?
const order = makeOrder({
id: 'ord_1', customerId: 'cus_1',
status: 'paid', total: 4999,
currency: 'GBP', createdAt: '2026-01-01',
items: [...],
})
// GOOD — the test is about refunds
const order = makeOrder({ status: 'refunded' })Overspecified test data hides the point of the test and makes it break when an unrelated field changes.
Isolation
The rule: a test must pass alone, in a suite, and in any order.
TRANSACTION ROLLBACK
wrap each test, roll back after
→ fast, complete, the best option
where the framework supports it
TRUNCATE AND RESEED
slower, works everywhere
UNIQUE DATA PER TEST
namespaced ids, generated emails
→ enables parallelism
A test that passes alone and fails in the suite is a state leak. Running the suite in a random order surfaces these deliberately, and it’s worth enabling — order-dependence is otherwise invisible until it breaks in CI.
Determinism
NON-DETERMINISTIC FIX
Date.now() freeze the clock
Math.random() seed it
auto-increment ids don't assert on
them
`new Date()` defaults inject a clock
locale-dependent set the locale
formatting explicitly
timezone set TZ in the
test environment
Timezone is the one that catches people out. A test passing in London and failing in CI running UTC is a genuine bug in the code that the test found — Test Doubles.
Realistic, hostile data
Test data that’s too tidy is why “works in testing” happens:
"Serum" fine
"O'Brien-Smith" apostrophe, hyphen
"Zoë Müller" accents — Character Encoding
"Professional Anti-Ageing
Serum With Hyaluronic
Acid 50ml Twin Pack" length — breaks layouts
"" empty
" " whitespace only
"<script>alert(1)</script>" escaping
"👨👩👧" multi-codepoint
0, -1, 0.005, 999999 numeric boundaries
See: Character Encoding
Build these into the factory defaults occasionally rather than always using “Test User”. A layout that only ever renders short tidy names has never been tested — Component States.
Never use production data
NEVER a copy of the production
database in a test
environment
WHY real customer PII, under
UK GDPR, in a system with
weaker access controls
and no retention policy
— PII in Analytics
INSTEAD generate realistic fake
data
or anonymise properly
which is harder than it
sounds and rarely done
correctly
See: PII in Analytics
“Anonymised” production data frequently isn’t. Re-identification from combinations of fields is straightforward, and a dump with names removed but postcodes, order histories and dates intact is still personal data.
Generate instead. A seeding script producing 10,000 plausible orders is a day’s work and removes the problem permanently.
Seeding for development
The related case, and worth doing well:
npm run db:seed
→ enough data to develop against
→ deterministic, so bugs reproduce
→ includes the edge cases: an order
with 50 items, a customer with
none, an out-of-stock product
A good seed script is part of onboarding, and the difference between a new developer being productive in an hour and in a day — Local Development Setup.