Tags: web-dev concept

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.