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.