Snapshot testing has a reputation problem, and it’s mostly earned. Ask ten engineers what they think of snapshot tests and you’ll get “great for catching accidental UI changes” from some and “a 400-line file nobody has read since 2023” from others. Both are describing the same feature. The difference is discipline, not tooling.

A snapshot test captures the output of some function or component the first time it runs, stores it, and fails on future runs if the output changes. That’s the entire mechanism — it’s less an assertion than a diff generator. Whether that diff generator makes your suite more trustworthy or turns your CI into a rubber stamp depends almost entirely on what you snapshot and how you review the failures.

How Snapshot Tests Actually Work

import { describe, it, expect } from "vitest";
import { formatInvoiceLine } from "./invoice";

describe("formatInvoiceLine", () => {
  it("formats a standard line item", () => {
    const result = formatInvoiceLine({
      description: "Consulting hours",
      quantity: 4,
      unitPrice: 15000,
      currency: "USD",
    });

    expect(result).toMatchSnapshot();
  });
});

The first run writes the result to a .snap file next to the test:

// __snapshots__/invoice.test.ts.snap
exports[`formatInvoiceLine formats a standard line item 1`] = `
"Consulting hours    x4    $150.00 each    $600.00 total"
`;

Every subsequent run compares the current output against that stored string. If they differ, the test fails and shows a diff. Crucially, the test itself asserts nothing about what the output should be — that judgment happened once, implicitly, at the moment someone approved the initial snapshot. This is the mechanism’s entire strength and its entire risk, and it’s worth sitting with that before deciding how much of your suite to build on it.

Inline Snapshots: Forcing the Review

A separate .snap file is easy to never open. toMatchInlineSnapshot() writes the expected value directly into the test file, which means the snapshot shows up in the same diff as the code change, in the same code review, right next to the assertion it belongs to.

it("formats a standard line item", () => {
  const result = formatInvoiceLine({
    description: "Consulting hours",
    quantity: 4,
    unitPrice: 15000,
    currency: "USD",
  });

  expect(result).toMatchInlineSnapshot(
    `"Consulting hours    x4    $150.00 each    $600.00 total"`
  );
});

This is a meaningfully better default for anything beyond a handful of snapshots, because it removes the “separate file I never look at” failure mode entirely — a reviewer approving the pull request is, by construction, also looking at the new expected output.

What Makes a Snapshot Worth Keeping

Not all snapshots are equal. The ones that catch real bugs share three properties.

Small. A snapshot of a five-line formatted string is reviewable in two seconds. A snapshot of a 200-node rendered component tree, or a full JSON API response with forty fields, is not — a reviewer will glance at the diff, see “lots of lines changed,” and approve it without reading a single one. The information density of a large snapshot is too low for a human to actually verify.

// Bad: Snapshotting the whole rendered tree — huge diffs on any style change
expect(render(<InvoicePage invoice={sampleInvoice} />)).toMatchSnapshot();

// Good: Snapshotting a specific, narrow piece of derived output
expect(formatInvoiceTotal(sampleInvoice)).toMatchSnapshot();

Stable. A snapshot that includes a timestamp, a random ID, or anything else that changes between runs for reasons unrelated to the code being tested will fail constantly for no real reason — and the fix people reach for under deadline pressure is --update, which trains everyone to stop reading the diff. Strip or normalize anything non-deterministic before it enters the snapshot.

// Bad: Flaky by construction
expect({ id: generateId(), createdAt: new Date(), total: 600 }).toMatchSnapshot();

// Good: Deterministic — inject or normalize the volatile fields
expect({ id: "fixed-test-id", createdAt: "2026-01-01T00:00:00Z", total: 600 })
  .toMatchSnapshot();

Reviewed. A snapshot is only as good as the human judgment applied when it was created or last updated. If nobody actually read the output the first time toMatchSnapshot() ran, the “expected” value isn’t a spec — it’s just whatever the code happened to produce, bugs included. Treat every new or changed snapshot in a diff the same way you’d treat a hand-written assertion: read it, and reject the pull request if it looks wrong, rather than trusting that “the test passed.”

The “Update All” Trap

The single most damaging habit in snapshot testing is running the blanket update command to make a failing suite green again without reading what changed.

# This accepts every current output as correct, no matter what it says
vitest run -u
jest --ci=false -u

Picture the sequence that actually happens on most teams: a refactor accidentally breaks currency formatting, $600.00 becomes $600. Forty snapshot tests fail. Someone under deadline pressure runs -u, glances at “40 snapshots updated,” and moves on. The bug is now enshrined as the expected output, and it will stay silently broken until a human notices in production — at which point the test suite, far from catching the regression, actively certified it as correct.

The mitigation isn’t a tooling fix, because the tool is working exactly as designed. It’s process: -u should never be run against more failures than a person is willing to individually read, and CI should never auto-accept snapshot updates. Some teams go further and forbid -u in CI outright, requiring updates to happen locally where a diff is actually in front of someone before it’s committed.

When to Reach for Something Else

SituationBetter fit
A single, short, deterministic output (formatted string, small serialized object)Snapshot (ideally inline)
A calculation with a clear expected valueExplicit assertion (expect(total).toBe(600))
Business rules with edge cases (rounding, thresholds, boundary conditions)Explicit assertions, one per case — a snapshot hides which input produced which output
Full-page visual appearance, layout, CSS regressionsVisual regression tooling (Percy, Chromatic, Playwright’s screenshot comparisons)
Large API response shapeA JSON Schema / Zod contract test, not a snapshot of the whole payload

The explicit-assertion case deserves emphasis. Compare:

// Snapshot: passes today, but a future reader has to reverse-engineer
// *why* 600 is correct, and a bug in the original snapshot is invisible
expect(calculateTotal(4, 15000)).toMatchSnapshot();

// Explicit assertion: the expectation and its reasoning are visible in the test itself
expect(calculateTotal(4, 15000)).toBe(60000); // 4 units x $150.00 = $600.00

A snapshot answers “did the output change.” An explicit assertion answers “is the output correct.” For business logic with a computable expected value, that distinction matters, and snapshots quietly trade the second question for the first.

Visual regression is the other place snapshot-style thinking earns its keep, but with proper tooling rather than a string diff of DOM output — pixel-level comparison, threshold tolerances, and a visual review UI are what make large, holistic diffs actually reviewable, which is exactly the property a giant text snapshot lacks.

Snapshot Testing and Code Review Culture

The failure mode described above isn’t really about the tool — it’s about what a code review is allowed to skip. A snapshot test moves the actual verification step out of the test run and into the pull request diff, which only works if reviewers treat a .snap change with the same scrutiny they’d give a hand-written assertion. In practice, a large or unfamiliar snapshot diff invites exactly the opposite: reviewers trust that CI passing means the change is fine, when all CI actually verified is that the new output matches whatever was last approved, correct or not.

This is worth naming explicitly on a team, because the incentive is subtle. A hand-written assertion forces the author to commit to a specific expected value before the test can even be written, which means they’ve already reasoned about correctness. A snapshot lets the author generate the expected value from the code itself, which quietly skips that step unless someone deliberately reads the result afterward. Teams that get burned by snapshot rot are usually teams where “the test suite is green” replaced “someone verified this is correct” as the definition of done — a substitution that snapshot testing makes unusually easy to fall into by accident.

A Practical Policy

Teams that get value out of snapshot testing long-term tend to converge on the same handful of rules: prefer inline snapshots so review happens where the code review already happens; keep each snapshot small enough that a diff can be read in one glance; never let CI or a script silently accept snapshot updates; and reserve snapshots for output where “did anything change” is genuinely the useful question, reaching for explicit assertions the moment you can state what the correct value should be.

Key Takeaways

  • A snapshot test is a diff generator, not an assertion — its usefulness depends entirely on whether the stored value was reviewed when it was created.
  • Prefer inline snapshots over separate .snap files so the expected output appears in the same code review as the change.
  • Keep snapshots small and deterministic; large or non-deterministic snapshots get rubber-stamped and stop catching anything.
  • Never let a blanket -u/--update run be someone’s default fix for a red suite — it silently promotes bugs into the expected baseline.
  • Use explicit assertions instead of snapshots whenever you can state the correct expected value, especially for business logic and edge cases.
  • Reserve full-tree or full-page snapshots for dedicated visual regression tools with real image diffing, not string comparison.
  • If nobody on the team can remember the last time a snapshot diff was actually read during review, that’s a signal to prune or replace it.