Snapshot Testing vs Approval Testing: A Practical Comparison

Snapshot Testing vs Approval Testing: A Practical Comparison

"Snapshot testing" and "approval testing" describe the same fundamental idea: capture output, store it, and detect future deviations. But the two approaches differ in important ways—their review process, tooling philosophy, and best-fit use cases. Conflating them leads to poor test design choices.

This comparison covers how they differ, where each excels, and how to pick the right one.


The Shared Foundation

Both approaches follow the same three-step cycle:

  1. Capture: run code, capture output
  2. Store: save output as the reference
  3. Compare: on future runs, compare current output to stored reference

Both detect unintended changes. Both require human judgment when output changes—is this a regression or an intentional update?

The difference is in how that judgment is exercised and when it happens.


Snapshot Testing

Snapshot testing is most commonly associated with Jest's toMatchSnapshot(), though other tools support it.

How it works

// Jest snapshot test
it("renders user card correctly", () => {
  const component = render(<UserCard name="Alice" plan="Pro" />);
  expect(component.toJSON()).toMatchSnapshot();
});

First run: Jest creates a .snap file automatically.

// UserCard.test.js.snap
exports[`renders user card correctly 1`] = `
<div className="user-card">
  <h2>Alice</h2>
  <span className="plan">Pro</span>
</div>
`;

Subsequent runs: Jest compares the rendered output to the .snap file. Any difference fails the test.

Updating snapshots

When you change the component intentionally:

jest --updateSnapshot
# or
jest -u

This updates all failing snapshots in one command. No per-test review process.

Characteristics

  • Automatic capture: no explicit approval step—the first run creates the snapshot
  • Bulk update: --updateSnapshot updates everything at once
  • Inline snapshots: can be embedded in the test file itself
  • Framework-coupled: tightly integrated with the test runner (Jest, Vitest)
  • Inline review: the diff appears in the test output, not a separate diff tool

Approval Testing

Approval testing follows a more deliberate process. The ApprovalTests library (available in Java, .NET, JavaScript, Python, Ruby) is the most common implementation.

How it works

import { verify } from "approvals/lib/Providers/Jest/JestApprovals";

it("generates invoice", () => {
  const invoice = generateInvoice(order);
  verify(JSON.stringify(invoice, null, 2));
});

First run: test fails. The library writes a .received.txt file and opens a diff tool for review.

tests/__approved__/
  InvoiceTest.generates_invoice.approved.txt   ← doesn't exist yet
  InvoiceTest.generates_invoice.received.txt   ← current output

You review the diff in your preferred tool (Beyond Compare, Kaleidoscope, VS Code). If correct, you copy received to approved (or click "accept" in the diff tool).

Subsequent runs: received is compared to approved. Mismatch = failure + diff.

Characteristics

  • Explicit approval: each test output must be individually approved
  • Diff-tool integration: review happens in a dedicated diff tool
  • Per-test control: you approve each test's output independently
  • Framework-agnostic: works with any test runner
  • Separate files: .received.txt and .approved.txt are distinct

Side-by-Side Comparison

Dimension Snapshot Testing Approval Testing
First capture Automatic on first run Manual approval required
Update workflow --updateSnapshot (bulk) Per-test approval via diff tool
Review granularity All-at-once or per-file Per test
Diff tool Terminal/IDE inline External diff tool (configurable)
File format .snap (Jest-specific) .approved.txt (plain text)
Framework coupling Tight (Jest, Vitest) Loose (works with any runner)
Output types JSON, component trees Any string, text, JSON
Best for UI components, React trees Complex objects, reports, text
Human oversight Low (easy to --updateSnapshot) High (explicit approval step)

The Key Philosophical Difference

Snapshot testing optimizes for speed. The workflow is: run, auto-capture, update when needed. The mental model is "approve unless proven wrong."

Approval testing optimizes for deliberateness. The workflow is: run, review, explicitly accept. The mental model is "reject unless proven right."

This matters when output changes frequently:

  • With snapshots, developers often --updateSnapshot without carefully reviewing—especially on large refactors where dozens of snapshots fail simultaneously
  • With approval testing, every change requires a deliberate review action

When to Use Snapshot Testing

UI component testing. Snapshot testing is purpose-built for this. React component trees, rendered HTML—snapshot testing captures them naturally.

it("renders empty state", () => {
  const { container } = render(<UserList users={[]} />);
  expect(container).toMatchSnapshot();
});

Output that changes frequently but predictably. When UI changes are common and review is handled in code review (the diff in the PR), snapshot --updateSnapshot is efficient.

Tightly integrated toolchains. Jest projects with React or Vue benefit from zero-configuration snapshot support.


When to Use Approval Testing

Complex non-UI output. Generated reports, formatted text, API response structures, serialized domain objects—approval testing handles these better.

verify(generateMonthlyReport(data));  // complex multi-page text

High-stakes correctness requirements. When updates require explicit human sign-off (not just --updateSnapshot), approval testing enforces the review step.

Legacy code characterization. Approval testing is a natural fit for capturing existing behavior before refactoring.

Mixed-language codebases. ApprovalTests has consistent behavior across Java, .NET, Python, JavaScript—snapshot tools are framework-specific.


The "Snapshot Trap"

The most common complaint about snapshot testing:

Snapshot summary
  › 47 snapshots failed.

Run with --updateSnapshot to update them.

When a refactor or dependency update breaks 47 snapshots simultaneously, the common response is to --updateSnapshot all of them without carefully reviewing each one. Regressions hide in the noise.

Approval testing avoids this by requiring per-test approval. But it also slows you down when changes are legitimate and widespread.

Mitigation for snapshot testing:

  • Keep snapshots small and focused—one component, one state
  • Use --ci mode in CI to fail on any snapshot mismatch (never auto-update)
  • Review snapshot diffs carefully in code review

Combining Both

The approaches aren't mutually exclusive. Many teams use:

  • Snapshot testing for React components (fast, tightly integrated)
  • Approval testing for backend output: API responses, generated files, reports
// Component: snapshot test
it("renders invoice", () => {
  expect(render(<InvoicePage data={invoiceData} />).toJSON()).toMatchSnapshot();
});

// Generator: approval test
it("generates invoice JSON", () => {
  verify(JSON.stringify(generateInvoice(orderData), null, 2));
});

Beyond Output Comparison

Both snapshot and approval tests verify internal output. They don't verify the full user experience—the HTTP layer, the browser, the real rendering stack.

HelpMeTest complements both with end-to-end tests written in plain English. When your snapshots confirm the component renders correctly in isolation, and your approval tests confirm the API returns the right JSON, HelpMeTest confirms the full flow works for actual users.


Summary

Choose When
Snapshot testing React/Vue components, Jest-based projects, frequently changing UI
Approval testing Complex text output, high-stakes reviews, legacy characterization, multi-language teams
Both Large projects with distinct UI and backend output testing needs

The difference isn't which output to capture—it's how deliberately you want to approve it. Snapshot testing is fast and easy to skip. Approval testing is deliberate and harder to bypass.

Read more

Start now free