← Recipes

阅读中文版本

Add a Test Data Builder

Replace ad-hoc test fixtures with a builder that has valid defaults, explicit overrides, and small factory functions for related objects.

~25 mintestingmaintainabilitytypescript

Task

Make tests readable and resilient to schema changes by constructing only the data each test cares about.

When to use this

Every test repeats a 40-line user fixture. Adding a required field breaks dozens of tests, and reviewers cannot tell which fields matter to each test.

Prerequisites

  • A test suite with repeated object literals
  • TypeScript or a language with typed builders
  • A clear domain model for the object under test

Steps

1. Create a builder with valid defaults and a merge-based override method.

export function aUser(overrides: Partial<User> = {}): User {
  return {
    id: 'user-1',
    name: 'Ada',
    email: '[email protected]',
    role: 'member',
    active: true,
    ...overrides,
  };
}

Expected output: Tests construct a valid user with one line and override only relevant fields.

2. Add small factories for related objects and avoid sharing mutable fixtures between tests.

const user = aUser({ role: 'admin' });
const order = anOrder({ userId: user.id, total: 4200 });

Expected output: Each test owns its data and can vary one dimension without noise.

3. Migrate repeated fixtures to builders and run the suite to confirm behavior is unchanged.

pnpm test

Expected output: The suite passes and fixture duplication is reduced.

Troubleshooting

Problem

Defaults hide a required field

Solution

Keep defaults valid and representative; add a test that constructs the object with no overrides so a schema change fails loudly in one place.

Problem

Builders become a second schema

Solution

Derive the type from the domain model and keep the builder small. Do not reimplement validation.

Limitations

  • Builders do not replace integration tests with real data shapes.
  • Overusing overrides can hide which fields a test actually depends on.

Defaults hide noise

A test should state the one or two values that matter. A builder provides a valid object by default and lets the test override only the relevant fields. The test becomes a description of the scenario instead of a wall of irrelevant data.

const admin = aUser({ role: 'admin' });
const inactive = aUser({ active: false });

One place for schema changes

When a required field is added, the builder is updated once. Tests that do not care about the field keep working. Add a test that calls the builder with no overrides to ensure the default object remains valid.

Avoid shared mutable fixtures

A module-level object shared across tests can be mutated by one test and affect another. Builders return a fresh object each time. For nested objects, build fresh children or deep-clone when the test mutates them.

Verify the builder

Expected outcome: calling the builder with no overrides produces a valid domain object, and changing one field affects only that field. Confirm with a test that validates the default object against the schema and a test that builds two users and asserts they are distinct objects. The suite should fail in one place, not dozens, when a required field is added.