AI Loves Snapshots Because They Are Easy to Generate
When asked to test a component, the AI often generates: expect(component).toMatchSnapshot(). One line. The test: renders the component, serializes the output to a .snap file, and passes if the output matches the saved snapshot. Easy to generate. Easy to pass. But: the snapshot test breaks on every UI change (rearrange a div, the snapshot fails). The fix: run pnpm test -- -u to auto-update all snapshots. The review: a developer glances at 500 lines of snapshot diff and clicks approve. No one actually reviews: whether the change was intentional or a bug.
The result: snapshot tests become noise. They break on every change (high maintenance), get auto-updated without review (no value), and do not test behavior (they test serialized output structure, not what the user sees or does). A team with 200 snapshot tests: spends more time updating snapshots than writing meaningful tests. The tests: provide an illusion of coverage without testing anything meaningful. The snapshot: records what the output IS, not what the output SHOULD BE.
This article provides: rules that tell the AI when snapshots are appropriate (they have specific valid use cases), when assertions are better (most of the time), and how to generate meaningful tests instead of fragile snapshots. The goal: AI-generated tests that actually catch bugs, not tests that catch every CSS change and get auto-updated.
200 snapshot tests: break on every UI change, get auto-updated without review. They record what the output IS, not what it SHOULD BE. Zero bugs caught. High maintenance. An illusion of coverage that provides no value. One assertion test that verifies behavior: catches more bugs than 100 snapshots.
When Snapshot Tests Are Appropriate
Snapshot tests are valuable for: serialized output that should not change accidentally. Configuration files: expect(generateConfig(options)).toMatchSnapshot() — the config output should match exactly. API response format: expect(formatApiResponse(data)).toMatchSnapshot() — the response structure is a contract. Error messages: expect(formatErrorMessage(error)).toMatchSnapshot() — error messages are user-facing text that should be reviewed when changed. Inline snapshots: expect(result).toMatchInlineSnapshot() — the expected value is inline in the test file, visible during code review.
Snapshot rule: "Use snapshot tests only for: serialized output (config generation, API response format, error messages, CLI output). Use inline snapshots (toMatchInlineSnapshot) instead of file snapshots (.snap files) — inline snapshots are visible in code review and harder to auto-update without noticing. Never snapshot: React components (test behavior instead), HTML output (fragile, changes on every style update), or large objects (impossible to review the diff meaningfully)."
The key distinction: snapshot tests are appropriate when the exact output IS the specification (a config file must be exactly this format). They are inappropriate when the output is incidental (a component renders a div with a class — the class name is not the specification, the user-visible behavior is). The rule tells the AI: snapshot for exact-output contracts, assertions for behavior.
- Valid snapshots: config generation, API response format, error messages, CLI output
- Use inline snapshots (toMatchInlineSnapshot): visible in code review, harder to blindly update
- Never snapshot: React components, HTML output, large objects — fragile, meaningless diffs
- Snapshot when: exact output IS the specification (config must be exactly this format)
- Assertions when: the output is incidental (component renders correctly → test behavior, not HTML)
When Assertions Are Better: Testing Behavior
Assertions are better for: user-visible behavior ("clicking the button shows the modal" — not "the button renders with className='btn-primary'"), computed results ("the total is $49.99" — not "the entire receipt HTML matches"), state changes ("after login, the user name appears" — not "the component tree matches the logged-in snapshot"), and error handling ("invalid input shows an error message" — not "the component with invalid input matches the error snapshot").
Assertion rule: "Test behavior, not structure. Use: expect(screen.getByText('Alice')).toBeInTheDocument() (the user sees Alice). Not: expect(component).toMatchSnapshot() (the component HTML matches a stored file). Use: expect(result.total).toBe(49.99) (the calculation is correct). Not: expect(result).toMatchSnapshot() (the entire result object matches). Assertions tell: what SHOULD happen. Snapshots tell: what DOES happen (which may or may not be correct)."
The behavior test pattern: render the component, interact (click, type, submit), and assert the result (text appears, element is visible, function was called). render(<Cart items={testItems} />); expect(screen.getByText('$49.99')).toBeInTheDocument(); await userEvent.click(screen.getByRole('button', { name: 'Checkout' })); expect(screen.getByText('Order placed')).toBeInTheDocument(). This test: verifies what the user sees and does. A snapshot: verifies what the HTML looks like (the user does not see HTML).
- Test behavior: 'clicking button shows modal'. Not structure: 'button has className btn-primary'
- Assertions tell: what SHOULD happen. Snapshots tell: what DOES happen (may be wrong)
- Pattern: render → interact (click, type) → assert (text visible, element present)
- expect(screen.getByText('$49.99')): tests what user sees. toMatchSnapshot(): tests HTML structure
- Rule: 'Test behavior, not structure. Use screen queries and user assertions, not snapshots'
expect(screen.getByText('$49.99')): tests what the user sees (a price). toMatchSnapshot(): tests what the HTML looks like (div with class 'price'). The user does not see HTML. Behavior tests: survive CSS refactors, style changes, and component restructuring. Snapshots: break on all of them.
Snapshot Update Hygiene Rules
If your project uses snapshots: the AI needs hygiene rules. "Never auto-update all snapshots blindly (pnpm test -- -u updates everything — some changes may be bugs). Review each snapshot diff: is the change intentional? Does the new output match the specification? If unsure: do not update — investigate first. Commit snapshot updates in a separate commit from code changes (reviewers can see the snapshot diff clearly). Inline snapshots: preferred because the expected value is in the test file, visible during review."
The auto-update problem: a developer runs tests, 15 snapshots fail (a utility function changed). They run -u to update all 15. 14 were intentional updates. 1 was a bug (the utility now returns incorrect data for an edge case). The auto-update: hid the bug in a wall of snapshot diffs. The reviewer: did not notice. The bug: shipped to production. With assertion tests: the edge case assertion would have failed with a clear message ("expected 49.99, got 50.00"). The developer would have investigated. The bug would have been caught.
The AI rule to prevent this: "Prefer assertion tests over snapshots for all new tests. Use snapshots only for: exact-output contracts (configs, formats). For component tests: use React Testing Library with screen queries and user assertions. For logic tests: use expect(result).toBe(expected) or expect(result).toEqual(expected). Generate zero toMatchSnapshot() calls for React components. Zero."
- Never blindly -u: some snapshot changes may hide bugs in a wall of diffs
- Review each diff: is the change intentional? Does new output match the spec?
- Commit snapshots separately: reviewers see snapshot diffs clearly
- Inline snapshots: preferred (visible in test file, harder to blindly update)
- AI rule: 'Zero toMatchSnapshot() for React components. Use assertions for behavior'
The Testing Quality Rule
The combined AI rule for test quality: "Generate meaningful tests, not fragile snapshots. For components: render + interact + assert with React Testing Library (screen.getByText, userEvent.click, expect.toBeInTheDocument). For logic: expect(fn(input)).toBe(expected) with specific values. For exact-output contracts (configs, API formats): toMatchInlineSnapshot(). Never: toMatchSnapshot() for React components, HTML output, or large objects. Test what the user sees and does, not what the HTML looks like."
This one rule: eliminates the most common AI testing anti-pattern (snapshot everything). The AI generates: behavior tests that catch real bugs. The tests: do not break on CSS changes, do not require blind updates, and clearly communicate what they verify. A failing behavior test: "expected text 'Alice' but found nothing" — the developer knows exactly what broke. A failing snapshot test: 500 lines of HTML diff — the developer knows nothing about what broke without reading every line.
The ROI: one rule about test quality produces: tests that remain valuable for the lifetime of the project. Snapshot tests: become noise within months (every change triggers updates). Assertion tests: remain meaningful as long as the behavior they test exists. The rule investment: one sentence in CLAUDE.md. The payoff: every AI-generated test is meaningful, maintainable, and catches real bugs.
'Zero toMatchSnapshot() for components. Assert behavior with screen queries and user assertions.' One sentence in CLAUDE.md. The payoff: every AI-generated test catches real bugs, communicates what it verifies, and remains valuable for the lifetime of the project. Snapshots become noise within months.
Snapshot vs Assertion Summary
Summary of snapshot vs assertion test AI rules.
- Snapshots valid for: config output, API format, error messages — exact-output contracts
- Snapshots invalid for: React components, HTML, large objects — fragile, meaningless diffs
- Assertions valid for: behavior, calculations, state changes, user interactions — what SHOULD happen
- Snapshot problem: break on every change, auto-updated without review, hide bugs in diff walls
- Assertion advantage: clear failure message ('expected Alice, found nothing'), test behavior not structure
- AI default: generates snapshots (easy). Rule: 'Zero snapshots for components. Assert behavior'
- Inline snapshots > file snapshots: visible in code review, harder to blindly update
- One rule eliminates: the most common AI testing anti-pattern. Every generated test: meaningful