Snapshot Testing Best Practices: Keeping Snapshots Maintainable
Snapshots that start as a safety net often become a maintenance burden. Test files fill with .snap files that nobody reviews. Developers run jest -u reflexively when tests fail. The snapshots grow to thousands of lines covering third-party component internals.
This guide covers the practices that keep snapshot tests small, reviewable, and genuinely useful over time.
Practice 1: Keep Snapshots Small and Focused
The single most important snapshot best practice: test only what you care about.
Full-component snapshots capture everything — including elements, attributes, and nested children you don't care about. When any part of the tree changes, the snapshot fails. This leads to false positives and reflexive jest -u runs.
// Bad — snapshots the entire rendered tree
test('UserCard renders correctly', () => {
const { container } = render(<UserCard user={user} />);
expect(container).toMatchSnapshot(); // hundreds of lines
});
// Good — snapshots the specific element you care about
test('UserCard shows user name and role badge', () => {
const { getByTestId } = render(<UserCard user={user} />);
expect(getByTestId('user-name')).toMatchInlineSnapshot(`
<span data-testid="user-name">Jane Doe</span>
`);
expect(getByTestId('role-badge')).toMatchInlineSnapshot(`
<span class="badge badge-admin" data-testid="role-badge">Admin</span>
`);
});A good snapshot is 5-30 lines. If yours is longer, scope it down.
Practice 2: Prefer Inline Snapshots for Small Output
Inline snapshots embed the expected value directly in the test file, eliminating the need to open a separate .snap file to understand what a test is checking:
// Inline snapshot — everything visible in one place
test('formats price with currency symbol', () => {
expect(formatPrice(29.99, 'USD')).toMatchInlineSnapshot(`"$29.99"`);
expect(formatPrice(29.99, 'EUR')).toMatchInlineSnapshot(`"€29.99"`);
expect(formatPrice(29.99, 'GBP')).toMatchInlineSnapshot(`"£29.99"`);
});When to use inline: Output is under 20 lines, and you want the expected value visible in the test.
When to use file snapshots: Output is larger (20-50 lines) and would clutter the test file.
Never use file snapshots when: The output is over 50 lines — scope it down instead.
Practice 3: Use Descriptive Test Names
The snapshot file key is derived from the test name. Descriptive test names make snapshot files navigable:
// Bad — vague test name
test('renders correctly', () => { // → "renders correctly 1"
expect(component).toMatchSnapshot();
});
// Good — specific test name
test('renders error state with retry button when fetch fails', () => {
// → "renders error state with retry button when fetch fails 1"
expect(component).toMatchSnapshot();
});When you open the .snap file, you should be able to understand what each snapshot represents without reading the test.
Practice 4: Mask Dynamic Values
Snapshots with dynamic values (timestamps, random IDs, session tokens) fail on every run because the values change. Mask them explicitly:
// Bad — includes dynamic timestamp
test('creates user record', async () => {
const user = await createUser({ name: 'Alice' });
expect(user).toMatchSnapshot(); // fails because createdAt changes
});
// Good — mask dynamic values with asymmetric matchers
test('creates user record', async () => {
const user = await createUser({ name: 'Alice' });
expect(user).toMatchSnapshot({
id: expect.any(Number),
createdAt: expect.any(String),
updatedAt: expect.any(String),
sessionToken: expect.any(String),
// Static fields matched exactly
name: 'Alice',
role: 'user',
emailVerified: false,
});
});Asymmetric matchers tell Jest "expect something of this type here" rather than matching the exact value.
Practice 5: Never Update Snapshots Without Reading the Diff
This is the most common failure mode. When tests fail, developers run jest -u to make the red tests green without understanding what changed.
Establish a team norm: snapshots are only updated after the diff is reviewed and understood.
Make the diff visible by running Jest with the diff flag:
# See the full diff before deciding to update
jest --verbose 2>&1 <span class="hljs-pipe">| grep -A 20 <span class="hljs-string">"Snapshot name:"
<span class="hljs-comment"># Update only after reviewing
jest -uAdd this to your PR description template:
## Snapshot Changes
- [ ] Reviewed all snapshot diffs — changes are intentional
- [ ] No unexpected output changes in updated snapshotsPractice 6: Organize Snapshot Files
Jest auto-creates __snapshots__/ directories next to test files. Keep this structure and avoid moving .snap files manually:
src/
components/
Button/
Button.test.tsx
__snapshots__/
Button.test.tsx.snap
UserCard/
UserCard.test.tsx
__snapshots__/
UserCard.test.tsx.snapDon't consolidate all snapshots into one file — the auto-generated structure ties each snapshot to its test file, making git diffs readable.
Practice 7: Delete Obsolete Snapshots Regularly
When tests are deleted or renamed, their snapshots become orphaned. Orphaned snapshots accumulate and mislead:
# See what obsolete snapshots exist
jest --verbose --ci 2>&1 <span class="hljs-pipe">| grep <span class="hljs-string">"obsolete"
<span class="hljs-comment"># Remove them (also removes if tests are renamed)
jest -uMake snapshot cleanup part of your regular test maintenance. A monthly jest -u run specifically to clean up obsolete snapshots prevents the accumulation.
Practice 8: Use Custom Serializers for Cleaner Output
The default Jest serializer produces verbose output for DOM nodes. Custom serializers can make snapshots more readable:
jest-serializer-html
Formats HTML more readably:
npm install --save-dev jest-serializer-html// jest.config.js
module.exports = {
snapshotSerializers: ['jest-serializer-html'],
};Filtering Irrelevant Attributes
If your test output includes internal framework attributes you don't care about (like data-reactroot, generated class names), filter them with a custom serializer:
// jest.setup.js
expect.addSnapshotSerializer({
test: (val) => val && val.nodeType === 1, // DOM elements
print: (val, serialize) => {
// Remove data-testid from snapshots (test-only attribute)
val.removeAttribute('data-testid');
return serialize(val);
},
});This keeps snapshots focused on what matters semantically, not test infrastructure.
Practice 9: Don't Snapshot Third-Party Component Internals
When your component renders a third-party UI library component, the snapshot includes that library's internal DOM structure. This creates two problems:
- The snapshot becomes enormous and unreadable
- Updating the UI library version fails your snapshots even when your code is correct
Scope snapshots to exclude third-party internals:
// Bad — includes all of MUI Button's internals
test('renders submit button', () => {
const { container } = render(<CheckoutButton />);
expect(container).toMatchSnapshot();
// Includes 50+ lines of MUI internals
});
// Good — only test what you own
test('renders submit button with correct label', () => {
const { getByRole } = render(<CheckoutButton />);
const button = getByRole('button', { name: 'Complete Purchase' });
expect(button).toBeInTheDocument();
expect(button).toBeEnabled();
});Practice 10: Review Snapshots in PRs Like Any Other Code
Snapshot updates show up in pull request diffs. Treat them with the same attention you'd give a code change:
Red flags in snapshot reviews:
- Snapshot that grew from 10 lines to 200 lines — too large
- New class names from a CSS-in-JS library — testing implementation details
- Timestamp or ID changes — dynamic values not being masked
- Snapshot updated for a PR that says "no frontend changes" — something changed that shouldn't have
Green flags:
- Snapshot diff matches the described feature change exactly
- Small, focused diff that clearly corresponds to the PR's intent
- Updated inline snapshot directly next to its test
Make snapshot review an explicit step in your PR review checklist.
Practice 11: Migrate Large Snapshots to Explicit Assertions
When you inherit a codebase with large, unmaintainable snapshots, don't delete them all at once. Migrate incrementally:
- Identify tests with snapshots over 50 lines
- Write explicit assertions for the important behaviors these tests guard
- Delete the large snapshot test once the explicit tests are green
- Repeat
This is lower risk than deleting all snapshots and hoping your explicit tests cover everything.
# Find tests with large snapshots
find . -name <span class="hljs-string">"*.snap" -<span class="hljs-built_in">exec <span class="hljs-built_in">wc -l {} + <span class="hljs-pipe">| <span class="hljs-built_in">sort -rn <span class="hljs-pipe">| <span class="hljs-built_in">head -20Start with the largest files — they have the most ROI.
Summary
Maintainable snapshot testing comes down to discipline around six things:
| Practice | What It Prevents |
|---|---|
| Small, focused snapshots | Brittle tests that fail for unrelated reasons |
| Inline snapshots for small output | Context-switching to read .snap files |
| Masking dynamic values | Flaky tests that fail due to timestamps/IDs |
| Reading diffs before updating | Approving regressions without noticing |
| Cleaning up obsolete snapshots | Misleading test coverage |
| No third-party component internals | Snapshot breakage on library updates |
Teams that follow these practices use snapshots effectively. Teams that don't end up with a snapshot tax — hundreds of tests that fail for irrelevant reasons, get updated without thought, and provide no real protection against regressions.