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