Vitest 快照测试 #
快照测试概述 #
快照测试是一种测试方法,它将组件或数据的输出保存为"快照",在后续测试中比较当前输出与快照是否一致。这种方法特别适合测试 UI 组件、大型对象和复杂数据结构。
快照测试的工作流程 #
text
┌─────────────────────────────────────────────────────────────┐
│ 快照测试工作流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 首次运行: │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 测试代码 │ -> │ 生成输出 │ -> │ 保存快照 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 后续运行: │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 测试代码 │ -> │ 生成输出 │ -> │ 比较快照 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │ │
│ │ 相同? 不同? │
│ │ │ │ │
│ │ 测试通过 测试失败 │
│ │ (需确认是否更新) │
│ │
└─────────────────────────────────────────────────────────────┘
基本用法 #
toMatchSnapshot() #
创建和比较快照:
typescript
import { expect, test } from 'vitest'
test('snapshot basic', () => {
const user = {
id: 1,
name: 'John Doe',
email: 'john@example.com',
}
// 首次运行:创建快照
// 后续运行:比较快照
expect(user).toMatchSnapshot()
})
快照文件 #
快照保存在 __snapshots__ 目录:
text
project/
├── src/
│ └── user.test.ts
└── __snapshots__/
└── user.test.ts.snap
快照文件内容 #
snap
// __snapshots__/user.test.ts.snap
// Vitest Snapshot v1
exports[`snapshot basic 1`] = `
{
"id": 1,
"name": "John Doe",
"email": "john@example.com"
}
`;
内联快照 #
toMatchInlineSnapshot() #
将快照直接写在测试代码中:
typescript
import { expect, test } from 'vitest'
test('inline snapshot', () => {
const user = {
id: 1,
name: 'John Doe',
}
// 首次运行:自动填充快照
// 后续运行:比较快照
expect(user).toMatchInlineSnapshot(`
{
"id": 1,
"name": "John Doe"
}
`)
})
内联快照的优势 #
| 特性 | toMatchSnapshot | toMatchInlineSnapshot |
|---|---|---|
| 快照位置 | 独立文件 | 测试代码内 |
| 代码审查 | 需要查看快照文件 | 直接在测试中看到 |
| 适用场景 | 大型快照 | 小型快照 |
| 可维护性 | 集中管理 | 分散管理 |
属性匹配器 #
匹配动态值 #
对于包含动态值(如时间戳、ID)的对象:
typescript
import { expect, test } from 'vitest'
test('dynamic values', () => {
const user = {
id: Math.random(),
createdAt: new Date(),
name: 'John Doe',
}
// 使用属性匹配器
expect(user).toMatchSnapshot({
id: expect.any(Number),
createdAt: expect.any(Date),
})
})
内联快照中的属性匹配器 #
typescript
import { expect, test } from 'vitest'
test('inline dynamic values', () => {
const user = {
id: Math.floor(Math.random() * 1000),
createdAt: new Date(),
name: 'John Doe',
}
expect(user).toMatchInlineSnapshot(
{
id: expect.any(Number),
createdAt: expect.any(Date),
},
`
{
"createdAt": Any<Date>,
"id": Any<Number>,
"name": "John Doe"
}
`
)
})
更新快照 #
更新所有快照 #
bash
# 更新所有快照
vitest -u
# 或
vitest --update
更新特定快照 #
bash
# 只运行特定文件的快照
vitest -u user.test.ts
交互式更新 #
bash
# Watch 模式下交互式更新
vitest --watch
# 按键操作
# u - 更新当前快照
# i - 更新失败的快照
快照测试组件 #
Vue 组件快照 #
typescript
import { expect, test } from 'vitest'
import { mount } from '@vue/test-utils'
import MyComponent from './MyComponent.vue'
test('Vue component snapshot', () => {
const wrapper = mount(MyComponent, {
props: {
title: 'Hello World',
},
})
expect(wrapper.html()).toMatchSnapshot()
})
React 组件快照 #
typescript
import { expect, test } from 'vitest'
import { render } from '@testing-library/react'
import MyComponent from './MyComponent'
test('React component snapshot', () => {
const { container } = render(<MyComponent title="Hello World" />)
expect(container.firstChild).toMatchSnapshot()
})
Svelte 组件快照 #
typescript
import { expect, test } from 'vitest'
import { render } from '@testing-library/svelte'
import MyComponent from './MyComponent.svelte'
test('Svelte component snapshot', () => {
const { container } = render(MyComponent, {
props: { title: 'Hello World' },
})
expect(container.innerHTML).toMatchSnapshot()
})
快照序列化 #
自定义序列化器 #
typescript
import { expect, test, vi } from 'vitest'
// 添加自定义序列化器
expect.addSnapshotSerializer({
test(value) {
return value && value.isCustom
},
serialize(value, config, indentation, depth, refs, printer) {
return `Custom: ${value.name}`
},
})
test('custom serializer', () => {
const custom = {
isCustom: true,
name: 'MyCustomObject',
}
expect(custom).toMatchSnapshot()
})
格式化选项 #
typescript
// vitest.config.ts
import { defineConfig } from 'vitest'
export default defineConfig({
test: {
snapshotFormat: {
escapeString: true,
printBasicPrototype: true,
indent: 2,
},
},
})
快照最佳实践 #
1. 测试有意义的内容 #
typescript
import { expect, test } from 'vitest'
// 好的做法:测试渲染输出
test('renders correctly', () => {
const html = renderComponent()
expect(html).toMatchSnapshot()
})
// 不好的做法:测试实现细节
test('implementation details', () => {
const component = createComponent()
expect(component._internalState).toMatchSnapshot()
})
2. 使用描述性名称 #
typescript
import { expect, test } from 'vitest'
// 好的做法
test('renders user profile with avatar', () => {
expect(renderUserProfile()).toMatchSnapshot()
})
test('renders user profile without avatar', () => {
expect(renderUserProfile({ avatar: null })).toMatchSnapshot()
})
// 不好的做法
test('snapshot 1', () => {
expect(renderUserProfile()).toMatchSnapshot()
})
3. 避免过大的快照 #
typescript
import { expect, test } from 'vitest'
// 好的做法:测试特定部分
test('user name is displayed', () => {
const { getByText } = render(<UserCard user={user} />)
expect(getByText(user.name)).toBeDefined()
})
// 不好的做法:整个组件快照
test('user card', () => {
const { container } = render(<UserCard user={user} />)
expect(container).toMatchSnapshot() // 太大,难以维护
})
4. 使用属性匹配器处理动态数据 #
typescript
import { expect, test } from 'vitest'
test('timestamp is handled', () => {
const data = {
id: 1,
timestamp: Date.now(),
value: 'test',
}
expect(data).toMatchSnapshot({
timestamp: expect.any(Number),
})
})
5. 定期审查快照 #
typescript
// 在代码审查时,检查快照的变化
// 确保变化是预期的,而不是意外的 bug
快照文件管理 #
快照文件位置 #
typescript
// vitest.config.ts
import { defineConfig } from 'vitest'
export default defineConfig({
test: {
// 自定义快照目录
snapshotFormat: {
// 格式化选项
},
},
})
清理未使用的快照 #
bash
# 清理未使用的快照
vitest --update --run
高级用法 #
多个快照 #
typescript
import { expect, test } from 'vitest'
test('multiple snapshots', () => {
const users = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
]
// 每个快照需要唯一的名称
expect(users[0]).toMatchSnapshot('user 1')
expect(users[1]).toMatchSnapshot('user 2')
})
条件快照 #
typescript
import { expect, test } from 'vitest'
test('conditional snapshot', () => {
const data = process.env.CI ? { mode: 'production' } : { mode: 'development' }
expect(data).toMatchSnapshot()
})
快照与 Mock 结合 #
typescript
import { expect, test, vi } from 'vitest'
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Mock User' }),
}))
test('snapshot with mock', async () => {
const { fetchUser } = await import('./api')
const user = await fetchUser(1)
expect(user).toMatchSnapshot()
})
快照测试的局限性 #
不适合的场景 #
- 频繁变化的数据:时间戳、随机数
- 大型复杂对象:难以审查
- 实现细节:应该测试行为而非实现
替代方案 #
typescript
import { expect, test } from 'vitest'
// 对于频繁变化的数据,使用具体断言
test('timestamp is recent', () => {
const data = { timestamp: Date.now() }
expect(data.timestamp).toBeGreaterThan(Date.now() - 1000)
})
// 对于大型对象,测试关键属性
test('user has required fields', () => {
const user = createUser()
expect(user).toHaveProperty('id')
expect(user).toHaveProperty('name')
expect(user).toHaveProperty('email')
})
快照命令速查表 #
| 命令 | 说明 |
|---|---|
vitest -u |
更新所有快照 |
vitest --update |
更新所有快照 |
vitest -u file.test.ts |
更新特定文件快照 |
vitest --watch |
交互式更新 |
快照 API 速查表 #
| 方法 | 说明 |
|---|---|
toMatchSnapshot() |
创建/比较快照 |
toMatchSnapshot(name) |
命名快照 |
toMatchSnapshot(matchers) |
属性匹配器 |
toMatchInlineSnapshot() |
内联快照 |
toMatchInlineSnapshot(snapshot) |
内联快照(带内容) |
toMatchInlineSnapshot(matchers, snapshot) |
内联快照(带匹配器) |
下一步 #
现在你已经掌握了快照测试,接下来学习 异步测试 了解如何测试异步代码!
最后更新:2026-03-28