Skip to content
C Codeloom
Testing

Snapshot Tests Explained

Learn how snapshot tests work, when they shine, and how to keep them maintainable instead of letting them rot into noise.

·3 min read · By Codeloom
Beginner 8 min read

What you'll learn

  • What a snapshot test is
  • When snapshots help and when they hurt
  • How to keep snapshots small and meaningful
  • Inline vs file snapshots
  • Reviewing snapshot diffs in PRs

Prerequisites

  • Comfortable with basic JS or TS testing

What and Why

A snapshot test captures a serialized version of some output, like a component tree or JSON response, and compares it on later runs. If anything changes, the test fails and shows a diff. They are fast to write and cover lots of surface area, but they only earn their keep when teams treat snapshots as code, not noise.

Mental Model

A snapshot test has two phases. First run records the output to disk. Later runs compare the new output against the stored copy. If they match, pass. If not, you either update the snapshot or fix the code.

First run        Later runs
---------        ----------
render -> out    render -> out
write to disk    compare to disk
PASS             match -> PASS
               differ -> FAIL (show diff)

Update flow: review diff, accept with --updateSnapshot
Snapshot lifecycle

Hands-on Example

A simple component test in Vitest with React.

import { render } from "@testing-library/react";
import { expect, test } from "vitest";
import { PriceTag } from "./PriceTag";

test("renders price tag", () => {
  const { container } = render(<PriceTag amount={42.5} currency="USD" />);
  expect(container).toMatchSnapshot();
});

The first run creates __snapshots__/PriceTag.test.jsx.snap with the rendered HTML. Later runs compare against it. When you intentionally change the format, run with vitest -u to update.

Inline snapshots keep the expected output next to the test.

test("formats currency", () => {
  expect(formatPrice(42.5, "USD")).toMatchInlineSnapshot(`"$42.50"`);
});

For API responses, snapshot a normalized subset rather than the whole payload.

test("user response shape", async () => {
  const res = await client.get("/users/1");
  const { id, name, role } = res.data;
  expect({ id, name, role }).toMatchInlineSnapshot(`
    {
      "id": 1,
      "name": "Ada",
      "role": "admin",
    }
  `);
});

For Python, syrupy does the same job with pytest.

def test_user_shape(client, snapshot):
    r = client.get("/users/1")
    assert {"id": r.json()["id"], "name": r.json()["name"]} == snapshot

Common Pitfalls

  • Treating a snapshot update like a chore. Engineers blindly run -u and ship regressions.
  • Snapshotting unstable values: timestamps, randomly ordered keys, generated ids. Strip or freeze them first.
  • Massive snapshots that no one reviews. If the file is hundreds of lines, no one will catch a real bug in the diff.
  • Using snapshots instead of explicit assertions for important fields. expect(user.email).toBe(...) is clearer than burying it in a 200-line blob.

Practical Tips

  • Always review snapshot diffs in code review. The diff is the test.
  • Keep each snapshot focused. One component or one response shape per test.
  • Normalize before snapshotting. Replace dates with <date> and ids with <id>.
  • Prefer inline snapshots for small values; they document intent in the test file.
  • Add serializers for custom types so output stays stable across runs.

Wrap-up

Snapshot tests are powerful when they remain small, focused, and read like documentation. They are a liability when they grow into giant unchecked blobs that everyone updates without thinking. Use them for shapes that are tedious to assert by hand, normalize unstable parts, and review diffs as carefully as you review code.