Quick answer: Snapshot tests are cheap to write and expensive to trust when they lock down an entire response that includes volatile fields like timestamps, generated ids, and request echoes. A snapshot that captures every byte will fail on the next run for a harmless created_at difference even when the real behavior is unchanged, and teams quickly learn to re-run with --update unread, which quietly trains everyone to ignore snapshot failures. The fix is to snapshot only the stable contract: strip or pin volatile fields before comparing, and assert the dynamic values separately with targeted expectations. A snapshot should prove that the meaningful output did not change, and anything that changes on every run by design has no business being in it.

The failure mode is seductive because the first snapshot is so satisfying. One assertion replaces a dozen hand-written checks, and the test passes immediately. The pain arrives on the second run, or in CI where the clock and the generated identifiers differ from the developer's machine. The diff shows a wall of red on updated_at and trace ids, the actual fields under test are buried underneath, and the path of least resistance is to update the snapshot without reading it. From that point the test is noise.

What makes a snapshot brittle

A handful of field types account for nearly all the false failures, and they share the trait of being correct and different on every run.

Volatile field Why it changes Better treatment
created_at / updated_at Wall clock at execution Freeze or match format
Generated UUID / id New value each run Assert shape, not value
Trace / request id Unique per request Ignore in snapshot
Echoed server time Different in CI Strip before compare
Ordering of a set Undefined sort Sort deterministically

The ordering problem is often mistaken for a timestamp problem. When a response contains a list whose order is not guaranteed, the snapshot can fail purely because two equivalent items swapped positions. Sorting the list by a stable key before snapshotting removes a source of churn that has nothing to do with the data itself.

Freeze time and pin the inputs

For timestamps, the cleanest solution is to control the clock during the test so "now" is a known constant, rather than capturing whatever the wall clock returns. Combined with fixed inputs, this makes the stable parts of the response genuinely reproducible.

With a frozen clock, the timestamp in the snapshot is the same on every machine, including CI. The explicit assertion on created_at also documents that the field is meaningful, rather than letting it hide inside a blob.

Normalize the response before snapshotting

When you cannot control the source of a volatile field, normalize the response: drop the fields that are intentionally unique, replace generated ids with placeholders, and sort lists. The snapshot then captures the durable contract while the stripped values are checked with focused assertions.

Using expect.any(String) for the generated id keeps the snapshot honest about the field existing while allowing the value to vary, and the separate regex assertion proves the id is well formed. The snapshot no longer fails on identity fields it was never in a position to verify.

Prefer contract assertions for the fields that matter

Snapshots are a regression net, not a design tool. The fields with real business meaning, money amounts, status transitions, and required keys, should have explicit assertions that fail with a clear message and document intent. Reserve the snapshot for the surrounding structure so a single critical field changing does not get lost in a large diff.

In a spec-driven workflow, the OpenAPI description already defines the response shape, so generated types and schema validation cover the structural contract that a snapshot would otherwise pin by hand. The snapshot then only needs to guard behavior the schema cannot express, which keeps it small and meaningful.

Frequently asked questions

Are snapshots bad in general?

No. They are excellent regression nets for stable, small outputs such as a formatted string or a normalized object. They become brittle when they capture large, naturally volatile responses without normalization.

How do I stop the team blindly updating snapshots?

Keep snapshots small and free of noise so a real failure is obvious, and require review of any snapshot diff. When churn drops, updating no longer feels routine, and failures regain their meaning.

Should I freeze time everywhere?

Use fake timers for tests that depend on "now," and keep a couple of real-clock tests that assert the format rather than the exact value. Freezing every test can hide genuine timezone or formatting bugs.

What if the volatile field is itself what I want to test?

Then assert it directly with a matcher, a regex, or a type check rather than snapshotting its exact value. Snapshot the stable structure and verify the dynamic field separately.

How does this relate to schema validation?

A schema or generated type enforces the presence and type of every field, including volatile ones, without pinning their exact values. That covers structural regression and lets the snapshot focus on behavior the schema cannot express.

This is part of the API Testing Guide — start there for the broader discipline of writing tests that fail for real reasons.