Skip to content
Back to Quality Assurance

Snapshot Testing

Capture the rendered output of a component or serialized structure and fail on any unintended change.

Why

For complex UI components with many props and states, writing explicit assertions for every rendered node is impractical — there are hundreds of elements, and the expected HTML changes every time the design is updated. Snapshot testing captures the full rendered output once, stores it as a committed artifact, and on every subsequent run compares the new output against the stored snapshot. Any diff fails the test, forcing a developer to either accept the change (update the snapshot) or fix the regression. It is the most efficient way to guard against unintended UI changes, but it is a blunt instrument — it catches everything, including changes that do not matter.

When

Use for presentational components with stable structure (a card, a table row, a form layout). Avoid for components with dynamic content (timestamps, random IDs, async data) unless you mock those. Do not use as a replacement for behavioral tests.

How

1. Choose the snapshot mechanism
   - Vitest/Jest: toMatchInlineSnapshot() or toMatchSnapshot()
   - React: react-test-renderer or @testing-library/react renderToStaticMarkup
   - Storybook: Storyshots addon for visual snapshots per story

2. Write the first snapshot — render the component with representative props
   - Use realistic but deterministic data (no Date.now(), no Math.random())
   - Mock any async data fetching so the snapshot is stable across runs
   - The first run creates the snapshot file; commit it to the repo

3. On subsequent runs, the test compares new output to the stored snapshot
   - Match → test passes (no unintended change)
   - Diff → test fails with a readable diff; the developer reviews it

4. When the change is intentional, update the snapshot
   - vitest -u (or jest -u) regenerates the snapshot
   - Review the diff in the PR carefully — a blind update accepts regressions
   - Commit the updated snapshot alongside the code change

5. Periodically review and prune snapshots
   - Snapshots grow stale as components evolve; a 2000-line snapshot is a smell
   - If a snapshot is mostly dynamic noise, replace it with targeted assertions

Example

// src/components/ProductCard.test.tsx
import { render } from "@testing-library/react";
import { ProductCard } from "./ProductCard";

it("matches the saved snapshot", () => {
  const { container } = render(
    <ProductCard
      product={{
        id: 42,
        name: "Wireless Headphones",
        price: 79.99,
        image: "/img/42.png",
        inStock: true,
      }}
    />,
  );
  expect(container.firstChild).toMatchSnapshot();
});

// First run → creates __snapshots__/ProductCard.test.tsx.snap:
// exports[`matches the saved snapshot 1`] = `<div class="card">...</div>`;

// Later, if someone changes the card layout, the test fails:
//   - 4 lines removed
//   + 6 lines added
// Review the diff. If intentional: npx vitest -u to update.

Common Problems

  • ⚠Blindly running vitest -u to make the test green without reviewing the diff accepts regressions — the snapshot becomes a record of whatever the component currently renders, guarding nothing.
  • ⚠Snapshots of components with unmocked dynamic data (timestamps, UUIDs, randomized order) fail on every run, so developers habitually update them, training the team to ignore snapshot failures entirely.
  • ⚠Giant snapshots (500+ lines) fail on any tiny change and produce unreadable diffs, so nobody reviews them — the snapshot exists but provides no useful signal.
  • ⚠Using snapshot tests as the only test for a component means a behavior change (button onClick no longer fires) is not caught, because the snapshot only compares rendered HTML, not behavior.

Best Practices

  • ✓Keep snapshots small by rendering the component with minimal props and asserting complex behavior separately — a 30-line snapshot is reviewable; a 300-line one is not.
  • ✓Mock all sources of nondeterminism (Date.now, Math.random, UUID generation, fetch) before rendering, so the snapshot is byte-identical across runs and a diff always means a real change.
  • ✓Always review the snapshot diff in the PR before accepting the update — if you cannot explain every line of the diff, you do not understand the change well enough to merge it.
  • ✓Use inline snapshots (toMatchInlineSnapshot) for small components so the expected output lives in the test file and is visible in code review without opening a separate .snap file.