Snapshot Testing Patterns

· 6 min read · Updated March 16, 2026 · intermediate
testing jest vitest snapshots tdd

Introduction

Snapshot tests capture the output of your code at a point in time and compare future runs against that baseline. When the output changes, the test fails — alerting you to unexpected changes. This makes snapshots particularly useful for guarding against unintended changes in data structures, API responses, UI output, or any serializable value.

In this tutorial, you’ll learn how to write, update, and maintain snapshot tests effectively using Jest and Vitest. We’ll cover practical patterns that help you get the most out of snapshots while avoiding common pitfalls.

How Snapshots Work

When you first run a snapshot test, Jest or Vitest serializes the value being tested and saves it to a separate file. On subsequent runs, the serializer output is compared against the saved snapshot:

// component.js
export function formatUser(user) {
  return JSON.stringify(user, null, 2);
}

// component.test.js
import { formatUser } from './component';

test('formats user correctly', () => {
  const user = { id: 1, name: 'Alice', email: 'alice@example.com' };
  expect(formatUser(user)).toMatchSnapshot();
});

The first time you run this test, it passes and creates a snapshot file:

// __snapshots__/component.test.js.snap
exports[`formats user correctly 1`] = `
"{
  \\"id\\": 1,
  \\"name\\": \\"Alice\\",
  \\"email\\": \\"alice@example.com\\"
}"
`;

On the next run, if formatUser returns the same output, the test passes. If the output changes, the test fails and shows you exactly what changed.

Inline Snapshots

Instead of storing snapshots in separate files, you can embed them directly in your test file using inline snapshots:

import { formatUser } from './component';

test('formats user with inline snapshot', () => {
  const user = { id: 1, name: 'Alice', email: 'alice@example.com' };
  expect(formatUser(user)).toMatchInlineSnapshot();
});

When you run this test with the --updateSnapshot flag, the snapshot value gets written directly into your test file:

test('formats user with inline snapshot', () => {
  const user = { id: 1, name: 'Alice', email: 'alice@example.com' };
  expect(formatUser(user)).toMatchInlineSnapshot(`
    "{
      \\"id\\": 1,
      \\"name\\": \\"Alice\\",
      \\"email\\": \\"alice@example.com\\"
    }"
  `);
});

Inline snapshots keep snapshots close to their tests, which works well for small, stable outputs. Use external snapshot files for large outputs or when multiple tests share the same data.

Snapshot Matchers

Beyond toMatchSnapshot(), Jest and Vitest provide additional snapshot matchers:

toHaveSnapshot()

The newer toHaveSnapshot() API (available in Jest 29+) provides better type safety and clearer error messages:

import { formatUser } from './component';

test('user format has snapshot', () => {
  const user = { id: 1, name: 'Alice' };
  expect(formatUser(user)).toHaveSnapshot();
});

toThrowErrorMatchingSnapshot()

Capture error messages as snapshots to track changes in error handling:

import { validateEmail } from './validator';

test('throws on invalid email', () => {
  expect(() => validateEmail('invalid')).toThrowErrorMatchingSnapshot();
});

Property Matchers

For objects with dynamic values like timestamps or IDs, use property matchers to snapshot only the stable parts:

test('user with dynamic data', () => {
  const user = {
    id: expect.any(Number),
    name: 'Alice',
    createdAt: expect.any(Date),
    token: expect.stringMatching(/^[a-z0-9]+$/)
  };
  
  expect(formatUser(user)).toMatchSnapshot({
    id: expect.any(Number),
    createdAt: expect.any(Date),
    token: expect.stringMatching(/^[a-z0-9]+$/)
  });
});

Snapshots with Async Data

Snapshots work with promises, but you need to handle the async nature properly:

import { fetchUser } from './api';

test('fetches user snapshot', async () => {
  const user = await fetchUser(1);
  
  // Snapshot stable fields only
  expect(user).toMatchSnapshot({
    id: expect.any(Number),
    timestamp: expect.any(String),
    avatarUrl: expect.stringMatching(/^https?:\/\//)
  });
});

For API responses, consider snapshotting specific transformations rather than raw responses to avoid brittle tests:

test('user display data', async () => {
  const user = await fetchUser(1);
  
  // Transform to the shape you actually care about
  const displayData = {
    name: user.name,
    initials: user.name.split(' ').map(n => n[0]).join(''),
    isVerified: user.verifiedAt !== null
  };
  
  expect(displayData).toMatchSnapshot();
});

Snapshotting DOM Output

A common use case for snapshots is testing component render output. Even without a full testing library, you can snapshot rendered HTML:

// renderer.js
export function renderCard(title, content) {
  return `
    <div class="card">
      <h2 class="card-title">${title}</h2>
      <div class="card-content">${content}</div>
    </div>
  `;
}

// renderer.test.js
import { renderCard } from './renderer';

test('renders card correctly', () => {
  const html = renderCard('Welcome', 'Hello, world!');
  expect(html).toMatchSnapshot();
});

When using React Testing Library, you can snapshot the container’s HTML:

import { render } from '@testing-library/react';
import { Card } from './Card';

test('Card renders correctly', () => {
  const { container } = render(<Card title="Hello">World</Card>);
  expect(container.innerHTML).toMatchSnapshot();
});

Managing Snapshot Files

Updating Snapshots

When legitimate changes occur, update snapshots with the CLI flag:

# Update all snapshots
npm test -- --updateSnapshot

# Or the shorter alias
npm test -- -u

For Vitest:

vitest update

Interactive Update Mode

Jest offers an interactive mode that lets you review each changed snapshot:

npm test -- --watchAll --updateSnapshot

This shows you each failing snapshot and lets you approve or reject changes individually.

Organizing Snapshots

For larger projects, organize snapshots by feature or module:

__snapshots__
  ├── auth.test.js.snap
  ├── api.test.js.snap
  └── components.test.js.snap

Use descriptive test names so snapshot names are meaningful:

test('formats user with email', () => { /* ... */ });
test('formats user without email', () => { /* ... */ });
// Creates snapshots: "formats user with email 1" and "formats user without email 1"

When to Use Snapshots

Snapshots excel in these scenarios:

  • API response validation — Catch unexpected changes in external API contracts
  • Configuration objects — Monitor changes to complex configuration structures
  • Error messages — Track intentional changes to user-facing messages
  • Generated output — Validate code generation, serialization, or transformation results
  • Large data structures — Avoid manually asserting on many properties

Avoid snapshots for:

  • Frequently changing data — Daily timestamps, counters, or session IDs
  • Complex UI — Use component tests with assertions instead
  • Behavioral testing — Snapshots don’t express intent; use regular assertions for logic
  • Large binary data — Snapshot files become unwieldy

Best Practices

1. Review Snapshots Before Committing

Always review snapshot diffs before committing. Ask yourself: “Did I intentionally change this?“

2. Keep Snapshots Small

Snapshot large outputs only after trimming to the essential parts:

// Instead of snapshotting the full response
test('user response', () => {
  const user = await fetchUser(1);
  
  // Snapshot only what matters
  expect(user).toMatchSnapshot({
    id: expect.any(Number),
    name: user.name,
    // Skip verbose nested objects
    preferences: expect.objectContaining({
      theme: expect.any(String)
    })
  });
});

3. Use Property Matchers for Dynamic Values

Prevent snapshot churn from expected variations:

test('generates report', () => {
  const report = generateReport({
    generatedAt: new Date().toISOString(),
    requestId: uuid(),
    data: [1, 2, 3]
  });
  
  expect(report).toMatchSnapshot({
    generatedAt: expect.any(String),
    requestId: expect.any(String)
  });
});

4. Version Control Your Snapshots

Commit snapshot files alongside code changes. They’re integral to your test suite.

5. Add Custom Serializers for Complex Objects

For objects that don’t serialize well by default, add custom serializers:

// jest.config.js
module.exports = {
  snapshotSerializers: ['my-serializer'],
};

// my-serializer.js
testUtils.addEqualityTesters([
  (a, b) => {
    if (a instanceof CustomClass && b instanceof CustomClass) {
      return a.id === b.id;
    }
    return undefined; // Use default comparison
  }
]);

Summary

Snapshot testing is a powerful tool when used appropriately. Remember these key points:

  • Use toMatchSnapshot() for initial snapshots and toMatchInlineSnapshot() for embedded snapshots
  • Leverage property matchers (expect.any(), expect.stringMatching()) for dynamic values
  • Update snapshots intentionally using CLI flags after reviewing changes
  • Avoid snapshotting frequently changing data or complex UI directly
  • Organize snapshots logically and keep them focused on stable outputs

In the previous tutorial in this series, you learned about code coverage and CI integration. Snapshot testing complements these practices by providing a safety net against unintended changes.

In the next tutorial in this series, you’ll explore TypeScript-specific testing patterns and type-aware testing strategies.

See Also

  • Testing with Jest — Get started with Jest, the most popular JavaScript testing framework
  • Testing with Vitest — Learn about Vitest, a fast and modern alternative to Jest
  • Mocking — Master mocking techniques for isolating units under test
  • JSON.parse() — Understand how JSON parsing works under the hood