Jest 快照测试 #
快照测试概述 #
快照测试是一种测试技术,它会记录组件的输出,并在后续运行中与记录的快照进行比较,确保 UI 不会意外改变。
text
┌─────────────────────────────────────────────────────────────┐
│ 快照测试流程 │
├─────────────────────────────────────────────────────────────┤
│ 1. 首次运行:生成快照文件 │
│ 2. 后续运行:比较当前输出与快照 │
│ 3. 如果匹配:测试通过 │
│ 4. 如果不匹配:测试失败(可能是 bug 或有意修改) │
│ 5. 有意修改:更新快照 │
└─────────────────────────────────────────────────────────────┘
基本用法 #
toMatchSnapshot #
javascript
import renderer from 'react-test-renderer';
import Button from './Button';
test('Button snapshot', () => {
const tree = renderer.create(<Button>Click me</Button>).toJSON();
expect(tree).toMatchSnapshot();
});
首次运行后,会生成快照文件:
text
// __snapshots__/Button.test.js.snap
exports[`Button snapshot 1`] = `
<button
className="btn"
type="button"
>
Click me
</button>
`;
快照文件位置 #
text
src/
├── components/
│ ├── Button.jsx
│ ├── Button.test.jsx
│ └── __snapshots__/
│ └── Button.test.js.snap
React 组件快照 #
简单组件 #
javascript
import renderer from 'react-test-renderer';
import Title from './Title';
test('Title renders correctly', () => {
const tree = renderer.create(<Title>Hello World</Title>).toJSON();
expect(tree).toMatchSnapshot();
});
带属性的组件 #
javascript
import Avatar from './Avatar';
test('Avatar with image', () => {
const tree = renderer.create(
<Avatar src="/avatar.jpg" alt="User avatar" size="large" />
).toJSON();
expect(tree).toMatchSnapshot();
});
带状态的组件 #
javascript
import Counter from './Counter';
test('Counter initial state', () => {
const tree = renderer.create(<Counter initialCount={0} />).toJSON();
expect(tree).toMatchSnapshot();
});
test('Counter after increment', () => {
const component = renderer.create(<Counter initialCount={0} />);
// 触发状态变化
component.getInstance().increment();
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
条件渲染 #
javascript
import UserCard from './UserCard';
test('UserCard when logged in', () => {
const tree = renderer.create(
<UserCard isLoggedIn={true} user={{ name: 'John' }} />
).toJSON();
expect(tree).toMatchSnapshot();
});
test('UserCard when logged out', () => {
const tree = renderer.create(
<UserCard isLoggedIn={false} />
).toJSON();
expect(tree).toMatchSnapshot();
});
内联快照 #
toMatchInlineSnapshot #
快照直接写在测试文件中:
javascript
import Button from './Button';
test('Button inline snapshot', () => {
const tree = renderer.create(<Button>Click</Button>).toJSON();
expect(tree).toMatchInlineSnapshot(`
<button
className="btn"
type="button"
>
Click
</button>
`);
});
优点 #
- 快照和测试在同一个文件
- 更容易进行代码审查
- 适合小型快照
缺点 #
- 大型快照会使测试文件臃肿
- 需要手动维护
属性匹配器 #
忽略动态值 #
javascript
import DateDisplay from './DateDisplay';
test('DateDisplay snapshot', () => {
const tree = renderer.create(<DateDisplay />).toJSON();
expect(tree).toMatchSnapshot({
date: expect.any(String),
timestamp: expect.any(Number),
});
});
自定义序列化 #
javascript
expect.addSnapshotSerializer({
test: (val) => val && val.isMoment,
serialize: (val) => `Date(${val.format('YYYY-MM-DD')})`,
});
test('moment date', () => {
const tree = renderer.create(<DateComponent />).toJSON();
expect(tree).toMatchSnapshot();
});
快照更新 #
更新所有快照 #
bash
jest --updateSnapshot
# 或
jest -u
更新特定快照 #
bash
# 运行特定测试并更新
jest Button.test.jsx -u
交互式更新 #
bash
jest --watch
# 按 'u' 更新失败的快照
# 按 'i' 更新失败的快照(交互式)
快照测试最佳实践 #
1. 测试有意义的快照 #
javascript
// ❌ 测试整个页面
test('Page snapshot', () => {
const tree = renderer.create(<Page />).toJSON();
expect(tree).toMatchSnapshot();
});
// ✅ 测试独立组件
test('Header snapshot', () => {
const tree = renderer.create(<Header />).toJSON();
expect(tree).toMatchSnapshot();
});
test('Footer snapshot', () => {
const tree = renderer.create(<Footer />).toJSON();
expect(tree).toMatchSnapshot();
});
2. 使用描述性名称 #
javascript
// ❌ 不好的命名
test('snapshot', () => {});
// ✅ 好的命名
test('Button renders with primary variant', () => {});
test('Button renders with secondary variant', () => {});
test('Button renders in disabled state', () => {});
3. 测试不同状态 #
javascript
describe('Button', () => {
test('default state', () => {
const tree = renderer.create(<Button>Click</Button>).toJSON();
expect(tree).toMatchSnapshot();
});
test('disabled state', () => {
const tree = renderer.create(<Button disabled>Click</Button>).toJSON();
expect(tree).toMatchSnapshot();
});
test('loading state', () => {
const tree = renderer.create(<Button loading>Click</Button>).toJSON();
expect(tree).toMatchSnapshot();
});
test('with icon', () => {
const tree = renderer.create(
<Button icon={<Icon />}>Click</Button>
).toJSON();
expect(tree).toMatchSnapshot();
});
});
4. 避免过大快照 #
javascript
// ❌ 过大的快照
test('Large component', () => {
const tree = renderer.create(<LargeComponent />).toJSON();
expect(tree).toMatchSnapshot();
});
// ✅ 拆分为小组件
test('Component parts', () => {
const { header, content, footer } = renderComponent();
expect(header).toMatchSnapshot();
expect(content).toMatchSnapshot();
expect(footer).toMatchSnapshot();
});
5. 使用属性匹配器处理动态数据 #
javascript
test('Component with dynamic data', () => {
const tree = renderer.create(<Component />).toJSON();
expect(tree).toMatchSnapshot({
id: expect.any(String),
createdAt: expect.any(String),
updatedAt: expect.any(String),
});
});
Vue 组件快照 #
使用 Vue Test Utils #
javascript
import { mount } from '@vue/test-utils';
import Button from './Button.vue';
test('Button snapshot', () => {
const wrapper = mount(Button, {
props: {
label: 'Click me',
},
});
expect(wrapper.html()).toMatchSnapshot();
});
带插槽的组件 #
javascript
test('Button with slot', () => {
const wrapper = mount(Button, {
slots: {
default: 'Click me',
},
});
expect(wrapper.html()).toMatchSnapshot();
});
非 UI 快照 #
对象快照 #
javascript
test('config snapshot', () => {
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
};
expect(config).toMatchSnapshot();
});
错误消息快照 #
javascript
test('error message snapshot', () => {
const error = new ValidationError('Invalid email');
expect(error.message).toMatchSnapshot();
});
数据结构快照 #
javascript
test('data structure snapshot', () => {
const data = transformData(rawData);
expect(data).toMatchSnapshot();
});
快照文件管理 #
.snap 文件结构 #
javascript
// __snapshots__/Button.test.js.snap
exports[`Button renders correctly 1`] = `
<button
className="btn"
>
Click me
</button>
`;
exports[`Button renders with icon 1`] = `
<button
className="btn btn-icon"
>
<svg />
Click me
</button>
`;
版本控制 #
gitignore
# 不要忽略快照文件
# .gitignore
# __snapshots__/ # 不要这样做!
快照文件应该提交到版本控制。
常见问题 #
快照太大 #
javascript
// 问题:快照包含太多细节
test('too detailed', () => {
const tree = renderer.create(<Component />).toJSON();
expect(tree).toMatchSnapshot();
});
// 解决方案:只测试关键部分
test('key parts only', () => {
const tree = renderer.create(<Component />).toJSON();
expect(tree.children[0]).toMatchSnapshot();
});
快照频繁变化 #
javascript
// 问题:包含动态数据
test('with timestamp', () => {
const tree = renderer.create(<Component date={new Date()} />).toJSON();
expect(tree).toMatchSnapshot();
});
// 解决方案:使用固定数据
test('with fixed date', () => {
const fixedDate = new Date('2024-01-01');
const tree = renderer.create(<Component date={fixedDate} />).toJSON();
expect(tree).toMatchSnapshot();
});
快照测试失败但不是 bug #
javascript
// 场景:有意修改了组件
// 1. 检查变更是否正确
// 2. 如果正确,更新快照
jest -u
快照测试 vs 其他测试 #
| 测试类型 | 用途 | 优点 | 缺点 |
|---|---|---|---|
| 快照测试 | UI 结构验证 | 快速、简单 | 可能过于脆弱 |
| 单元测试 | 逻辑验证 | 精确、可靠 | 需要更多代码 |
| 集成测试 | 功能验证 | 更真实 | 较慢、复杂 |
下一步 #
现在你已经掌握了 Jest 快照测试,接下来学习 DOM 测试 学习如何测试 DOM 交互!
最后更新:2026-03-28