|$ curl https://forge-ai.dev/api/markdown?path=docs/testing/snapshots
$cat docs/testing-—-snapshot-testing.md
updated Recently·10 min read·published

Testing — Snapshot Testing

TestingIntermediate
Introduction

Snapshot testing is a technique where the output of your code (typically a rendered component or serialized data structure) is captured and compared against a stored reference file. If the output changes unexpectedly, the test fails, alerting you to potential regressions.

Introduced by Jest in 2016, snapshot testing has become both popular and controversial. When used judiciously, snapshots catch unintended UI changes and configuration drift. When overused, they produce brittle tests that fail on every minor change and are rarely inspected for correctness.

How Snapshot Testing Works

The snapshot workflow has three phases: creation, comparison, and update. On first run, Vitest or Jest creates a snapshot file. On subsequent runs, the new output is compared to the stored snapshot. If they match, the test passes. If they differ, the test fails and you must decide whether the change is intentional.

snapshot-basics.test.tsx
TypeScript
1// Snapshot test — first run creates snapshot, subsequent runs compare
2import { render } from "@testing-library/react";
3import UserProfile from "./UserProfile";
4
5test("renders user profile snapshot", () => {
6 const { container } = render(
7 <UserProfile
8 user={{
9 id: 1,
10 name: "Alice Johnson",
11 email: "alice@example.com",
12 avatar: "/avatars/alice.jpg",
13 role: "admin",
14 joinDate: "2024-01-15",
15 }}
16 />
17 );
18
19 // Creates or compares snapshot of the rendered HTML
20 expect(container.firstChild).toMatchSnapshot();
21});
22
23// First run output — creates: __snapshots__/UserProfile.test.tsx.snap
24// Snapshot file content:
25// exports[`renders user profile snapshot 1`] = `
26// <div class="user-profile">
27// <img src="/avatars/alice.jpg" alt="Alice Johnson" class="avatar" />
28// <div class="user-info">
29// <h2 class="name">Alice Johnson</h2>
30// <p class="email">alice@example.com</p>
31// <span class="badge badge-admin">Admin</span>
32// <p class="join-date">Joined: 2024-01-15</p>
33// </div>
34// </div>
35// `;
36
37// If component output changes, test fails with diff:
38// - Snapshot
39// + Received
40// <div class="user-profile">
41// - <img src="/avatars/alice.jpg" class="avatar" />
42// + <img src="/avatars/alice.jpg" class="avatar" alt="Alice Johnson" />
43// </div>

info

Snapshot files should be committed to version control. They serve as the reference for future comparisons. When reviewing PRs, check snapshot diffs carefully — a changed snapshot is often a sign of an intentional UI change, but can also hide bugs if reviewed carelessly.
Inline Snapshots

Inline snapshots store the snapshot value directly in the test file instead of a separate .snap file. This keeps the expected output visible alongside the test code, making it easier to understand what the test is verifying.

inline-snapshots.test.tsx
TypeScript
1// Inline snapshot — stored in the test file itself
2import { render } from "@testing-library/react";
3import Badge from "./Badge";
4
5test("renders success badge", () => {
6 const { container } = render(<Badge variant="success">Completed</Badge>);
7
8 // toMatchInlineSnapshot() stores the snapshot here
9 expect(container.firstChild).toMatchInlineSnapshot(`
10 <span
11 class="badge badge-success"
12 >
13 Completed
14 </span>
15 `);
16});
17
18test("renders error badge", () => {
19 const { container } = render(<Badge variant="error">Failed</Badge>);
20
21 // On first run, Vitest replaces the argument with actual output
22 expect(container.firstChild).toMatchInlineSnapshot();
23 // After first run, the snapshot is written inline:
24 // expect(container.firstChild).toMatchInlineSnapshot(`
25 // <span
26 // class="badge badge-error"
27 // >
28 // Failed
29 // </span>
30 // `);
31});
32
33// Benefits of inline snapshots:
34// 1. Snapshot is visible next to the assertion — easier to review
35// 2. No separate .snap file to manage
36// 3. Changes are immediately visible in PR diffs
37// 4. Harder to ignore failing snapshots (they're in your face)
38
39// Tradeoffs:
40// 1. Longer test files
41// 2. No reusability (each snapshot is specific to one test)
42// 3. Editor might auto-format the snapshot string

warning

Inline snapshots can cause merge conflicts in code reviews. If two developers both change the same component, the inline snapshot will conflict. For shared components in active development, prefer external snapshot files to reduce merge friction.
Updating Snapshots

When you intentionally change a component's output, you need to update its snapshots. Both Vitest and Jest provide CLI flags for updating all snapshots or specific ones.

CLI commands
Bash
1# Update ALL snapshots (use with caution!)
2npx vitest --update
3# or
4npx jest --updateSnapshot
5# or shorthand:
6npx vitest -u
7npx jest -u
8
9# Update snapshots in a specific test file
10npx vitest -u src/components/UserProfile.test.tsx
11
12# Update only failed snapshots (not new ones)
13npx vitest -u --failed
14
15# When to update snapshots:
16# 1. You intentionally changed component output (new feature, redesign)
17# 2. You fixed a bug that affected rendered output
18# 3. You upgraded a dependency that changed generated class names
19# 4. You added new data to existing components
20
21# NEVER update snapshots without reviewing the diff first!
22# Bad practice — updates all snapshots without inspection:
23git add . && npx vitest -u
24
25# Good practice — review changes before updating:
26# 1. Run tests to see which snapshots fail
27# 2. Inspect the diff for each failing snapshot
28# 3. Confirm changes are intentional
29# 4. Update only the affected snapshot files
30# 5. Commit snapshot changes with the component changes

info

Create a CI check that fails if snapshot files are updated without corresponding source code changes. This prevents accidentally bundled snapshot updates. You can do this by checking that the snapshot diff matches the expected component changes in the PR.
Snapshot File Management

Snapshot files are stored in __snapshots__ directories alongside your test files. They are auto-generated and should be committed to version control, but they require careful management to avoid bloat and confusion.

snapshot-management.ts
TypeScript
1// Directory structure of snapshot files
2// src/
3// components/
4// UserProfile.tsx
5// UserProfile.test.tsx
6// __snapshots__/
7// UserProfile.test.tsx.snap ← Snapshot file
8// UserProfile.test.tsx.snap.new ← Partial update in progress
9
10// Snapshot file format (Vitest/Jest)
11// __snapshots__/UserProfile.test.tsx.snap
12// Jest Snapshot v1, https://goo.gl/fbAQLw
13
14exports[`UserProfile renders with admin role 1`] = `
15<div>
16 <div class="profile-card">
17 <h2>Alice Johnson</h2>
18 <span class="badge-admin">Admin</span>
19 </div>
20</div>
21`;
22
23exports[`UserProfile renders with user role 1`] = `
24<div>
25 <div class="profile-card">
26 <h2>Bob Smith</h2>
27 <span class="badge-user">User</span>
28 </div>
29</div>
30`;
31
32// Snapshot file best practices:
33// 1. Review snapshot diffs in PRs — don't auto-approve
34// 2. Delete orphaned snapshot files when removing tests
35// 3. Use npx vitest -u to clean stale snapshots (prompts for deletion)
36// 4. Keep snapshot files small — large snapshots are hard to review
37// 5. Consider .gitattributes for binary snapshot formats:
38// *.snap diff=snapshot linguist-generated

warning

Never delete snapshot files manually without understanding why they were created. A missing snapshot file will cause all snapshot tests to create new snapshots, which may silently pass with incorrect output. Run npx vitest -u to safely update and manage snapshot files.
When to Use Snapshots

Snapshots excel in specific scenarios but are overused in others. Understanding when snapshots add value versus when they create maintenance burden is key to effective testing.

ScenarioSnapshot?Alternative
Static component (header, footer)GoodPresence assertions
Dynamic component (data-driven)Use with careSpecific assertions
Error statesGoodSnapshot is concise
Large lists (10+ items)PoorVerify count + first/last
CSS class namesFragilePresence + style tests
API response shapeGoodTypeScript types
Configuration objectsExcellentManual assertions
Markdown/rich text outputGoodStructural matching

info

Use the "three strikes" rule: if a snapshot fails more than three times due to intentional changes, it might be too brittle. Consider replacing it with more specific assertions that focus on the parts of the output that matter most.
Snapshot Anti-Patterns

These anti-patterns are common sources of snapshot-related frustration. Avoiding them will keep your snapshot tests useful and maintainable.

snapshot-anti-patterns.test.tsx
TypeScript
1// Anti-pattern 1: Snapshot of entire page
2test("renders entire dashboard", () => {
3 const { container } = render(<Dashboard />);
4 // Bad — huge snapshot that changes on every minor edit
5 expect(container).toMatchSnapshot();
6});
7
8// Better — snapshot individual sections
9test("renders dashboard header", () => {
10 const { container } = render(<DashboardHeader />);
11 expect(container.firstChild).toMatchSnapshot();
12});
13
14// Anti-pattern 2: Snapshot with dynamic data
15test("renders with current date", () => {
16 const date = new Date().toISOString();
17 const { container } = render(<DateDisplay date={date} />);
18 // Bad — snapshot changes every day
19 expect(container.firstChild).toMatchSnapshot();
20});
21
22// Better — freeze the date
23test("renders with specific date", () => {
24 const date = "2024-01-15T00:00:00.000Z";
25 const { container } = render(<DateDisplay date={date} />);
26 expect(container.firstChild).toMatchSnapshot();
27});
28
29// Anti-pattern 3: Snapshot of every component prop combination
30test("renders Button variants", () => {
31 for (const variant of ["primary", "secondary", "ghost"]) {
32 for (const size of ["sm", "md", "lg"]) {
33 const { container } = render(
34 <Button variant={variant} size={size}>
35 Click
36 </Button>
37 );
38 // Bad — 9 snapshots for one component
39 expect(container.firstChild).toMatchSnapshot();
40 }
41 }
42});
43
44// Better — test specific behavior, not output
45test.each([
46 ["primary", "sm"],
47 ["secondary", "md"],
48 ["ghost", "lg"],
49])("Button variant %s size %s handles click", (variant, size) => {
50 const onClick = vi.fn();
51 render(<Button variant={variant} size={size} onClick={onClick}>Click</Button>);
52 fireEvent.click(screen.getByText("Click"));
53 expect(onClick).toHaveBeenCalledTimes(1);
54});
55
56// Anti-pattern 4: Auto-updating snapshots without review
57test("updates snapshot automatically", () => {
58 // Bad habit — run -u and commit without reviewing
59 expect(output).toMatchSnapshot();
60});

warning

Never run npx vitest -u as part of your test script or pre-commit hook. Automatic snapshot updates defeat the entire purpose of snapshot testing — you will never notice when output changes unexpectedly. Always update snapshots manually after reviewing the diff.
Alternatives to Snapshot Testing

For many scenarios, Testing Library's assertion methods provide more precise and maintainable alternatives to snapshots.

snapshot-alternatives.test.tsx
TypeScript
1// Alternative 1: Specific presence assertions
2import { render, screen } from "@testing-library/react";
3import userEvent from "@testing-library/user-event";
4
5// Instead of: snapshot of UserProfile
6expect(container.firstChild).toMatchSnapshot();
7
8// Do: assert specific elements exist
9test("renders user profile with name and email", () => {
10 render(<UserProfile user={{ name: "Alice", email: "alice@test.com" }} />);
11
12 expect(screen.getByText("Alice")).toBeInTheDocument();
13 expect(screen.getByText("alice@test.com")).toBeInTheDocument();
14 expect(screen.getByRole("img")).toHaveAttribute("src", "/avatar.jpg");
15});
16
17// Alternative 2: Custom matchers for specific behavior
18expect.extend({
19 toHaveStyle(received, style) {
20 const computed = window.getComputedStyle(received);
21 const pass = Object.entries(style).every(
22 ([key, val]) => computed[key] === val
23 );
24 return {
25 pass,
26 message: () => `Expected style ${JSON.stringify(style)}`,
27 };
28 },
29});
30
31// Alternative 3: toMatchObject for partial matching
32test("API response has expected shape", async () => {
33 const response = await fetchUser(1);
34 // Only check specific fields, ignore dynamic data
35 expect(response).toMatchObject({
36 id: expect.any(Number),
37 name: expect.any(String),
38 email: expect.stringMatching(/@/),
39 });
40});
41
42// Alternative 4: JSON serialization for config objects
43test("build config is correct", () => {
44 const config = getBuildConfig("production");
45 expect(JSON.stringify(config)).toMatch(/minify.*true/);
46 expect(config.sourceMaps).toBe(false);
47});

info

Testing Library's expect(element).toHaveTextContent(), toHaveAttribute(), and toHaveClass() are often better than snapshots. They test what the user sees (behavior) rather than the implementation details (markup structure).
Best Practices
  • Keep snapshots small — A snapshot should capture a single component or unit of output. Avoid snapshots of entire pages or complex layouts.
  • Use descriptive test names — Snapshot keys are derived from test names. Clear names make it easier to identify which snapshot failed: test("renders success badge with checkmark icon").
  • Review snapshot diffs in PRs — Snapshot changes should be as carefully reviewed as source code changes. If a snapshot diff is too large to review, the snapshot is too large.
  • Prefer inline snapshots for small outputs — Inline snapshots keep the expected output visible alongside the test. Use external snapshots only for larger outputs.
  • Freeze dynamic values — Dates, random IDs, and timestamps should be frozen (mocked) in snapshot tests to prevent flaky failures.
  • Commit snapshot files — Snapshot files are part of your test suite. Commit them alongside your source code and test files.

info

A snapshot test should answer the question: "Did the output of this code change?" If the answer is "yes, intentionally," you update the snapshot. If the answer is "yes, unintentionally," you found a regression. If you don't care about the exact output, don't use a snapshot.
$Blueprint — Engineering Documentation·Section ID: TEST-SNAP·Revision: 1.0