Skip to main content

Snapshot Testing

What a Snapshot Is

A snapshot test serializes a value, stores it in a file, and fails whenever the value changes:

test("builds the order confirmation payload", () => {
const payload = buildConfirmation(order);
expect(payload).toMatchSnapshot();
});

The first run always passes — Jest has nothing to compare against, so it writes the serialized value to __snapshots__/orderConfirmation.test.js.snap:

__snapshots__/orderConfirmation.test.js.snap
exports[`builds the order confirmation payload 1`] = `
{
"currency": "PKR",
"customerName": "Hafsa Ashiq",
"items": [
{
"name": "Wireless Keyboard",
"quantity": 1,
"unitPrice": 7500,
},
],
"subject": "Order #4521 confirmed",
"total": 7500,
}
`;

Every later run compares the fresh output against this file and shows a diff on mismatch. The .snap file is code: commit it, and review it in pull requests like any other file.

When output legitimately changes, regenerate with:

npx jest -u

That flag is where snapshot testing lives or dies — more on that below.

Inline Snapshots

For small values, toMatchInlineSnapshot writes the snapshot directly into the test file on first run:

test("formats the invoice line", () => {
expect(formatLine({ name: "USB Cable", qty: 3, price: 450 })).toMatchInlineSnapshot(
`"USB Cable x3 — Rs 1,350"`
);
});

I prefer inline snapshots whenever they fit on a few lines: the expected value sits next to the assertion where reviewers actually look, instead of in a .snap file most people scroll past.

Where Snapshots Genuinely Help

Snapshots earn their keep when the output is structured, deterministic, and tedious to assert field-by-field:

  • Generated text: email bodies, CLI help output, error messages with formatting
  • Serialized config: a function that assembles a webpack/CI/JSON config object
  • API response shapes: locking down a public contract so accidental field renames fail loudly
  • Complex transformations: a report builder that turns raw rows into a nested summary

The common thread: you've manually verified the output once, and from now on you only care that it doesn't change by accident. A snapshot converts "someone eyeballs this occasionally" into "any change requires an explicit decision in review."

Where Snapshots Are a Trap

Trap 1: The snapshot nobody reads

A 400-line snapshot of an entire rendered page or full API response fails on every unrelated change. After the third false alarm, everyone's fix becomes:

npx jest -u # "snapshots were failing, updated them"

At that point the test verifies nothing — it's a rubber stamp with extra CI minutes. If you can't read the whole snapshot diff in a code review and say "yes, intended," the snapshot is too big. Snapshot the piece you care about, not the world.

Trap 2: Snapshots don't encode intent

A regular assertion documents why:

expect(invoice.total).toBe(7500); // quantity 1 at 7500, no shipping under Rs 10,000

A snapshot only documents what happened to be true when someone pressed save. If buildInvoice had a rounding bug on the day the snapshot was written, the snapshot enshrines the bug and passes forever. Snapshots protect against change, not against wrongness — never use one as the only test for logic that computes something.

Trap 3: Non-determinism

Anything with a timestamp, random ID, or environment-dependent value fails on the second run. Fix it with property matchers — exact-match the stable fields, type-match the volatile ones:

test("creates an order record", () => {
const order = createOrder({ customer: "Ibrahim", total: 2200 });

expect(order).toMatchSnapshot({
id: expect.any(String),
createdAt: expect.any(Date),
});
});

The stored snapshot keeps customer: "Ibrahim" and total: 2200 literal, while id and createdAt are recorded as Any<String> / Any<Date>. Combine with jest.setSystemTime from Async Testing when time leaks in deeper than the top level.

Rules I Hold Snapshots To

  1. Small enough to fully read in a PR diff — otherwise assert on the relevant slice instead.
  2. Never the only test for computed logic; pair with explicit assertions on the numbers that matter.
  3. -u only after reading every diff. Updating a snapshot is changing an expected value — treat it with the same care as editing toBe(7500) to toBe(7600).
  4. Deterministic input, pinned clock, property matchers for generated fields.
  5. If a snapshot has been blindly updated twice, delete it and write real assertions. It has told you it's not load-bearing.