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