Skip to main content
Back to Blog
JavaScript
2026-06-15

Jest Snapshot Testing Guide (2026)

Complete Jest snapshot testing guide for 2026: toMatchSnapshot, inline snapshots, updating with -u, obsolete/stale snapshots, property matchers, and when to avoid them.

Jest Snapshot Testing Guide

Snapshot testing in Jest captures a serialized representation of a value the first time a test runs, stores it in a .snap file, and on every subsequent run compares the new output against that stored baseline — failing the test if they differ. It is a fast way to lock in the output of components, serializable objects, and error messages without hand-writing every assertion. The core API is expect(value).toMatchSnapshot(); you update intentional changes with jest --updateSnapshot (-u). Snapshots are powerful but easy to misuse — the discipline of reviewing every diff is what makes them valuable rather than noise.

This guide covers toMatchSnapshot, inline snapshots, updating and reviewing, obsolete and stale snapshots, property matchers for dynamic data, custom serializers, and — importantly — when not to use snapshots. For installable, agent-ready JavaScript testing skills, browse the QASkills directory.

How snapshot testing works

The first time a test with toMatchSnapshot() runs, Jest serializes the value and writes it to a __snapshots__/<testfile>.snap file next to your test. That file is committed to version control. On later runs Jest re-serializes the current value and compares it to the committed snapshot:

  • Match → test passes.
  • Mismatch → test fails and Jest prints a diff of what changed.

The whole value of the technique rests on one rule: commit snapshots and review their diffs in code review like any other code. A snapshot you blindly regenerate provides zero protection.

toMatchSnapshot: the basic API

test('renders a button', () => {
  const tree = render(<Button variant="primary">Save</Button>);
  expect(tree).toMatchSnapshot();
});

test('serializes config', () => {
  expect(buildConfig({ env: 'prod' })).toMatchSnapshot();
});

After the first run, a .snap file appears:

// __snapshots__/button.test.js.snap
exports[`renders a button 1`] = `
<button class="btn btn--primary">
  Save
</button>
`;

Each toMatchSnapshot() call in a test gets a numbered entry (1, 2, …), keyed by the test name. Renaming a test orphans its old snapshot (it becomes obsolete) and creates a new one — a frequent source of confusion.

Updating snapshots: the -u flag

When you change behavior on purpose, the snapshot is supposed to fail — that is the point. You then update the baseline:

# Update all snapshots that don't match
jest -u
# or the long form
jest --updateSnapshot

# Update only one file's snapshots
jest button.test.js -u

In watch mode (jest --watch), press u to update failing snapshots interactively, or i to step through them one at a time and decide each — which is far safer than blindly updating everything. Before running -u, read the diff. If the change is expected, update; if it is not, you just caught a regression. Running -u reflexively is the single most common way teams turn snapshots into worthless rubber stamps.

Inline snapshots

Inline snapshots store the serialized value in the test file itself rather than a separate .snap, using toMatchInlineSnapshot(). Jest writes the value into your source on first run (or on update):

test('parses a semver string', () => {
  expect(parse('2.4.1')).toMatchInlineSnapshot(`
    {
      "major": 2,
      "minor": 4,
      "patch": 1,
    }
  `);
});

Inline snapshots keep the expected value next to the assertion, which makes diffs visible directly in the test during review and avoids hunting through a .snap file. They work best for small values — a few lines. For large component trees, use a regular external snapshot so the test stays readable. (Inline snapshots require Prettier to be available for Jest to format the inserted value.)

Obsolete and stale snapshots

Two distinct problems plague snapshot suites:

Obsolete snapshots are entries in a .snap file whose test no longer exists (deleted or renamed). Jest detects them and reports them after a run:

› 2 snapshots obsolete.
  · renders a button: deleted variant 1
  · old config test 1

Remove obsolete snapshots by running an update with the full suite (not a filtered subset):

jest --ci=false -u        # prunes obsolete snapshots across the whole run

Important caveat: Jest only prunes obsolete snapshots for test files it actually executes. If you run a filtered subset, Jest cannot know whether the skipped tests still own their snapshots, so it leaves them. Always prune by running the full suite.

Stale snapshots are a worse, silent problem: a snapshot that passes but no longer reflects correct output because someone updated it carelessly. Jest cannot detect these — only human review of the -u diff prevents them. A snapshot full of obviously-wrong markup that still "passes" is the classic symptom of a team that updates without reading.

Property matchers for dynamic data

A snapshot of an object with timestamps, UUIDs, or random IDs will fail on every run because those values change. Use property matchers — pass an object of matchers as the first argument so the volatile fields are asserted by type while the rest is snapshotted literally:

test('creates a user record', () => {
  const user = createUser('ada@example.com');
  expect(user).toMatchSnapshot({
    id: expect.any(String),
    createdAt: expect.any(Date),
  });
});

The resulting snapshot stores Any<String> / Any<Date> placeholders for those fields and the exact values for everything else:

exports[`creates a user record 1`] = `
{
  "createdAt": Any<Date>,
  "email": "ada@example.com",
  "id": Any<String>,
}
`;

Without property matchers, your only options for dynamic data are to mock the source of nondeterminism (fake timers for Date.now(), a seeded UUID, jest.setSystemTime) — which is often the better fix because it pins the output exactly.

Custom serializers

Jest serializes most values sensibly, but you can register a custom serializer to control formatting — for example, to strip noise or normalize a domain object:

expect.addSnapshotSerializer({
  test: (val) => val && val.__type === 'Money',
  serialize: (val) => `Money(${val.amount} ${val.currency})`,
});

Component libraries commonly use a serializer (such as one that renders a component to a clean DOM-like tree) so snapshots are readable and resilient to irrelevant internal changes. Configure serializers globally via the snapshotSerializers option in your Jest config.

When NOT to use snapshot testing

Snapshots are a tool, not a default. Avoid or limit them when:

  • The output is large and frequently changing. A 500-line component snapshot that changes every sprint produces diffs nobody reads — they just press u. This is "snapshot rot." Prefer targeted assertions on the specific behavior you care about.
  • You would use a snapshot instead of an explicit assertion of intent. expect(total).toMatchSnapshot() hides what the total should be; expect(total).toBe(42) documents the intended value. Use explicit matchers for business logic and small, meaningful values.
  • The value is nondeterministic and you have not pinned it. Timestamps, ordering, and random IDs cause perpetual churn unless controlled with property matchers or mocking.
  • As a substitute for thinking about edge cases. Snapshotting a happy-path render does not test error states, empty states, or interactions — write real assertions for those.

A healthy rule of thumb: snapshots for output you would otherwise transcribe by hand and rarely changes (serialized config, stable markup, formatted error messages); explicit matchers for logic and values with meaning. For deeper testing-strategy guidance, see the QASkills blog.

A realistic example: stable error output

A good snapshot use case is locking in a formatted, human-facing string that should change only deliberately:

test('formats a validation error report', () => {
  const report = formatErrors([
    { field: 'email', code: 'required' },
    { field: 'age', code: 'min', meta: { min: 18 } },
  ]);
  expect(report).toMatchInlineSnapshot(`
    "Validation failed:
      - email is required
      - age must be at least 18"
  `);
});

If someone changes the wording, the diff is small, readable, and clearly meaningful in review — exactly the conditions under which snapshots earn their place.

CI considerations

In CI, Jest treats snapshots as immutable: any toMatchSnapshot call with no existing snapshot fails rather than silently writing a new one. This prevents a forgotten new snapshot from being auto-created and passing on the build server. Jest enables this automatically when it detects CI; you can force it with the --ci flag. The takeaway: always create and commit snapshots locally, never expect CI to generate them.

jest --ci    # fail (don't write) on missing snapshots

Common errors and troubleshooting

SymptomCauseFix
Snapshot fails on every runNondeterministic value (date/UUID)Property matchers (expect.any(...)) or mock the source
"N snapshots obsolete" warningTests renamed/deletedRun jest -u over the full suite to prune
New snapshot fails in CICI mode won't write missing snapshotsCreate and commit the snapshot locally first
Reviewers rubber-stamp huge diffsSnapshot too large / overusedReplace with targeted assertions; reserve snapshots for small stable output
Inline snapshot won't updatePrettier missingEnsure Prettier is installed so Jest can format inline values

Frequently Asked Questions

How do I update a Jest snapshot?

Run jest -u (or jest --updateSnapshot) to rewrite snapshots that no longer match, or filter to one file with jest path/to/file.test.js -u. In watch mode press u to update all failing snapshots, or i to review them one at a time. Always read the diff first — only update when the change is intentional, because reflexively updating turns snapshots into meaningless rubber stamps.

What is the difference between toMatchSnapshot and toMatchInlineSnapshot?

toMatchSnapshot stores the serialized value in a separate __snapshots__/*.snap file, which suits large outputs like component trees. toMatchInlineSnapshot writes the value directly into the test source, keeping the expected result next to the assertion so diffs are visible during review. Use inline snapshots for small values and external snapshots for large ones; inline snapshots require Prettier to format the inserted value.

How do I handle dynamic values like dates and IDs in snapshots?

Pass a property-matchers object as the first argument to toMatchSnapshot, mapping each volatile field to a type matcher such as expect.any(String) or expect.any(Date). Jest then stores a type placeholder for those fields and the literal value for everything else. Alternatively, pin the nondeterminism at the source with fake timers, jest.setSystemTime, or a seeded ID generator, which produces an exact, stable snapshot.

What are obsolete snapshots and how do I remove them?

Obsolete snapshots are entries in a .snap file whose corresponding test was renamed or deleted, and Jest reports them after a run. Remove them by running an update over the entire suite with jest -u, since Jest only prunes snapshots for test files it actually executes. Running a filtered subset leaves the others in place, so always prune with a full run.

When should I avoid snapshot testing?

Avoid snapshots for large, frequently changing output (the diffs become noise nobody reviews), for business-logic values where an explicit matcher like toBe(42) documents intent far better, and for nondeterministic data you have not pinned. Snapshots also should not replace real tests of error states, empty states, and interactions. Reserve them for stable output you would otherwise transcribe by hand, such as formatted messages or serialized config.

Why does my snapshot test fail in CI but pass locally?

In CI, Jest refuses to write a missing snapshot and fails instead, so a snapshot you created but never committed will fail on the build server. This guard prevents forgotten new snapshots from being silently auto-generated and passing. The fix is to run the test locally to create the snapshot, review it, and commit the .snap (or inline) value so CI has a baseline to compare against.

Jest Snapshot Testing Guide (2026) | QASkills.sh