Snapshot Testing
Recording output and comparing it on every run — where snapshots earn their place, the approval reflex that destroys their value, and better alternatives.
2 min read · updated 19 September 2026
A snapshot test records output the first time it runs and compares against that record on every subsequent run. Jest popularised it; every major runner now has one.
It is a genuinely useful technique with a specific and well-documented way of going wrong.
#Where it earns its place
Output that is large, where the correctness criterion is genuinely "unchanged since a human last checked", and where a diff is readable.
// Formatted output: the diff is legible and every change is meaningful.
it('formats an invoice', () => {
expect(formatInvoice(invoice)).toMatchInlineSnapshot(`
"Invoice INV-1 1 January 2026
─────────────────────────────────────────────────
Field Notes 2 × £12.00 £24.00
Shipping £3.95
─────────────────────────────────────────────────
Total £27.95"
`);
});// Generated SQL: you want to know the moment the query builder changes.
it('builds the orders query with a tenant filter', () => {
expect(buildOrdersQuery({ tenantId: 't1', status: 'paid' })).toMatchInlineSnapshot(
`"SELECT * FROM orders WHERE tenant_id = $1 AND status = $2 ORDER BY placed_at DESC"`
);
});// C#, Verify — the .NET approval-testing library.
[Fact]
public Task Serializes_an_order_for_the_public_api()
{
var order = OrderBuilder.Paid(sku: "book-1", quantity: 2);
return Verify(ApiSerializer.Serialize(order))
.ScrubMember("Id") // non-deterministic fields, scrubbed
.ScrubInlineGuids();
}#Where it fails
// A 400-line tree. Changes on every styling tweak. Approved without reading.
expect(render(<OrderPage order={order} />).container).toMatchSnapshot();The mechanism of failure is social:
- A refactor changes the markup in a way nobody cares about.
- Forty snapshots fail.
- Someone runs
jest -u. - The diff is 4,000 lines; nobody reads it.
- Repeat monthly.
After six months the snapshots record whatever the code does, which is precisely the thing a test is supposed to be independent of. They still appear in the count, still take time to run, and assert nothing.
#Rules that keep them honest
Inline, not external. toMatchInlineSnapshot puts the expected value in
the test file, so a pull request shows the change in context rather than in
a .snap file reviewers skip.
Small. If the snapshot does not fit on one screen, the assertion is too broad. Snapshot the thing you care about:
// Instead of the whole tree:
expect(screen.getByRole('table', { name: 'Order lines' }).textContent)
.toMatchInlineSnapshot(`"Field Notes2£12.00£24.00"`);Deterministic. Dates, UUIDs, random values and ordering must be pinned or scrubbed, or the snapshot becomes a flaky test.
expect(order).toMatchSnapshot({
id: expect.any(String),
createdAt: expect.any(Date)
});--ci in the pipeline. Without it, a missing snapshot is written and
the test passes — so a new test can pass on its first CI run having asserted
nothing.
jest --ci # a missing snapshot fails instead of being writtenFail on obsolete snapshots. An orphaned snapshot is dead weight that also hides the fact that a test was deleted.
// jest.config.js
module.exports = { ci: true };#Better alternatives, usually
Explicit assertions. Three lines naming the three things that matter, which fail with a message about what was wrong rather than a diff.
Property assertions. toMatchObject checks the fields you care about
and ignores the rest, so unrelated additions do not break it.
Contract or schema validation. For API responses, asserting against a JSON Schema or OpenAPI document says what must be true rather than what happened to be true. See contract testing.
Visual regression for anything genuinely visual. A DOM snapshot cannot see that the button is now white on white; a screenshot comparison can:
// Playwright. This is what people usually want when they snapshot markup.
await expect(page).toHaveScreenshot('checkout.png', { maxDiffPixels: 100 });See screenshots.
#The review test
Before adding a snapshot, ask: when this fails in six months, will anyone read the diff?
If the honest answer is no, it is not a test. Write the three assertions that will actually be read instead.
Common questions
- Why do people warn against snapshot testing?
- Because the failure mode is social rather than technical. A large snapshot fails on every unrelated change, the team learns to run --update-snapshot reflexively, and after a few months the snapshots assert nothing at all while still appearing to be tests.
- When is a snapshot the right tool?
- When the output is large, the correctness criterion is "unchanged", and a human genuinely reviews the diff — serialized API responses, generated SQL, formatted documents, configuration output. Small and meaningful beats large and comprehensive every time.
- Should I snapshot React component trees?
- Rarely. A rendered tree changes for many reasons that do not matter and the diff is unreadable, so it is the snapshot most likely to be approved without being read. Assert on the specific things a user would notice instead.
Runnable samples for this page
last test results ↗- TypeScript
typescript/src/web-frameworks/snapshot-testing
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- JestThe default JavaScript test runner — configuration, projects, snapshot testing, fake timers, and the choices that keep a large Jest suite fast.
- Testing ReactComponents, hooks, context, server components and async state — what to test in a React application, what to leave alone, and how to avoid act warnings.
- ScreenshotsCapturing the screen at the moment of failure, and using screenshot comparison as visual regression testing — configuration, masking and the flakiness to design out.
- Naming and Structuring TestsTest names that say what broke without opening the file, the naming conventions worth adopting, and how to organise a suite so people can find things.
- VitestA Vite-native test runner with a Jest-compatible API — faster startup, native ESM and TypeScript, and the cases where it is the better default.
- Flutter TestingFlutter renders to a canvas with no native accessibility tree, so it brings its own three-layer test harness — unit, widget and integration — plus golden files.
- iOS App TestingXCTest, Swift Testing, XCUITest and the accessibility identifiers that make UI automation possible — plus what to run on a simulator and what needs a device.
- Test-Driven DevelopmentRed-green-refactor, what TDD actually changes about a codebase, where it fits badly, and the honest evidence for and against it.