← Recipes

阅读中文版本

Add Golden File Tests for API Responses

Commit canonical API responses as golden files, review every diff in pull requests, and regenerate only through an explicit opt-in command.

~30 mintestingregressionapi

Task

Detect unintended API response changes with committed golden files that are updated deliberately.

When to use this

A refactor renames a JSON field. Unit tests still pass because they assert a single property, and clients break in production when the response shape changes.

Prerequisites

  • Node.js 22 with node:test or Vitest
  • An endpoint whose response can be made deterministic
  • A code review process that inspects committed diffs

Steps

1. Write a golden helper that only updates files when an explicit environment flag is set.

import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import assert from 'node:assert/strict';

const file = (name) => fileURLToPath(new URL(`./golden/${name}.json`, import.meta.url));
const sortKeys = (value) =>
  Array.isArray(value) ? value.map(sortKeys)
    : value && typeof value === 'object'
      ? Object.fromEntries(Object.keys(value).sort().map((k) => [k, sortKeys(value[k])]))
      : value;

export async function assertGolden(name, payload) {
  const actual = JSON.stringify(sortKeys(payload), null, 2) + '\n';
  if (process.env.UPDATE_GOLDEN === '1') {
    await mkdir(new URL('./golden/', import.meta.url), { recursive: true });
    await writeFile(file(name), actual);
  }
  const expected = await readFile(file(name), 'utf8');
  assert.equal(actual, expected, `golden mismatch: ${name}.json`);
}

Expected output: UPDATE_GOLDEN=1 creates or rewrites the file; a normal run compares and fails with the file name.

2. Remove nondeterminism: fixed clock, fixed identifiers, sorted keys, and a projection of contract fields only.

const payload = {
  data: orders.map((o) => ({ id: o.publicId, status: o.status, total: o.totalCents })),
  meta: { page: 1, perPage: 20 },
};
await assertGolden('orders-success', payload);

Expected output: Two consecutive runs produce byte-identical files with no timestamps or database-generated ids.

3. Cover the shapes clients depend on: success, validation error, and an empty collection.

UPDATE_GOLDEN=1 node --test test/api-golden.test.js

Expected output: three files under test/golden: orders-success.json, orders-validation-error.json, orders-empty.json

4. Review the diff like source code, then commit the golden files with the change that caused them.

git diff --stat test/golden && git diff test/golden

Expected output: The diff shows exactly the intended field change; unrelated churn is reverted before committing.

5. Refuse silent regeneration on CI so a bad snapshot cannot be accepted automatically.

if (process.env.CI === 'true' && process.env.UPDATE_GOLDEN === '1') {
  throw new Error('UPDATE_GOLDEN is not allowed in CI');
}

Expected output: CI fails fast if the update flag is set, forcing snapshots to be updated and reviewed locally.

Troubleshooting

Problem

The golden file changes on every run

Solution

A timestamp, random id, or object key order is leaking in. Inject a fixed clock, map ids to stable values, and sort keys before serializing.

Problem

Nobody reads golden diffs before approving

Solution

Keep one small file per case, name it after the behaviour, and treat a golden diff as an API change requiring the same review as a schema change.

Problem

CI fails only because of line endings

Solution

Normalize output to LF and add .gitattributes with text eol=lf for the golden directory.

Limitations

  • Exact byte comparison is noisy for formatting-only changes, so normalization is part of the design.
  • Golden files do not validate types or required fields; pair them with JSON Schema or OpenAPI checks.
  • Committed snapshots can contain secrets or personal data; redact them and never capture production responses unmodified.

Golden files make changes visible

Most API tests assert a couple of fields: status code, one id, maybe an error message. They pass when a refactor renames created_at to createdAt, removes a nested object, or changes a number to a string. A golden file compares the whole canonical response, so any change shows up as a diff. The diff is the point: it forces the author and reviewer to decide whether the change is intentional.

Determinism comes first

A golden file that changes every run is worse than no test, because the team learns to regenerate it blindly. Remove timestamps, database-generated ids, randomized ordering, and environment-specific paths. Sort object keys, format with a fixed indentation, and select only fields that are part of the contract. If an id must appear, map it to a stable synthetic id such as order-1. Inject a fixed clock rather than mocking the date globally, so tests remain parallel-safe.

const actual = JSON.stringify(sortKeys(payload), null, 2) + '\n';
if (process.env.UPDATE_GOLDEN === '1') await writeFile(file(name), actual);

Update is an explicit act

The update path must be impossible to trigger by accident. An environment variable checked at the top of the helper is enough: normal runs compare, and UPDATE_GOLDEN=1 rewrites. Add a CI guard that rejects the flag, so a pipeline cannot bless a broken response. Locally, regenerate deliberately and read the diff; the command is a tool, not a fix.

Keep files small and named by behaviour

One large snapshot for the whole API is unreadable and conflicts constantly. Prefer one file per endpoint and case, such as orders-success.json, orders-validation-error.json, and orders-empty.json. Small files produce focused diffs, review quickly, and rarely conflict when two branches change different endpoints.

Pair with stronger checks

Golden files capture shape changes but do not tell you whether the shape is valid. Combine them with a JSON Schema or OpenAPI response validation, and with a few targeted assertions for business values. The golden file answers “did anything change”, the schema answers “is this allowed”, and the assertions answer “is this correct”. Together they cover what clients actually rely on.