Testing

Snapshot Testing

A testing approach that captures a snapshot of a component's output and compares it to a stored reference snapshot to detect unintended changes.

What Is Snapshot Testing?

Snapshot testing is an automated testing technique that captures a serialized representation of a component’s output — such as rendered HTML, JSON API responses, or data structures — and stores it as a reference file. On subsequent test runs, the component’s current output is compared against the stored snapshot. If they match, the test passes. If they differ, the test fails, alerting the developer to review the change.

Snapshot testing was popularized by Jest in the React ecosystem, where it is commonly used to verify that UI components render the same output after code changes. However, snapshot testing applies far beyond React. It can capture any serializable output: REST API responses, GraphQL query results, configuration file generation, error messages, CLI output, and database query results.

The key advantage of snapshot testing is that it requires minimal effort to create. You do not need to write detailed assertions about every property of a complex output. Instead, you capture the entire output once, and the testing framework automatically detects any future deviation. This makes snapshot testing particularly useful for large, complex outputs where manually asserting on every field would be tedious and error-prone.

The key limitation is that snapshot tests detect change, not correctness. When you create the initial snapshot, you are implicitly asserting that the current output is correct. If the current output contains a bug, the snapshot captures the bug, and the test will pass until someone changes the output.

How It Works

In Jest, snapshot testing is straightforward. The toMatchSnapshot() matcher serializes the test output and stores it in a .snap file alongside the test:

// Button.test.js
import React from "react";
import renderer from "react-test-renderer";
import Button from "./Button";

test("renders primary button correctly", () => {
  const tree = renderer
    .create(<Button variant="primary" label="Submit" />)
    .toJSON();
  expect(tree).toMatchSnapshot();
});

test("renders disabled button correctly", () => {
  const tree = renderer
    .create(<Button variant="primary" label="Submit" disabled />)
    .toJSON();
  expect(tree).toMatchSnapshot();
});

The first time the test runs, Jest creates a snapshot file:

// __snapshots__/Button.test.js.snap
exports[`renders primary button correctly 1`] = `
<button
  className="btn btn-primary"
  onClick={[Function]}
>
  Submit
</button>
`;

exports[`renders disabled button correctly 1`] = `
<button
  className="btn btn-primary btn-disabled"
  disabled={true}
  onClick={[Function]}
>
  Submit
</button>
`;

If a developer later changes the Button component and the rendered output changes, the snapshot test fails:

● renders primary button correctly

  expect(received).toMatchSnapshot()

  Snapshot name: `renders primary button correctly 1`

  - Snapshot  - 1
  + Received  + 1

    <button
  -   className="btn btn-primary"
  +   className="button button--primary"
      onClick={[Function]}
    >

The developer reviews the diff and decides: is this change intentional? If yes, they update the snapshot with jest --updateSnapshot. If no, they fix the code.

Snapshot testing is not limited to UI components. You can snapshot any serializable data:

// api.test.js
test("user endpoint returns expected shape", async () => {
  const response = await getUser(123);
  expect(response).toMatchSnapshot();
});

In Python, libraries like snapshottest and syrupy provide equivalent functionality:

# test_api.py
def test_user_response_shape(snapshot):
    response = get_user(123)
    assert response == snapshot

def test_report_generation(snapshot):
    report = generate_monthly_report("2025-01")
    assert report == snapshot

Jest also offers inline snapshots with toMatchInlineSnapshot(), which stores the snapshot directly in the test file rather than in a separate .snap file:

test("formats currency correctly", () => {
  expect(formatCurrency(1234.5)).toMatchInlineSnapshot(`"$1,234.50"`);
});

Inline snapshots are useful for small, simple outputs where a separate file adds unnecessary indirection.

Why It Matters

Snapshot testing provides a low-effort safety net against unintended changes. For UI components, a small CSS class rename, a prop change, or an accidental deletion of an HTML attribute changes the rendered output. Without snapshot tests, these changes might go unnoticed during code review, especially in large diffs. Snapshot tests surface these changes explicitly, forcing a conscious review.

Snapshot tests are particularly valuable for catching regressions in complex outputs that are difficult to assert on manually. A React component that renders a data table with 15 columns, conditional formatting, and dynamic tooltips would require dozens of individual assertions to verify completely. A single snapshot captures the entire output and detects any change, regardless of where it occurs.

Snapshot testing also accelerates test writing for legacy codebases. When adding tests to existing, untested code, writing detailed assertions requires understanding every aspect of the current behavior. Snapshot testing lets you capture the current behavior immediately and then investigate any changes that arise as you modify the code.

Best Practices

  • Review snapshot changes in code review. Snapshot files should be committed to version control and reviewed in pull requests. A snapshot diff that changes the button text from “Submit” to “Sbmit” is a typo that the reviewer should catch. Treat snapshot diffs with the same scrutiny as code diffs.
  • Keep snapshots small and focused. Large snapshots (hundreds of lines) are hard to review and often change for irrelevant reasons. Test individual components with specific props rather than rendering entire pages.
  • Use inline snapshots for simple outputs. When the snapshot is a single line or a small object, use toMatchInlineSnapshot() to keep the expected value visible in the test file. Reserve external snapshot files for larger outputs.
  • Combine snapshots with targeted assertions. Snapshot tests catch changes but do not verify intent. Add specific assertions for critical properties alongside the snapshot: expect(result.status).toBe("active") plus expect(result).toMatchSnapshot().
  • Update snapshots intentionally. Never run --updateSnapshot blindly. Review every changed snapshot to confirm the change is intentional. Blindly updating snapshots defeats the entire purpose of the test.

Common Mistakes

  • Committing snapshots without reviewing them. Developers who run --updateSnapshot and commit without reviewing the diffs may be capturing bugs in the new snapshots. Always inspect snapshot changes before committing.
  • Creating overly large snapshots. Rendering an entire page with all its child components produces a snapshot that is thousands of lines long. When it changes, no one reviews it carefully because the diff is too large. Keep snapshots small by testing components in isolation.
  • Snapshot testing dynamic data. Timestamps, random IDs, and auto-generated values change on every test run, causing constant snapshot failures. Use deterministic test data or Jest’s property matchers to handle dynamic values: expect.any(String) or expect.any(Number).
  • Relying solely on snapshot tests. Snapshot tests detect change, not correctness. They should complement unit tests and integration tests, not replace them. Critical behavior should have explicit assertions that verify correctness, not just consistency.

Related Terms

Learn More

Related Articles

Free Newsletter

Stay ahead with AI dev tools

Weekly insights on AI code review, static analysis, and developer productivity. No spam, unsubscribe anytime.

Join developers getting weekly AI tool insights.