What Is Snapshot Testing? β Definition and How It Works
Learn how snapshot testing catches unintended output changes, and why careless snapshot updates destroy its value.
Snapshot testing records a function's output once, commits it, and fails the test whenever the output changes. It answers "did anything change?" without anyone specifying what the output should be.
1. How It Works
def test_extraction_snapshot(snapshot):
records = parse_listing(read_fixture("example.com/listing.html"))
snapshot.assert_match(json.dumps(records, indent=2, sort_keys=True), "listing.json")
First run writes the snapshot. Later runs compare against it. A difference is a failure, showing a diff.
The appeal is cost: you get broad coverage of a complex output without writing assertions for every field.
2. Where It Fits Extraction Work
Extraction produces large structured outputs, which is exactly the shape snapshot testing handles well:
- A parser's full output for a fixture page.
- A normalisation function's result across a set of inputs.
- The complete field set, catching a field that silently disappeared.
- Ordering, which is easy to change accidentally.
A hand-written assertion set for a forty-field record is tedious and usually incomplete; a snapshot covers all of it.
3. The Failure Mode That Destroys Its Value
Snapshot testing has one characteristic pathology: updating snapshots without reading the diff.
The workflow that kills it is: test fails β run the update command β tests pass β commit. That converts a test suite into a recording of whatever the code currently does, which asserts nothing.
The discipline that keeps it useful:
- Read every diff. Every line.
- Update only when the change is intended, and say why in the commit.
- Never bulk-update across many snapshots at once.
- Treat a surprising diff as a finding, not an inconvenience.
Without that discipline the tests are worse than nothing, because they provide false assurance.
4. Keep Snapshots Deterministic
A snapshot containing anything that varies between runs fails constantly and gets ignored β or worse, gets updated reflexively.
Sources of nondeterminism to remove before snapshotting:
| Source | Fix |
|---|---|
| Timestamps | Inject a fixed clock, or exclude the field |
| Generated ids | Use deterministic ids in tests |
| Dict or set ordering | Sort keys before serialising |
| Floating-point formatting | Round to a fixed precision |
| Absolute paths | Make them relative |
| Locale-dependent formatting | Pin the locale |
Sorting keys and pinning the clock covers most of it.
5. Keep Them Small and Readable
A 5,000-line snapshot cannot be reviewed, so nobody reviews it, and the discipline above becomes impossible.
Better: snapshot a representative subset β the first three records rather than all two hundred, plus the record count and the field set. That is reviewable, and it still catches structural change.
summary = {
"count": len(records),
"fields": sorted({k for r in records for k in r}),
"first_three": records[:3],
}
That shape catches a vanished field, a changed count, and altered values, in a diff a person can actually read.
6. Snapshot Versus Golden Dataset
Easy to confuse, and different in kind:
| Snapshot | Golden dataset | |
|---|---|---|
| Expected values from | Whatever the code produced | A person reading the page |
| Answers | Did it change? | Is it correct? |
| Catches regressions | Yes | Yes |
| Catches being wrong from day one | No | Yes |
That last row is the crucial limitation. A snapshot of buggy output happily locks in the bug. Snapshot testing detects change; only human-verified expectations detect wrongness.
Use snapshots for breadth and golden data for correctness.
7. When Not to Use It
- For a small, well-understood output where explicit assertions are clearer.
- Where the output is legitimately nondeterministic and cannot be pinned.
- As the only form of testing, since it verifies nothing about correctness.
- For anything where a reviewer would not read the diff.
8. Summary
Snapshot testing records output once and fails on any change, giving broad cheap coverage of large structured results such as extraction output.
Its value depends entirely on reading every diff and updating only intended changes β reflexive updates turn the suite into a recording that asserts nothing β and because expectations come from the code rather than from a person, it detects change but never detects having been wrong from the start.