Snapshot Testing: When It Helps and When It Doesn't
- Contributor
- May 3
- 4 min read
A snapshot test captures the output of code (a rendered component, a serialized value, a JSON response) and compares future runs against the captured version. When the output matches, the test passes. When it differs, the test fails and the developer decides: was the change intended or a regression?
Snapshot testing is cheap to set up and surprisingly nuanced to use well. This guide is when it helps and when it doesn't.
How Snapshots Work
A typical Jest snapshot test:
test('UserCard renders correctly', () => {
const tree = renderer.create(<UserCard user={mockUser} />).toJSON();
expect(tree).toMatchSnapshot();
});
On first run, the test captures the rendered output to a snapshot file. On subsequent runs, the output is compared. Any difference fails the test.
When the change is intended, the developer updates the snapshot with jest --updateSnapshot.
Why Snapshots Are Tempting
Easy to write: one line replaces dozens of assertions
Catches any change to output
Provides a record of what the output looks like
Detects unintended side effects
For UI components, JSON responses, or generated code, snapshots can be efficient regression coverage.
Why Snapshots Often Disappoint
Several common failures:
Mass updates erase signal. When a snapshot fails, the developer often updates it without carefully reviewing. The "test" passes again, but the actual regression slipped through.
Snapshots become huge. Whole-component snapshots include hundreds of lines. Diffs are unreadable. The developer accepts the change rather than understanding it.
Snapshots become noisy. Any minor change — a new prop, a class rename — fails snapshots. The team gets used to updating snapshots routinely. The signal-to-noise ratio drops.
Snapshots verify the wrong thing. A test of "the component renders" passes when the component renders gibberish, as long as it consistently renders gibberish.
When Snapshots Help
Inline snapshots for small, focused output:
test('formatPrice handles whole numbers', () => {
expect(formatPrice(1000)).toMatchInlineSnapshot('"$1,000.00"');
});
Inline snapshots live next to the test. They're small, reviewed in the diff, and treated as assertions.
Schema/structure tests where the structure matters more than specific values:
test('user API returns expected structure', () => {
const user = api.getUser(1);
expect(Object.keys(user).sort()).toMatchSnapshot();
});
This captures the shape of the response without coupling to specific values.
Generated code where you want to know it changed:
test('generated migration matches expected', () => {
const sql = generateMigration(model);
expect(sql).toMatchSnapshot();
});
Migrations should change deliberately; an unexpected diff is signal.
When Snapshots Don't Help
Component rendering with rich props. Snapshot includes everything. Any change fails. Test becomes noise.
API responses with timestamps. The snapshot fails every run because timestamps change. Solution: mock the timestamp, but that's more work than direct assertions.
Code with non-determinism. Random IDs, ordering that varies. Snapshots fail randomly.
Code that changes often by design. Constantly updating snapshots without thinking. Defeats the purpose.
The Discipline That Makes Snapshots Useful
A few practices that separate effective from cargo-cult snapshot usage:
Review every snapshot diff carefully. If you're updating snapshots without reading the diff, you've stopped using them as tests.
Keep snapshots small. Whole-component snapshots are usually too coarse. Test specific outputs.
Use inline snapshots where possible. They're visible in code review.
Prefer specific assertions for specific behaviors. "Click increments counter" is better as expect(counter).toBe(1) than as a snapshot.
Treat large snapshot diffs as smells. If a small code change produces a 500-line snapshot diff, you're testing too much in one snapshot.
Snapshot vs. Explicit Assertion
When you have a choice between:
expect(result).toMatchSnapshot();
vs.
expect(result.title).toBe('Welcome');
expect(result.items).toHaveLength(3);
expect(result.items[0].id).toBe('abc');
The explicit form is usually better. It:
Documents what's being tested
Resists noise (only fails for what you assert)
Survives non-essential changes
Reads as documentation
Snapshots replace many small assertions with one large blob. Sometimes that's a win; often it isn't.
Snapshots for Visual Regression
Visual regression testing — taking screenshots, comparing them — is a specialized form of snapshot testing.
Tools: Percy, Chromatic, BackstopJS.
Strengths:
Catches actual visual regressions
Reveals subtle UI bugs
Documents UI states
Challenges:
Noisy (fonts, anti-aliasing, animation timing)
Requires careful baseline management
Cross-browser variation
Higher infrastructure cost
For design-system-heavy products, worth the investment. For simpler UIs, may be overkill.
Maintenance Practices
To keep snapshots useful:
Review snapshot files in PRs like any other code
Delete obsolete snapshots with jest --ci (warns about unused)
Investigate flaky snapshots as test bugs, not noise
Use snapshot serializers to normalize content (timestamps, IDs)
Anti-Patterns
Snapshot all the things. Every test ends with toMatchSnapshot(). No specific behavior is verified.
Auto-accept on update. Running --updateSnapshot without reading the diff. Defeats the test.
Snapshot of complex objects. Massive blobs no one reads. Diffs are unreviewable.
Snapshot tests as primary regression strategy. Other tests are easier to interpret; snapshots should be supplementary.
Snapshots as documentation. "The snapshot shows what the component does." If you need documentation, write it. Tests aren't docs.
When the Build Fails on Snapshots
A common pattern: CI fails because snapshots don't match. The team's response distinguishes effective from ineffective snapshot usage.
Effective response:
Read the diff
Determine if it's an intended change or regression
For intended changes, update the snapshot
For regressions, fix the code
Ineffective response:
Auto-update snapshots to make CI green
Don't read the diff
Carry on
The second response makes snapshots into theater.
A Working Snapshot Strategy
For a typical project:
Use inline snapshots for small outputs. Visible, reviewable.
Use file snapshots for generated code. Migrations, config files, etc.
Avoid snapshot testing for UI components. Use explicit assertions about behavior.
Use visual regression tools (if appropriate) for actual UI.
Review snapshot changes carefully. Treat them as test changes.
Key Takeaway
Snapshot testing is easy to write and surprisingly hard to use well. The technique works when snapshots are small, focused, and reviewed carefully. It fails when snapshots are large, comprehensive, and updated reflexively. Prefer explicit assertions for specific behaviors; use snapshots for cases where the output's structure matters more than its specific values. Inline snapshots beat file snapshots for visibility. Treat unintended snapshot changes as test failures, not noise.
Related reading
Keep learning. This article is part of the Test Automation path in the ShiftQuality Learning Center. Build test automation that lasts, with ROI you can defend.


