Jest 快照测试 #
快照测试概述 #
快照测试是一种测试技术,它会记录组件的输出,并在后续测试中比较输出是否发生变化。
text
┌─────────────────────────────────────────────────────────────┐
│ 快照测试流程 │
├─────────────────────────────────────────────────────────────┤
│ 1. 首次运行 - 生成快照文件 │
│ 2. 后续运行 - 比较当前输出与快照 │
│ 3. 发现变化 - 测试失败,提示更新 │
│ 4. 确认更新 - 更新快照文件 │
└─────────────────────────────────────────────────────────────┘
基本使用 #
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();
});
生成的快照文件 #
javascript
// __snapshots__/Button.test.js.snap
exports[`Button snapshot 1`] = `
<button
className="btn"
>
Click me
</button>
`;
React 组件快照 #
基本组件 #
javascript
import renderer from 'react-test-renderer';
import Button from './Button';
describe('Button', () => {
test('renders correctly', () => {
const tree = renderer.create(<Button>Click me</Button>).toJSON();
expect(tree).toMatchSnapshot();
});
test('renders with variant', () => {
const tree = renderer.create(<Button variant="primary">Primary</Button>).toJSON();
expect(tree).toMatchSnapshot();
});
test('renders disabled', () => {
const tree = renderer.create(<Button disabled>Disabled</Button>).toJSON();
expect(tree).toMatchSnapshot();
});
});
带状态的组件 #
javascript
import renderer from 'react-test-renderer';
import Counter from './Counter';
test('Counter snapshot', () => {
const tree = renderer.create(<Counter initialCount={0} />).toJSON();
expect(tree).toMatchSnapshot();
});
test('Counter with interaction', () => {
const component = renderer.create(<Counter initialCount={0} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
// 触发点击
tree.props.onClick();
// 更新后的快照
expect(component.toJSON()).toMatchSnapshot();
});
带 Props 的组件 #
javascript
import renderer from 'react-test-renderer';
import UserCard from './UserCard';
test('UserCard snapshot', () => {
const user = {
id: 1,
name: 'John Doe',
email: 'john@example.com',
avatar: 'https://example.com/avatar.jpg',
};
const tree = renderer.create(<UserCard user={user} />).toJSON();
expect(tree).toMatchSnapshot();
});
内联快照 #
toMatchInlineSnapshot #
快照直接写在测试文件中:
javascript
test('inline snapshot', () => {
const user = { name: 'John' };
expect(user).toMatchInlineSnapshot(`
{
"name": "John"
}
`);
});
React 组件内联快照 #
javascript
import renderer from 'react-test-renderer';
import Button from './Button';
test('Button inline snapshot', () => {
const tree = renderer.create(<Button>Click</Button>).toJSON();
expect(tree).toMatchInlineSnapshot(`
<button>
Click
</button>
`);
});
属性匹配器 #
动态属性处理 #
对于动态生成的属性(如 ID、时间戳),使用属性匹配器:
javascript
import renderer from 'react-test-renderer';
import UserCard from './UserCard';
test('UserCard with dynamic props', () => {
const user = {
id: Math.random(),
name: 'John',
createdAt: new Date(),
};
const tree = renderer.create(<UserCard user={user} />).toJSON();
expect(tree).toMatchSnapshot({
props: {
id: expect.any(Number),
createdAt: expect.any(String),
},
});
});
自定义匹配器 #
javascript
expect.addSnapshotSerializer({
test: (val) => val && val.isMoment,
serialize: (val) => `Date(${val.toISOString()})`,
});
test('date snapshot', () => {
const data = {
date: moment('2024-01-01'),
};
expect(data).toMatchSnapshot();
});
快照更新 #
交互式更新 #
bash
# 运行测试并提示更新
jest --updateSnapshot
# 或简写
jest -u
更新特定快照 #
bash
# 只更新失败的快照
jest --updateSnapshot --testNamePattern="Button"
审查模式 #
bash
# 交互式审查快照变化
jest --interactive
快照最佳实践 #
1. 保持快照简洁 #
javascript
// ❌ 太多细节
expect(component.debug()).toMatchSnapshot();
// ✅ 只关注关键部分
expect(component.toJSON()).toMatchSnapshot();
2. 使用描述性名称 #
javascript
// ❌ 不好的命名
test('snapshot 1', () => {});
// ✅ 好的命名
test('renders primary button', () => {});
test('renders disabled button', () => {});
3. 测试不同状态 #
javascript
describe('Button', () => {
test('renders default button', () => {
const tree = renderer.create(<Button>Default</Button>).toJSON();
expect(tree).toMatchSnapshot();
});
test('renders primary button', () => {
const tree = renderer.create(<Button variant="primary">Primary</Button>).toJSON();
expect(tree).toMatchSnapshot();
});
test('renders disabled button', () => {
const tree = renderer.create(<Button disabled>Disabled</Button>).toJSON();
expect(tree).toMatchSnapshot();
});
test('renders loading button', () => {
const tree = renderer.create(<Button loading>Loading</Button>).toJSON();
expect(tree).toMatchSnapshot();
});
});
4. 避免大快照 #
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();
});
Vue 组件快照 #
使用 vue-test-utils #
javascript
import { mount } from '@vue/test-utils';
import Button from './Button.vue';
test('Button snapshot', () => {
const wrapper = mount(Button, {
slots: {
default: 'Click me',
},
});
expect(wrapper.html()).toMatchSnapshot();
});
带 Props 的组件 #
javascript
import { mount } from '@vue/test-utils';
import UserCard from './UserCard.vue';
test('UserCard snapshot', () => {
const wrapper = mount(UserCard, {
props: {
user: {
id: 1,
name: 'John Doe',
},
},
});
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('user data snapshot', () => {
const user = {
id: 1,
name: 'John',
roles: ['admin', 'user'],
settings: {
theme: 'dark',
notifications: true,
},
};
expect(user).toMatchSnapshot();
});
错误消息快照 #
javascript
test('error message snapshot', () => {
const error = new ValidationError('Invalid email');
expect(error.message).toMatchSnapshot();
});
快照序列化 #
自定义序列化器 #
javascript
// jest.config.js
module.exports = {
snapshotSerializers: ['my-serializer-module'],
};
// my-serializer-module.js
module.exports = {
test: (val) => val && val.isCustomType,
serialize: (val, config, indentation, depth, refs) => {
return `CustomType(${val.value})`;
},
};
使用 enzyme-to-json #
javascript
import { mount } from 'enzyme';
import toJson from 'enzyme-to-json';
import Button from './Button';
test('Button with enzyme', () => {
const wrapper = mount(<Button>Click me</Button>);
expect(toJson(wrapper)).toMatchSnapshot();
});
快照文件管理 #
文件结构 #
text
__snapshots__/
├── Button.test.js.snap
├── Header.test.js.snap
└── UserCard.test.js.snap
src/
├── components/
│ ├── Button.jsx
│ ├── Button.test.jsx
│ ├── Header.jsx
│ └── Header.test.jsx
快照文件内容 #
javascript
// __snapshots__/Button.test.js.snap
exports[`Button renders default button 1`] = `
<button
className="btn"
>
Default
</button>
`;
exports[`Button renders primary button 1`] = `
<button
className="btn btn-primary"
>
Primary
</button>
`;
常见问题 #
快照太大 #
javascript
// 问题:快照文件太大
// 解决方案:拆分测试,使用更具体的断言
// ❌ 不好的做法
expect(wrapper.html()).toMatchSnapshot();
// ✅ 好的做法
expect(wrapper.find('.title').text()).toBe('Expected Title');
expect(wrapper.find('.button').exists()).toBe(true);
快照频繁变化 #
javascript
// 问题:动态数据导致快照频繁变化
// 解决方案:使用属性匹配器或 Mock
test('snapshot with dynamic data', () => {
jest.spyOn(Date, 'now').mockReturnValue(1234567890);
const tree = renderer.create(<Timestamp />).toJSON();
expect(tree).toMatchSnapshot();
});
快照审查 #
bash
# 审查快照变化
git diff --name-only HEAD~1 | grep '\.snap$'
# 查看具体变化
git diff HEAD~1 -- '*.snap'
CI 集成 #
GitHub Actions #
yaml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm test
防止意外更新 #
yaml
# CI 中使用 --ci 标志
- run: npm test -- --ci
下一步 #
现在你已经掌握了 Jest 快照测试,接下来学习 DOM 测试 学习 DOM 操作测试!
最后更新:2026-03-28