Testing — Snapshot Testing
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.
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.
| 1 | // Snapshot test — first run creates snapshot, subsequent runs compare |
| 2 | import { render } from "@testing-library/react"; |
| 3 | import UserProfile from "./UserProfile"; |
| 4 | |
| 5 | test("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
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.
| 1 | // Inline snapshot — stored in the test file itself |
| 2 | import { render } from "@testing-library/react"; |
| 3 | import Badge from "./Badge"; |
| 4 | |
| 5 | test("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 | |
| 18 | test("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
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.
| 1 | # Update ALL snapshots (use with caution!) |
| 2 | npx vitest --update |
| 3 | # or |
| 4 | npx jest --updateSnapshot |
| 5 | # or shorthand: |
| 6 | npx vitest -u |
| 7 | npx jest -u |
| 8 | |
| 9 | # Update snapshots in a specific test file |
| 10 | npx vitest -u src/components/UserProfile.test.tsx |
| 11 | |
| 12 | # Update only failed snapshots (not new ones) |
| 13 | npx 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: |
| 23 | git 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
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.
| 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 | |
| 14 | exports[`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 | |
| 23 | exports[`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
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.
| Scenario | Snapshot? | Alternative |
|---|---|---|
| Static component (header, footer) | Good | Presence assertions |
| Dynamic component (data-driven) | Use with care | Specific assertions |
| Error states | Good | Snapshot is concise |
| Large lists (10+ items) | Poor | Verify count + first/last |
| CSS class names | Fragile | Presence + style tests |
| API response shape | Good | TypeScript types |
| Configuration objects | Excellent | Manual assertions |
| Markdown/rich text output | Good | Structural matching |
info
These anti-patterns are common sources of snapshot-related frustration. Avoiding them will keep your snapshot tests useful and maintainable.
| 1 | // Anti-pattern 1: Snapshot of entire page |
| 2 | test("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 |
| 9 | test("renders dashboard header", () => { |
| 10 | const { container } = render(<DashboardHeader />); |
| 11 | expect(container.firstChild).toMatchSnapshot(); |
| 12 | }); |
| 13 | |
| 14 | // Anti-pattern 2: Snapshot with dynamic data |
| 15 | test("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 |
| 23 | test("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 |
| 30 | test("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 |
| 45 | test.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 |
| 57 | test("updates snapshot automatically", () => { |
| 58 | // Bad habit — run -u and commit without reviewing |
| 59 | expect(output).toMatchSnapshot(); |
| 60 | }); |
warning
For many scenarios, Testing Library's assertion methods provide more precise and maintainable alternatives to snapshots.
| 1 | // Alternative 1: Specific presence assertions |
| 2 | import { render, screen } from "@testing-library/react"; |
| 3 | import userEvent from "@testing-library/user-event"; |
| 4 | |
| 5 | // Instead of: snapshot of UserProfile |
| 6 | expect(container.firstChild).toMatchSnapshot(); |
| 7 | |
| 8 | // Do: assert specific elements exist |
| 9 | test("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 |
| 18 | expect.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 |
| 32 | test("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 |
| 43 | test("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
- 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