What you'll learn
- Quick answer
- What's actually inside a .snap file
- The first run just writes the file; every run after that compares
- What a real failing snapshot diff looks like
- -u and --update: accepting a new snapshot as the new truth
- Why Jest and Vitest snapshots look nearly identical
- Why experienced teams are wary of snapshot tests
- Where snapshot testing genuinely earns its place
- FAQ
Quick Answer
A snapshot test runs your code once, serializes the result to text, and saves it to a .snap file; every future run re-serializes the current output and compares it character-for-character against that saved file. Jest and Vitest both build on the same serializer lineage, so a failing snapshot isn't testing against a spec of correct behavior — it's comparing today's output against whatever a person previously approved and committed.
What's actually inside a .snap file
A snapshot file is not a hash, not a diff, and not anything binary — it's plain text produced by running your code once and serializing the result. Here's a real one, generated by actually running Vitest against a small formatReceipt() function that turns an order object into a receipt:
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`formats a receipt for a two-item order 1`] = `
{
"currency": "INR",
"customer": "Aditi Sharma",
"id": "ORD-1001",
"items": [
{
"lineTotal": 499,
"name": "Priodemy Pro plan",
"qty": 1,
},
{
"lineTotal": 398,
"name": "Certification voucher",
"qty": 2,
},
],
"total": 897,
}
`;Each snapshot is keyed by the test name plus a number (in case a single test calls toMatchSnapshot() more than once). Something genuinely easy to misread in this real output: the keys are alphabetically sorted — currency, customer, id, items, total — even though the function actually builds the object in a completely different order (id, customer, items, total, currency). That's the serializer's default formatting behavior, not a reflection of your object's real key order, and it's worth knowing before you mistake a reordered diff for an actual bug.
The first run just writes the file; every run after that compares
The very first time a toMatchSnapshot() assertion runs and no matching entry exists in the .snap file, there's nothing to compare against, so the test runner writes the current output as the new baseline and passes. Real Vitest output from that first run:
Snapshots 1 written
Test Files 1 passed (1)
Tests 1 passed (1)Every subsequent run is different: the assertion serializes the current output the same way, then does a straightforward string comparison against whatever is already saved on disk. If they match exactly, the test passes silently, the same as any other assertion. If they don't, that's a snapshot mismatch, and that's where the actual judgment call in snapshot testing begins.
What a real failing snapshot diff looks like
To see an actual failure, adding one new field to formatReceipt()'s return value (a taxRate) and re-running against the committed snapshot produces this real, unedited output:
FAIL formatReceipt.test.js > formats a receipt for a two-item order
Error: Snapshot `formats a receipt for a two-item order 1` mismatched
- Expected
+ Received
@@ -12,7 +12,8 @@
"lineTotal": 398,
"name": "Certification voucher",
"qty": 2,
},
],
+ "taxRate": 0.18,
"total": 897,
}The minus-prefixed lines are what's saved in the committed .snap file ("Expected"); the plus-prefixed lines are what the code actually produced on this run ("Received"). Reading this diff tells you precisely what changed — a new taxRate field appeared. It tells you absolutely nothing about why, or whether that's correct. That judgment call — is this a deliberate feature addition, or did something upstream start leaking a field it shouldn't — sits entirely with whoever is reading the failure. The tool has no opinion.
-u and --update: accepting a new snapshot as the new truth
Once you've decided a change is correct and intentional, running the test command with -u (or --update, both spellings work in both Jest and Vitest) rewrites the .snap file to match the current output for every failing snapshot in that run. Real output from doing exactly that against the mismatch above:
Snapshots 1 updated
Test Files 1 passed (1)
Tests 1 passed (1)The test suite is green again. But notice the scope of what just happened: -u doesn't ask about each failing snapshot individually by default — it accepts all of them, project-wide, in one pass. If a shared formatting change causes forty snapshots to fail at once, and thirty-nine of those failures are the expected formatting change while one is a genuine regression riding along in the same commit, a blanket -u accepts all forty as correct without distinguishing between them. The suite passes. The regression ships. This is the single most common way snapshot testing fails in practice — not because the tool is broken, but because -u makes it just as easy to approve a bug as to approve an intentional change.
Why Jest and Vitest snapshots look nearly identical
Running the exact same formatReceipt() test under Jest instead of Vitest produces a snapshot file that's identical except for the header comment:
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testingEvery line of serialized data below that header matches the Vitest version byte for byte — same alphabetical key sorting, same trailing-comma formatting, same indentation. That's not a coincidence: inspecting vitest's own package.json shows it declares a dependency on @vitest/pretty-format, which is Vitest's own fork of Jest's pretty-format serializer package, kept deliberately compatible for exactly this reason. Practical takeaway: if you already know how to read a Jest snapshot or a Jest diff, you already know how to read a Vitest one — the underlying comparison mechanism is effectively the same code lineage wearing two different CLI banners.
Why experienced teams are wary of snapshot tests
An ordinary assertion like expect(receipt.total).toBe(897) encodes intent — it tells a future reader what's supposed to be true, and why the author wrote the test. A snapshot assertion encodes none of that; it only says "whatever this produced last time, produce it again." That absence of stated intent is the real source of the controversy, and it compounds badly at scale: a snapshot of a small, structured object like the receipt above is genuinely readable in a diff. A snapshot of an entire rendered component tree or a full page of HTML is not — a real change produces hundreds of lines of diff noise, most of it irrelevant, and reviewers under time pressure learn to skim past it and run -u reflexively rather than actually read what changed. Once that habit sets in, the test suite is still green, but it has stopped meaningfully catching anything.
Where snapshot testing genuinely earns its place
Snapshots work best on stable, structured output where the real question is simply "did this change at all" and the full shape is small enough that a reviewer will actually read the diff — a pure function's return value like formatReceipt() above, a generated config file, or the shape of a small API response. They work poorly on anything containing non-deterministic values — a raw Date.now() timestamp, a randomly generated ID, an absolute file path that differs between machines — unless those values are explicitly normalized or mocked before serialization, since otherwise the test fails on every run regardless of whether the actual logic changed. And they work poorly on anything so large that "read the diff before approving" stops being a realistic ask of a human reviewer — at that size, a handful of targeted, intent-carrying assertions on the specific properties that matter will catch real regressions more reliably than one enormous snapshot ever will.
