Use factory functions (e.g., `createUser({role: 'admin'})`) instead of static JSON fixtures. Factories let you create exactly the data each test needs with sensible defaults, while fixtures force you to maintain large JSON files where a change to one test's data breaks another test.
Why This Matters
Static JSON fixtures create coupling between tests: every test that imports `fixtures/user.json` depends on the same data shape. When test A needs the user to have an `admin` role and test B needs `viewer`, you either create dozens of nearly-identical fixture files or use a single fixture that doesn't accurately represent either scenario. Factories solve this by generating test data on demand with sensible defaults and per-test overrides.
Static fixtures (JSON files, hardcoded objects) seem simple, but they create problems at scale:
Coupling: multiple tests share the same fixture. Changing it for one test breaks others.
Bloat: to avoid coupling, teams create dozens of near-identical fixtures: user.json, adminUser.json, userWithoutEmail.json, userWithExpiredSubscription.json.
Opacity: reading a test that uses fixtures.user, you must open the fixture file to understand what data the test is working with. The test's intent is hidden.
Staleness: when the data model changes (new required field), every fixture file must be updated manually.
Factories solve all of these by generating test data programmatically with sensible defaults. Each test specifies only the fields that matter for that test.
The rule
Create factory functions that generate test data with sensible defaults. Each test overrides only the fields relevant to its scenario. Use a library like fishery or write simple factory functions by hand.
Bad example
// fixtures/user.json{ "id": "user-1", "name": "Test User", "email": "test@example.com", "role": "viewer", "plan": "free", "createdAt": "2024-01-01T00:00:00Z"}// test fileimport user from "../fixtures/user.json";it("allows admin to delete users", () => { // BAD: fixture has role: "viewer", but this test needs "admin" // Options: modify the shared fixture (breaks other tests) // or create adminUser.json (fixture sprawl) const admin = { ...user, role: "admin" }; // spread-and-override in every test expect(canDeleteUsers(admin)).toBe(true);});
Good example
// test/factories/user.tslet counter = 0;export function createUser(overrides: Partial<User> = {}): User { counter++; return { id: `user-${counter}`, name: `Test User ${counter}`, email: `user-${counter}@test.com`, role: "viewer", plan: "free", createdAt: new Date().toISOString(), ...overrides, };}// test file — each test specifies exactly what mattersimport { createUser } from "../factories/user";it("allows admin to delete users", () => { const admin = createUser({ role: "admin" }); expect(canDeleteUsers(admin)).toBe(true);});it("prevents viewers from deleting users", () => { const viewer = createUser({ role: "viewer" }); expect(canDeleteUsers(viewer)).toBe(false);});it("requires paid plan for export", () => { const freeUser = createUser({ plan: "free" }); const paidUser = createUser({ plan: "pro" }); expect(canExport(freeUser)).toBe(false); expect(canExport(paidUser)).toBe(true);});