"A snapshot you never read is not a test. It's a diff committed to main."
What a Snapshot Test Actually Does
A snapshot test serializes some value (an object, a rendered DOM tree, a string), stores the serialized form, and on subsequent runs compares the live value against the stored one. If they match, pass. If they don't, fail — and Vitest shows you the diff.
That's it. There's no assertion in the conventional sense — you're not declaring what the output should be, just that whatever it was last time, it should be that again. The seductive part is how little code you write. The dangerous part is the same.
Two Shapes — Inline and File
Inline snapshots live in the test file itself, as a multi-line string argument to toMatchInlineSnapshot. They survive code review because the diff is visible right there in the PR. Good for short, stable output.
File snapshots live in a sibling __snapshots__/ directory, separate from the test file. They scale to larger captured output but tend to drift out of review attention — people approve PRs without reading them. Use sparingly.
The Update Flow (and Its Trap)
When a snapshot fails because the output legitimately changed (you refactored, you added a field), you update it: vitest -u or vitest --update. This regenerates the snapshot file from the current output, and you commit the diff.
The trap is muscle memory. After three or four cycles of "snapshot failed → press U → commit," the act of looking at what changed disappears. Now any unintended regression that touches the snapshot also gets U'd into oblivion. The test stopped catching anything months ago; nobody noticed because failures always end in U.
The fix is partly cultural (treat snapshot-only diffs with the same scrutiny as code diffs in review) and partly technical (don't snapshot things that change for reasons unrelated to the test's intent).
When Snapshots Earn Their Keep
- Error messages: short, stable, the exact text matters.
expect(formatError(input)).toMatchInlineSnapshot('"Expected number, got string"')is fine. - Normalized JSON contracts: small response shapes you want locked. Pair with a serializer that strips dates / ids so the snapshot is deterministic.
- CLI output: command-line tools that print specific lines benefit from snapshot tests on the rendered output.
When They Don't
- Large HTML trees from React components: every Tailwind class change breaks the snapshot. The signal-to-noise drops fast.
- Output containing dates, ids, hashes: non-deterministic by default. Add a serializer or assert on shape, not the snapshot.
- Anything you couldn't write a focused assertion for. A snapshot is not a substitute for thinking about what should be true.
expect(thing.foo).toBe('bar'), do that instead. Snapshots are a fallback when the output is genuinely unwieldy AND you genuinely need to lock it down. Both halves matter — unwieldy alone is not a reason.