When to Use Snapshot Tests (and When to Avoid Them)

When to Use Snapshot Tests (and When to Avoid Them)

Snapshot tests are one of those testing tools that developers either overuse or underuse. They get added to every component by default, then blindly updated whenever tests fail — or they get avoided entirely by teams burned by maintenance overhead.

The answer is in the middle: snapshot tests are genuinely useful for specific scenarios and genuinely harmful for others. This guide gives you a clear decision framework.

The Core Question: What Are You Testing?

Before adding a snapshot test, ask: what change in this code would be a bug?

If the answer is "the structure or content of the output changed unexpectedly," a snapshot test fits. If the answer is "the behavior changed" or "the visual appearance changed," you need a different kind of test.

Snapshots detect unintended changes to output. They don't verify correct behavior or correct appearance.

Good Uses for Snapshot Tests

1. Stable, Mature Components with Complex Output

When a component has been in use for months and its output structure is stable and meaningful, a snapshot provides a safety net against accidental regressions:

// A date formatting component — output is complex and important
test('formats timestamp for multiple locales', () => {
  const dates = ['2025-01-15', '2025-12-31', '2025-07-04'].map(d => 
    formatDate(new Date(d), { locale: 'en-US', format: 'long' })
  );
  expect(dates).toMatchInlineSnapshot(`
    [
      "January 15, 2025",
      "December 31, 2025",
      "July 4, 2025",
    ]
  `);
});

The snapshot here captures real business logic. If the formatting changes, you want to know.

Indicators a component is a good snapshot candidate:

  • Output is deterministic (no timestamps, random IDs)
  • The component has been stable for weeks or months
  • The output structure is non-trivial (not just a single element)
  • Changes to the output are usually intentional and infrequent

2. Data Transformation Functions

Pure functions that transform data are excellent snapshot targets:

// transforms complex API response into UI model
test('transforms order response correctly', () => {
  const apiResponse = {
    order_id: 'ord_123',
    line_items: [...],
    shipping_address: {...},
    created_at: '2025-01-15T10:00:00Z',  // fixed date — deterministic
  };

  const uiOrder = transformOrderResponse(apiResponse);
  expect(uiOrder).toMatchSnapshot({
    id: expect.any(String),  // match any string for id
  });
});

Data transformers often have complex output with many fields. A snapshot captures the full output in one assertion rather than dozens of individual expect calls.

3. Error Messages and Formatted Output

When the exact wording of error messages or formatted strings matters:

test('generates informative validation error', () => {
  const error = validatePassword('abc');
  expect(error).toMatchInlineSnapshot(
    `"Password must be at least 8 characters and contain at least one number"`
  );
});

test('formats invoice line items', () => {
  const lines = formatInvoiceLines([
    { description: 'Widget A', quantity: 2, price: 9.99 },
    { description: 'Widget B', quantity: 1, price: 24.99 },
  ]);
  expect(lines).toMatchSnapshot();
});

Error messages are easy to accidentally change during refactoring. Snapshots catch this.

4. Serialized API Schemas

When you want to document and protect the shape of an API response:

test('user endpoint returns expected schema', async () => {
  const response = await request(app).get('/api/users/1');
  expect(response.body).toMatchSnapshot({
    id: expect.any(Number),
    createdAt: expect.any(String),
    updatedAt: expect.any(String),
  });
});

This documents the contract your API provides. When the schema changes, the snapshot fails — prompting a deliberate review of whether the change is backward-compatible.

Bad Uses for Snapshot Tests

1. Testing UI Behavior

Behavior should be tested with explicit assertions, not snapshots:

// Bad — snapshot doesn't tell you if the button actually works
test('submit button works', () => {
  const { container } = render(<CheckoutForm onSubmit={mockSubmit} />);
  expect(container).toMatchSnapshot(); // what does this even test?
});

// Good — explicit behavior assertions
test('submit button calls onSubmit with form data', async () => {
  const onSubmit = jest.fn();
  const { getByRole, getByLabelText } = render(<CheckoutForm onSubmit={onSubmit} />);
  
  await userEvent.type(getByLabelText('Email'), 'test@example.com');
  await userEvent.click(getByRole('button', { name: 'Submit' }));
  
  expect(onSubmit).toHaveBeenCalledWith({ email: 'test@example.com' });
});

A snapshot of a form doesn't tell you whether it submits correctly. An explicit assertion does.

2. Rapidly Changing Components

During active development, a component's structure changes frequently. Snapshot tests in this phase become noise — they fail constantly, get updated without review, and add no value:

// Bad — added snapshot to a component being actively developed
// This snapshot will be updated 10 times this week
test('renders UserProfile', () => {
  const { container } = render(<UserProfile user={user} />);
  expect(container).toMatchSnapshot(); // updated without thought every time
});

Add snapshot tests after the component's structure has stabilized.

3. Components with Third-Party Dependencies

If your component renders third-party UI library components, their snapshot output is large, opaque, and changes whenever you update the dependency:

// Bad — includes MUI DataGrid internals
test('renders data table', () => {
  const { container } = render(
    <DataTable rows={rows} columns={columns} />
  );
  expect(container).toMatchSnapshot();
  // This snapshot is 800 lines long and includes MUI internals
  // A MUI minor version bump will fail this test
});

Instead, test your component's behavior or test only the parts you own.

4. Large, Complex Component Trees

Full-page or large-component snapshots are:

  • Hard to review meaningfully in PRs
  • Brittle — any change anywhere in the tree fails the snapshot
  • Slow to update when changes are intentional
// Bad — snapshots entire page
test('dashboard renders', () => {
  const { container } = render(<Dashboard user={user} data={data} />);
  expect(container).toMatchSnapshot(); // 3,000 line snapshot
});

// Better — test specific, meaningful parts
test('dashboard shows user name in header', () => {
  const { getByTestId } = render(<Dashboard user={user} data={data} />);
  expect(getByTestId('user-name')).toHaveTextContent('Jane Doe');
});

5. Tests Where You Can't Read the Diff

If a snapshot failure produces a diff that requires context you don't have to evaluate, it's not providing value:

- <div class="generated-class-abc123">
+ <div class="generated-class-xyz789">

This diff means nothing without understanding what generated those class names. The snapshot is testing CSS-in-JS implementation details, not meaningful behavior.

The Decision Framework

Use this checklist before adding a snapshot test:

Add the snapshot if:

  • The output is deterministic (no timestamps, random values)
  • The component/function has stable output that shouldn't change often
  • Changes to the output would genuinely be important to catch
  • The snapshot is small enough to review meaningfully when it fails
  • You own the full output (no third-party component internals)

Don't add the snapshot if:

  • You're testing behavior (use explicit assertions instead)
  • The component is still being actively developed
  • The output includes third-party component internals
  • The output is larger than ~50 lines (likely too much to review)
  • You're testing visual appearance (use visual regression testing)
  • Dynamic values are part of the output (unless using asymmetric matchers)

Alternatives When Snapshots Don't Fit

For behavior: Use @testing-library/react with explicit assertions (getByRole, getByText, fireEvent, userEvent).

For visual correctness: Use visual regression testing (Playwright's toHaveScreenshot(), Percy, or Chromatic).

For large, complex output: Use explicit assertions about the specific parts that matter, not the entire output.

For frequently-changing components: Skip snapshots entirely; add them when the component stabilizes.

Recovering from Snapshot Overuse

If your project has hundreds of snapshots that are reflexively updated whenever they fail:

  1. Audit snapshot size — any snapshot over 50 lines is probably too large
  2. Check update frequency — snapshots updated more than once a month are probably not providing value
  3. Delete large snapshots — remove them and add targeted assertions instead
  4. Add a code review policy — require explicit review of snapshot diffs before jest -u
  5. Use inline snapshots — they're harder to update mindlessly and stay close to the test

Summary

Snapshot tests are a precision tool, not a general-purpose testing strategy. Use them where they shine:

Scenario Use Snapshot?
Stable, deterministic component output Yes
Data transformation functions Yes
Error message formatting Yes
API response schema Yes (with matchers)
UI behavior testing No — use explicit assertions
Rapidly changing components No — wait for stability
Third-party component output No — too brittle
Large page renders No — too hard to review
Visual appearance No — use visual regression

The discipline of asking "what am I actually protecting with this snapshot?" before adding one is what separates teams that use snapshots effectively from teams that maintain a snapshot debt they can't pay down.

Read more