Day 7: Snapshot Testing
What You'll Learn Today
- What snapshot testing is and when to use it
- Using
toMatchSnapshot()for basic snapshot tests - Using
toMatchInlineSnapshot()for inline snapshots - Updating snapshots with
--updateSnapshot/-u - Creating custom serializers
- Snapshot testing for non-UI data (API responses, config objects)
- Best practices and common pitfalls
What Is Snapshot Testing?
Snapshot testing captures the output of a value, saves it to a file, and compares future outputs against that saved "snapshot." It automatically detects unintended changes in your code's output.
flowchart TB
subgraph First["First Run"]
T1["Run test"] --> S1["Generate snapshot"]
S1 --> F1["Save to\n__snapshots__/ file"]
end
subgraph Later["Subsequent Runs"]
T2["Run test"] --> S2["Generate new output"]
S2 --> CMP["Compare with\nsaved snapshot"]
CMP -->|"Match"| PASS["Test passes ✓"]
CMP -->|"Mismatch"| FAIL["Test fails ✗"]
end
style First fill:#3b82f6,color:#fff
style Later fill:#8b5cf6,color:#fff
When to Use Snapshot Testing
| Good Fit | Poor Fit |
|---|---|
| UI component output | Business logic validation |
| API response structure | Frequently changing data |
| Config object change detection | Dynamic values (dates, random IDs) |
| Error message consistency | Huge data structures |
toMatchSnapshot() Basics
toMatchSnapshot() saves a value to an external snapshot file and compares against it on subsequent runs.
// formatUser.js
function formatUser(user) {
return {
displayName: `${user.firstName} ${user.lastName}`,
email: user.email.toLowerCase(),
role: user.role || 'member',
};
}
module.exports = { formatUser };
// formatUser.test.js
const { formatUser } = require('./formatUser');
test('formats user correctly', () => {
const user = {
firstName: 'Alice',
lastName: 'Smith',
email: 'Alice@Example.com',
role: 'admin',
};
expect(formatUser(user)).toMatchSnapshot();
});
TypeScript version:
// formatUser.ts
interface User {
firstName: string;
lastName: string;
email: string;
role?: string;
}
interface FormattedUser {
displayName: string;
email: string;
role: string;
}
export function formatUser(user: User): FormattedUser {
return {
displayName: `${user.firstName} ${user.lastName}`,
email: user.email.toLowerCase(),
role: user.role || 'member',
};
}
On the first run, Jest auto-generates __snapshots__/formatUser.test.js.snap:
// Jest Snapshot v1, https://goo.gl/fbAXQV
exports[`formats user correctly 1`] = `
{
"displayName": "Alice Smith",
"email": "alice@example.com",
"role": "admin",
}
`;
Multiple Snapshots
When a single test file contains multiple snapshots, they are automatically numbered.
test('formats different user roles', () => {
const admin = formatUser({ firstName: 'Alice', lastName: 'Smith', email: 'a@b.com', role: 'admin' });
const member = formatUser({ firstName: 'Bob', lastName: 'Jones', email: 'b@c.com' });
// saved as "formats different user roles 1"
expect(admin).toMatchSnapshot();
// saved as "formats different user roles 2"
expect(member).toMatchSnapshot();
});
Tip: You can add a hint for readability:
expect(admin).toMatchSnapshot('admin user')
toMatchInlineSnapshot()
toMatchInlineSnapshot() embeds the snapshot directly in your test code. This is ideal for small snapshots since you can see the expected value without opening a separate file.
test('formats user inline', () => {
const user = formatUser({
firstName: 'Alice',
lastName: 'Smith',
email: 'Alice@Example.com',
role: 'admin',
});
// first run: Jest automatically fills in the snapshot
expect(user).toMatchInlineSnapshot(`
{
"displayName": "Alice Smith",
"email": "alice@example.com",
"role": "admin",
}
`);
});
On the first run, if toMatchInlineSnapshot() is called with an empty argument, Jest automatically inserts the snapshot into your test file.
flowchart LR
subgraph External["toMatchSnapshot()"]
E1["Saves snapshot\nto external file"]
E2["__snapshots__/\n*.snap"]
end
subgraph Inline["toMatchInlineSnapshot()"]
I1["Embeds snapshot\nin test code"]
I2["Test file itself\nis updated"]
end
style External fill:#3b82f6,color:#fff
style Inline fill:#22c55e,color:#fff
| Comparison | toMatchSnapshot() |
toMatchInlineSnapshot() |
|---|---|---|
| Storage | __snapshots__/*.snap |
Inside the test file |
| Readability | Requires opening separate file | Visible alongside the test |
| Best for | Larger snapshots | Small snapshots |
| Code review | Changes less visible | Diffs are clear |
Updating Snapshots
When code changes intentionally alter the output, you need to update the saved snapshots.
CLI Options
# update all snapshots
npx jest --updateSnapshot
# or shorthand
npx jest -u
# update snapshots for a specific test file
npx jest formatUser.test.js -u
Interactive Mode (Watch Mode)
When running jest --watch, press u to update failing snapshots:
Snapshot Summary
› 1 snapshot failed from 1 test suite.
› Press `u` to update failing snapshots.
Caution: Always review the diff before updating. Blindly updating snapshots defeats the purpose of snapshot testing.
Handling Dynamic Values
For values that change on every run (dates, IDs, random values), use property matchers.
test('creates a user with dynamic fields', () => {
const user = {
id: Math.random().toString(36).substr(2, 9),
name: 'Alice',
createdAt: new Date().toISOString(),
};
expect(user).toMatchSnapshot({
id: expect.any(String),
createdAt: expect.any(String),
});
});
In this case, id and createdAt are verified with expect.any(String), while name is compared exactly against the snapshot.
TypeScript version:
interface UserRecord {
id: string;
name: string;
createdAt: string;
}
test('creates a user with dynamic fields', () => {
const user: UserRecord = {
id: Math.random().toString(36).substr(2, 9),
name: 'Alice',
createdAt: new Date().toISOString(),
};
expect(user).toMatchSnapshot({
id: expect.any(String),
createdAt: expect.any(String),
});
});
You can also use property matchers with inline snapshots:
test('user with dynamic fields inline', () => {
const user = {
id: 'abc123',
name: 'Alice',
createdAt: '2025-01-01T00:00:00.000Z',
};
expect(user).toMatchInlineSnapshot(
{
id: expect.any(String),
createdAt: expect.any(String),
},
`
{
"createdAt": Any<String>,
"id": Any<String>,
"name": "Alice",
}
`
);
});
Custom Serializers
When Jest's default serializer doesn't suit your needs, you can create custom serializers.
// stripAnsi.serializer.js
// Custom serializer that removes ANSI escape codes
module.exports = {
serialize(val) {
return val.replace(/\u001b\[[0-9;]*m/g, '');
},
test(val) {
return typeof val === 'string' && /\u001b\[[0-9;]*m/.test(val);
},
};
Register in Jest Config
// jest.config.js
module.exports = {
snapshotSerializers: ['./stripAnsi.serializer.js'],
};
Add Per-Test
expect.addSnapshotSerializer({
serialize(val) {
// format Date objects as YYYY-MM-DD
return `"${val.toISOString().split('T')[0]}"`;
},
test(val) {
return val instanceof Date;
},
});
test('date snapshot', () => {
const date = new Date('2025-06-15T12:00:00Z');
expect(date).toMatchInlineSnapshot(`"2025-06-15"`);
});
TypeScript version:
import type { NewPlugin } from 'pretty-format';
const dateSerializer: NewPlugin = {
serialize(val: Date) {
return `"${val.toISOString().split('T')[0]}"`;
},
test(val: unknown): val is Date {
return val instanceof Date;
},
};
expect.addSnapshotSerializer(dateSerializer);
Snapshot Testing for Non-UI Data
Snapshot testing isn't just for UI components. It works well for many types of data structures.
API Response Structure
// apiClient.test.js
const { fetchUserProfile } = require('./apiClient');
test('user profile API response structure', async () => {
const profile = await fetchUserProfile(1);
expect(profile).toMatchSnapshot({
id: expect.any(Number),
lastLogin: expect.any(String),
});
});
Config Objects
// config.test.js
const { getConfig } = require('./config');
test('production config snapshot', () => {
const config = getConfig('production');
expect(config).toMatchInlineSnapshot(`
{
"database": {
"host": "db.production.example.com",
"port": 5432,
"ssl": true,
},
"logging": {
"level": "error",
},
}
`);
});
Error Messages
// validator.test.js
const { validate } = require('./validator');
test('validation error messages', () => {
const errors = validate({
name: '',
email: 'invalid',
age: -1,
});
expect(errors).toMatchInlineSnapshot(`
[
"Name is required",
"Email format is invalid",
"Age must be a positive number",
]
`);
});
Best Practices
1. Keep Snapshots Small and Focused
// BAD: snapshot of the entire component tree
test('renders page', () => {
const tree = render(<EntirePage />);
expect(tree).toMatchSnapshot(); // hundreds of lines
});
// GOOD: snapshot of specific parts
test('renders user card', () => {
const card = render(<UserCard name="Alice" role="admin" />);
expect(card).toMatchSnapshot();
});
2. Use Descriptive Test Names
// BAD
test('snapshot 1', () => { ... });
// GOOD
test('renders error state when API fails', () => { ... });
3. Commit Snapshot Files
Include the __snapshots__/ directory in version control. This allows code reviewers to verify snapshot changes.
4. Clean Up Obsolete Snapshots
When you delete tests, their corresponding snapshots remain. Periodically clean them up:
npx jest --ci --updateSnapshot
flowchart TB
subgraph Good["Good Snapshots"]
G1["Small and focused"]
G2["Changes are meaningful"]
G3["Easy to review"]
end
subgraph Bad["Bad Snapshots"]
B1["Huge and all-encompassing"]
B2["Changes are unclear"]
B3["Blindly updated"]
end
style Good fill:#22c55e,color:#fff
style Bad fill:#ef4444,color:#fff
Common Pitfalls
Snapshot Fatigue
When you have too many snapshot tests, developers tend to run jest -u to bulk-update without reviewing diffs. This defeats the purpose of snapshot testing entirely.
flowchart LR
A["Too many snapshots"] --> B["Many failures\non every change"]
B --> C["Bulk update\nwithout reviewing"]
C --> D["Bugs slip through"]
D --> E["Snapshot tests\nbecome meaningless"]
style A fill:#f59e0b,color:#fff
style E fill:#ef4444,color:#fff
Solutions:
- Only use snapshots where they truly add value
- Break large snapshots into smaller, focused ones
- Always review snapshot diffs during code review
Meaningless Updates
Platform-dependent values or auto-generated class names can cause snapshots to break without any meaningful code change.
// BAD: platform-dependent snapshot
test('snapshot with platform-dependent value', () => {
expect(process.platform).toMatchSnapshot();
});
// BAD: snapshot with CSS-in-JS
test('snapshot with CSS-in-JS', () => {
// auto-generated class names change on every build
expect(tree).toMatchSnapshot();
});
Oversized Snapshots
Nobody reviews snapshots that are hundreds of lines long.
| Anti-pattern | Solution |
|---|---|
| Snapshot of entire page | Split into component-level snapshots |
| Dynamic values in snapshot | Use property matchers |
| Update without reviewing | Review diffs during code review |
| Snapshot everything | Choose the right testing approach |
Summary
| Concept | Description |
|---|---|
toMatchSnapshot() |
Saves value to external file and compares |
toMatchInlineSnapshot() |
Embeds snapshot in test code for comparison |
--updateSnapshot / -u |
Updates snapshots to match current output |
| Property matchers | Handles dynamic values with expect.any() |
| Custom serializers | Customizes snapshot formatting |
| Snapshot fatigue | Problem of ignoring diffs due to too many snapshots |
Key Takeaways
- Snapshot testing detects "what changed," not "what is correct"
- Keep snapshots small and focused
- Use property matchers for dynamic values
- Always review snapshot diffs during code review
- Combine snapshot tests with other testing approaches - don't rely on them exclusively
Exercises
Exercise 1: Basics
Write a snapshot test using toMatchSnapshot() for the following formatProduct function.
function formatProduct(product) {
return {
title: product.name.toUpperCase(),
price: `$${product.price.toFixed(2)}`,
inStock: product.quantity > 0,
};
}
Exercise 2: Intermediate
Write a test for the following generateReport function using toMatchInlineSnapshot(), handling the dynamic fields (generatedAt, id) with property matchers.
function generateReport(data) {
return {
id: Math.random().toString(36).substr(2, 9),
title: `Report: ${data.name}`,
itemCount: data.items.length,
generatedAt: new Date().toISOString(),
};
}
Challenge
Create a custom serializer that formats Date objects as YYYY/MM/DD, and write a snapshot test that uses it.
References
- Jest - Snapshot Testing
- Jest - toMatchSnapshot()
- Jest - toMatchInlineSnapshot()
- Jest - Snapshot Serializers
- Effective Snapshot Testing
Next up: Day 8 covers Coverage and Debugging -- how to measure test coverage, read coverage reports, and debug your tests effectively!