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