In a frontend test suite, twelve tests each paste the same thirty-line user JSON object and change one field. What is a test data factory, and what do default values plus per-test overrides fix about those tests?
answer
- one builder, many variations
- defaults live in exactly one place
- each test states only its difference
- fresh object per call, never shared
- return type comes from the schema
basics
~20 sA test data factory is a function that returns a valid default payload and merges any per-test overrides into it. Each test then states only the field it cares about, duplicated JSON disappears, and the payload's shape has exactly one place to change.
solid answer
~40 sA factory is a small function like `makeUser(overrides)` that builds a complete, valid object from sensible defaults and spreads the caller's overrides on top. It fixes three things at once. First, the shape is defined once, so a field rename is one edit instead of twelve. Second, the test becomes readable: whatever the test passes as an override *is* the thing under test, and everything else is visibly irrelevant. Third, every call returns a fresh object, which removes the order-dependent bugs you get from mutating one shared fixture. I keep defaults boring and deterministic — no random data — and I type the factory's return value with the type generated from the API schema, so the factory is where a contract change breaks rather than in every test file.
code
typescript · 16 linestype User = { id: string; email: string; displayName: string; isAdmin: boolean };
export function makeUser(overrides: Partial<User> = {}): User {
return {
id: 'u-1',
email: '[email protected]',
displayName: 'Ada',
isAdmin: false,
...overrides,
};
}
const plainUser = makeUser();
const admin = makeUser({ isAdmin: true });
console.log(plainUser.isAdmin, admin.isAdmin, plainUser === admin);go deeper
Be ready to write the factory on a whiteboard: a function taking a partial object, spreading it over defaults, returning a complete value. Say plainly that it removes duplicated payloads and makes each test's intent visible.
Explain the mechanics: shallow spread does not merge nested objects, each call must return a fresh object, and defaults must be deterministic or CI failures cannot be reproduced.
Demonstrate that you treat the factory as the contract seam — typed from the generated schema type, one per response type, so a backend change breaks one file rather than leaking through a whole suite.
Own the convention across teams: whether factories are shared or local, how they are kept generated rather than hand-maintained, and how you stop a library of named variants from becoming an unreviewed parallel definition of the API.
## What a fixture is, and what a factory adds A *fixture* is the fake data a test feeds to the code under test — here, the JSON body a stubbed API call returns. Written by hand in each test, a fixture is a copy of the API's response shape, and copies multiply. A *factory* is a function that produces that fixture. Its signature is almost always the same: take an optional partial object, merge it over a complete set of defaults, return a valid whole. ```typescript type User = { id: string; email: string; displayName: string; isAdmin: boolean }; export function makeUser(overrides: Partial<User> = {}): User { return { id: 'u-1', email: '[email protected]', displayName: 'Ada', isAdmin: false, ...overrides }; } ``` ## The duplication is not the worst part Twelve pasted payloads cost twelve edits when the API renames a field, which is annoying but survivable. The worse cost is that the reader cannot see what a test is about. When a test contains thirty lines of JSON of which one line matters, the signal is buried. With a factory, `makeUser({ isAdmin: true })` announces its own subject: this test is about admins, and nothing else in the payload is load-bearing. ## Defaults should be boring, valid and deterministic Good defaults represent the ordinary case: a well-formed id, a plausible email, flags in their common state. Three rules matter. - **Valid.** The default must be something the real API could actually return. A factory that emits an impossible combination lets tests pass on data production never sends. - **Deterministic.** Randomised defaults from a data-generation library make failures unreproducible: the test that failed in CI cannot be replayed locally because the values differ. If you do randomise, print the seed on failure. - **Neutral.** A default should not accidentally satisfy the assertion. If every user defaults to `isAdmin: false`, a test asserting that admin controls are hidden proves something; if the default were `true`, the same test would prove nothing about the branch. ## Overrides are the test's intent The override argument should carry only the fields the assertion depends on. Everything else is delegated to the default, which is another way of saying "this test does not care". That convention makes a suite skimmable and makes reviewers notice when a test suddenly starts overriding six fields — usually a sign the test is doing too much. ## Composition Realistic payloads nest, and factories compose the same way: ```typescript const order = makeOrder({ customer: makeUser({ isAdmin: false }), lineItems: [makeLineItem()] }); ``` One factory per response type, composed at the call site, keeps each one small. Avoid factories that branch internally on flags — a factory with `if` statements is code that itself needs tests. ## Typing the factory against the contract The factory is the single place worth typing precisely. If its return type is the type generated from the API's OpenAPI or GraphQL schema, then a field that the backend renames or removes breaks the factory file at build time and nowhere else. Untyped factories returning plain object literals give you the deduplication benefit but none of the drift protection, and that is a cheap upgrade to skip. ## Failure modes to recognise - **A shared mutable fixture.** `const user = { ... }` at module scope, mutated by tests, produces pass/fail behaviour that depends on execution order. A factory returns a new object per call and avoids this entirely — but only if it builds the object inside the function rather than returning a reference to one constant. - **Deep-nested spreads.** A shallow spread does not merge nested objects; `makeUser({ profile: { city: 'X' } })` replaces the whole default `profile`. Either document that, or merge nested branches explicitly. - **Factory sprawl.** `makeAdminUser`, `makeSuspendedAdminUser`, `makeSuspendedAdminUserWithNoEmail` — every named variant is a default nobody reviews. Prefer one factory plus explicit overrides at the call site. - **Fixtures as a second API definition.** If the factory's defaults are maintained by hand and never checked against the schema, the suite is testing the app against the team's memory of the API rather than against the API.
- What makes a good set of default values in a fixture factory?Values the real API could actually return, deterministic rather than randomly generated, and neutral with respect to the assertions — a default should never be the reason a test passes. If you use generated random data, print the seed on failure, otherwise a CI failure cannot be reproduced locally.
- How do you stop a set of factories from becoming a second, unchecked definition of the API?Type each factory's return value with the type generated from the API's schema and regenerate those types in CI. The factory then stops compiling when the contract changes, which turns the factory file into the single place drift shows up instead of silently spreading through every test.
saying these in an interview costs you the question
- Copy-pasting a full response payload into every test
- Sharing one mutable fixture object across tests
- Randomised defaults that make failures unreproducible
- Factories that require every field to be passed
- Treating fixtures as throwaway untyped JSON