Snapshot Testing in Jest and Vitest: Patterns and Best Practices
Introduction
Snapshot testing captures the output of your code at a point in time and compares future runs against that baseline. When the output changes, the test fails, alerting you to unexpected changes. This makes snapshot testing particularly useful for guarding against unintended changes in data structures, API responses, UI output, or any serializable value. You will learn how to write, update, and maintain snapshot tests effectively using Jest and Vitest, covering practical patterns that help you get the most out of snapshots while avoiding common pitfalls.
How snapshots work
When you first run a snapshot test, Jest or Vitest serializes the value being tested and saves it to a separate file. On subsequent runs, the serializer output is compared against the saved snapshot:
// component.js
export function formatUser(user) {
return JSON.stringify(user, null, 2);
}
// component.test.js
import { formatUser } from './component';
test('formats user correctly', () => {
const user = { id: 1, name: 'Alice', email: 'alice@example.com' };
expect(formatUser(user)).toMatchSnapshot();
});
The first time you run this test, it passes and creates a snapshot file:
// __snapshots__/component.test.js.snap
exports[`formats user correctly 1`] = `
"{
\\"id\\": 1,
\\"name\\": \\"Alice\\",
\\"email\\": \\"alice@example.com\\"
}"
`;
On the next run, if formatUser returns the same output, the test passes. If the output changes, the test fails and shows you exactly what changed.
Inline Snapshots
Instead of storing snapshots in separate files, you can embed them directly in your test file using inline snapshots:
import { formatUser } from './component';
test('formats user with inline snapshot', () => {
const user = { id: 1, name: 'Alice', email: 'alice@example.com' };
expect(formatUser(user)).toMatchInlineSnapshot();
});
When you run this test with the --updateSnapshot flag, the snapshot value gets written directly into your test file:
test('formats user with inline snapshot', () => {
const user = { id: 1, name: 'Alice', email: 'alice@example.com' };
expect(formatUser(user)).toMatchInlineSnapshot(`
"{
\\"id\\": 1,
\\"name\\": \\"Alice\\",
\\"email\\": \\"alice@example.com\\"
}"
`);
});
Inline snapshots keep snapshots close to their tests, which works well for small, stable outputs. Use external snapshot files for large outputs or when multiple tests share the same data.
Snapshot Matchers
Beyond toMatchSnapshot(), Jest and Vitest provide additional snapshot matchers:
toHaveSnapshot()
The newer toHaveSnapshot() API (available in Jest 29+) provides better type safety and clearer error messages:
import { formatUser } from './component';
test('user format has snapshot', () => {
const user = { id: 1, name: 'Alice' };
expect(formatUser(user)).toHaveSnapshot();
});
toThrowErrorMatchingSnapshot()
Capture error messages as snapshots to track changes in error handling:
import { validateEmail } from './validator';
test('throws on invalid email', () => {
expect(() => validateEmail('invalid')).toThrowErrorMatchingSnapshot();
});
Property Matchers
For objects with dynamic values like timestamps or IDs, use property matchers to snapshot only the stable parts:
test('user with dynamic data', () => {
const user = {
id: expect.any(Number),
name: 'Alice',
createdAt: expect.any(Date),
token: expect.stringMatching(/^[a-z0-9]+$/)
};
expect(formatUser(user)).toMatchSnapshot({
id: expect.any(Number),
createdAt: expect.any(Date),
token: expect.stringMatching(/^[a-z0-9]+$/)
});
});
Snapshots with async data
Snapshots work with promises, but you need to handle the async nature properly:
import { fetchUser } from './api';
test('fetches user snapshot', async () => {
const user = await fetchUser(1);
// Snapshot stable fields only
expect(user).toMatchSnapshot({
id: expect.any(Number),
timestamp: expect.any(String),
avatarUrl: expect.stringMatching(/^https?:\/\//)
});
});
For API responses, consider snapshotting specific transformations rather than raw responses to avoid brittle tests:
test('user display data', async () => {
const user = await fetchUser(1);
// Transform to the shape you actually care about
const displayData = {
name: user.name,
initials: user.name.split(' ').map(n => n[0]).join(''),
isVerified: user.verifiedAt !== null
};
expect(displayData).toMatchSnapshot();
});
Snapshotting DOM Output
A common use case for snapshots is testing component render output. Even without a full testing library, you can snapshot rendered HTML:
// renderer.js
export function renderCard(title, content) {
return `
<div class="card">
<h2 class="card-title">${title}</h2>
<div class="card-content">${content}</div>
</div>
`;
}
// renderer.test.js
import { renderCard } from './renderer';
test('renders card correctly', () => {
const html = renderCard('Welcome', 'Hello, world!');
expect(html).toMatchSnapshot();
});
When using React Testing Library, you can snapshot the container’s HTML:
import { render } from '@testing-library/react';
import { Card } from './Card';
test('Card renders correctly', () => {
const { container } = render(<Card title="Hello">World</Card>);
expect(container.innerHTML).toMatchSnapshot();
});
Managing snapshot files
Updating Snapshots
When legitimate changes occur, update snapshots with the CLI flag:
# Update all snapshots
npm test -- --updateSnapshot
# Or the shorter alias
npm test -- -u
For Vitest:
vitest update
Interactive update mode
Jest offers an interactive mode that lets you review each changed snapshot:
npm test -- --watchAll --updateSnapshot
This shows you each failing snapshot and lets you approve or reject changes individually.
Organizing Snapshots
For larger projects, organize snapshots by feature or module:
__snapshots__
├── auth.test.js.snap
├── api.test.js.snap
└── components.test.js.snap
Use descriptive test names so snapshot names are meaningful:
test('formats user with email', () => { /* ... */ });
test('formats user without email', () => { /* ... */ });
// Creates snapshots: "formats user with email 1" and "formats user without email 1"
When to use snapshots
Snapshots excel in these scenarios:
- API response validation: Catch unexpected changes in external API contracts
- Configuration objects: Monitor changes to complex configuration structures
- Error messages: Track intentional changes to user-facing messages
- Generated output: Validate code generation, serialization, or transformation results
- Large data structures: Avoid manually asserting on many properties
Avoid snapshots for:
- Frequently changing data: Daily timestamps, counters, or session IDs
- Complex UI: Use component tests with assertions instead
- Behavioral testing: Snapshots don’t express intent; use regular assertions for logic
- Large binary data: Snapshot files become unwieldy
Best Practices
1. Review snapshots before committing
Always review snapshot diffs before committing. Ask yourself: “Did I intentionally change this?“
2. Keep snapshots small
Snapshot large outputs only after trimming to the essential parts:
// Instead of snapshotting the full response
test('user response', () => {
const user = await fetchUser(1);
// Snapshot only what matters
expect(user).toMatchSnapshot({
id: expect.any(Number),
name: user.name,
// Skip verbose nested objects
preferences: expect.objectContaining({
theme: expect.any(String)
})
});
});
3. Use property matchers for dynamic values
Prevent snapshot churn from expected variations:
test('generates report', () => {
const report = generateReport({
generatedAt: new Date().toISOString(),
requestId: uuid(),
data: [1, 2, 3]
});
expect(report).toMatchSnapshot({
generatedAt: expect.any(String),
requestId: expect.any(String)
});
});
4. Version control your snapshots
Commit snapshot files alongside code changes. They’re integral to your test suite.
5. Add custom serializers for complex objects
For objects that don’t serialize well by default, add custom serializers:
// jest.config.js
module.exports = {
snapshotSerializers: ['my-serializer'],
};
// my-serializer.js
testUtils.addEqualityTesters([
(a, b) => {
if (a instanceof CustomClass && b instanceof CustomClass) {
return a.id === b.id;
}
return undefined; // Use default comparison
}
]);
Snapshot Scope
Snapshots are most useful when they cover output that is stable and meaningful to read. If the value changes all the time, the snapshot becomes noise rather than protection. Before adding a snapshot, ask whether the shape is stable enough to compare and whether a failed diff will tell the reviewer something useful. That question keeps the test suite focused on behavior that benefits from a saved baseline instead of trying to snapshot everything in sight.
Reviewer Workflow
Snapshot diffs should be part of the review process, not something people approve automatically. A good review asks whether the new output is expected and whether the change belongs in a snapshot at all. That habit matters because snapshots are easy to update without thinking through the consequence. If the team treats the diff as a real decision point, the tests stay helpful instead of turning into a rubber stamp for visual or data churn.
Keep serializers close
When an object needs custom serialization, keep the serializer near the tests that depend on it. That makes it easier to understand why the snapshot looks the way it does and what would break if the serializer changed. A small, local serializer is also easier to remove later if the object shape becomes simpler. The goal is to make snapshots readable, not to bury the output behind a wide layer of hidden rules.
Snapshot files that grow too large become hard to review during code changes. When a file exceeds a few hundred lines, consider splitting it by test module or trimming the captured output to essential fields only. Keeping snapshots compact makes diffs easier to scan and keeps pull request reviews focused on meaningful changes rather than noise.
Summary
Snapshot testing is a powerful tool when used appropriately. Remember these key points:
- Use
toMatchSnapshot()for initial snapshots andtoMatchInlineSnapshot()for embedded snapshots - Use property matchers (
expect.any(),expect.stringMatching()) for dynamic values - Update snapshots intentionally using CLI flags after reviewing changes
- Avoid snapshotting frequently changing data or complex UI directly
- Organize snapshots logically and keep them focused on stable outputs
In the previous tutorial in this series, you learned about code coverage and CI integration. Snapshot testing complements these practices by providing a safety net against unintended changes.
In the next tutorial in this series, you’ll explore TypeScript-specific testing patterns and type-aware testing strategies.
See Also
- Testing with Jest: Get started with Jest, the most popular JavaScript testing framework
- Testing with Vitest: Learn about Vitest, a fast and modern alternative to Jest
- Mocking: Master mocking techniques for isolating units under test
- JSON.parse(): Understand how JSON parsing works under the hood