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()
})

快照测试的局限性 #

不适合的场景 #

  1. 频繁变化的数据:时间戳、随机数
  2. 大型复杂对象:难以审查
  3. 实现细节:应该测试行为而非实现

替代方案 #

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