快照测试
快照测试会捕获一段代码的输出并将其保存到文件中。后续运行时,会将输出与保存的快照进行比较。如果输出发生变化,测试就会失败。这种变化要么是 Bug,要么需要更新快照。
这种方法特别适用于测试会产生结构化输出的内容:一个返回复杂对象的函数、渲染 HTML 的组件,或生成多行消息的错误格式化程序。为每个字段或行编写手动断言会非常繁琐且脆弱。相反,你只需捕获一次完整输出,然后让 Vitest 判断它是否发生变化。
你的第一个快照
要创建快照测试,只需向 toMatchSnapshot() 传递一个值:
import { expect, test } from 'vitest'
function generateGreeting(name) {
return {
message: `Hello, ${name}!`,
timestamp: null,
version: 2,
}
}
test('generates a greeting', () => {
expect(generateGreeting('Alice')).toMatchSnapshot()
})第一次运行此测试时,没有现有的快照可供比较,因此 Vitest 会创建一个。它将快照存储在测试文件旁边的 __snapshots__ 目录中:
__snapshots__/
example.test.js.snap如果你打开该文件,会看到一个序列化后的值表示:
exports['generates a greeting 1'] = `
{
"message": "Hello, Alice!",
"timestamp": null,
"version": 2,
}
`从现在起,每次运行此测试时,Vitest 都会序列化 generateGreeting('Alice') 的输出,并逐字符地与存储的快照进行比较。如果输出发生变化(例如有人修改了消息格式或提升了版本号),测试就会失败,并显示清晰的变更差异。
TIP
提交你的快照文件到版本控制。它们作为预期输出的记录,应该像其他测试断言一样在代码审查中被检查。
内联快照
外部快照文件虽然好用,但你必须跳转到另一个文件才能查看预期输出的实际样子。对于较小的值,使用 toMatchInlineSnapshot() 将快照直接保留在测试文件中通常更方便。
首先在不带参数的情况下编写断言:
test('generates a greeting', () => {
expect(generateGreeting('Alice')).toMatchInlineSnapshot()
})运行测试时,Vitest 会自动填充快照作为字符串参数:
test('generates a greeting', () => {
expect(generateGreeting('Alice')).toMatchInlineSnapshot(`
{
"message": "Hello, Alice!",
"timestamp": null,
"version": 2,
}
`)
})现在预期输出就在产生它的代码旁边。你可以立即理解 generateGreeting 的预期返回值。当输出发生变化时,Vitest 会就地更新字符串,这样你就不需要管理单独的快照文件。
内联快照非常适合小型、聚焦的值。对于大型输出(如完整的 HTML 页面),外部快照或文件快照更合适。
TIP
与外部快照不同,内联快照不会创建单独的 .snap 文件。预期值会直接作为 toMatchInlineSnapshot() 的参数存储在你的测试文件中,因此无需额外提交任何内容。
Updating Snapshots
当你有意更改代码的输出时,现有快照会过时,测试会失败。这是设计如此;这正是快照测试的核心目的。但一旦你确认新输出是正确的,就需要更新快照。
有多种方法可以实现:
- 在观察模式中:在终端中按
u以更新所有失败快照 - 通过 CLI:运行
vitest -u或vitest --update来更新快照并退出 - 在 VS Code 中:使用测试边距图标上的“更新快照”命令(来自 Vitest 扩展)
vitest -u对于内联快照,Vitest 会直接修改测试文件中的新值。对于外部快照,它会重写 .snap 文件。
WARNING
更新快照时要小心。始终检查差异以确认更改是预期的,而不是 Bug。切勿盲目按 u 而不小心接受了错误的输出。
文件快照
有时你要测试的输出足够大,以至于外部 .snap 文件感觉很笨拙,或者你希望在编辑器中用合适的语法高亮查看快照。[toMatchFileSnapshot()](/api/expect#tomatchfilesnapshot) 允许你将快照保存为你想要的任何扩展名的文件:
test('renders the component', async () => {
const html = renderComponent()
await expect(html).toMatchFileSnapshot('./fixtures/component.html')
})快照存储为一个普通的 .html 文件,你可以直接在浏览器中打开、用语法高亮查看,或用标准工具进行差异比较。这适用于 HTML、SVG、CSS、生成的代码,或任何文件格式对可读性很重要的输出。
何时使用快照
当你在处理结构化、可序列化的输出,而手动断言会非常痛苦时,快照最为理想。一些常见场景:
- 一个返回具有许多嵌套字段的复杂配置对象的函数
- 由渲染函数或模板引擎生成的 HTML 或标记
- 包含格式化堆栈跟踪或上下文信息的错误消息
- 具有特定格式的 CLI 输出或日志消息
- 你希望捕获任何意外字段变化的 JSON API 响应
另一方面,快照并不总是最佳工具。如果输出经常变化(例如包含时间戳或随机 ID),你将花费更多时间更新快照,而不是它们为你节省的时间。而且,如果你只关心一两个特定字段,像 [toMatchObject](/api/expect#tomatchobject) 或 [toHaveProperty](/api/expect#tohaveproperty) 这样的目标断言比捕获所有内容的快照更清晰地表达你的意图。
一般规则是:当你希望保护输出的任何变化时使用快照,而当你只关心特定属性时使用目标断言。
处理动态值
如果你的输出包含每次运行都会变化的值(例如时间戳或 ID),你可以使用属性匹配器来固定结构,同时忽略易变字段。将带有非对称匹配器的对象作为 toMatchSnapshot() 或 toMatchInlineSnapshot() 的第一个参数传入:
test('user snapshot with dynamic fields', () => {
const user = createUser('Alice')
expect(user).toMatchSnapshot({
id: expect.any(Number),
createdAt: expect.any(Date),
})
})id 和 createdAt 字段会根据匹配器(任意数字、任意日期)进行检查,而不是与存储的值进行比较。其他所有字段都会像往常一样被保存为快照。
错误快照
内联快照的一种常见用法是捕获错误消息。toThrowErrorMatchingInlineSnapshot 将 toThrow 与 toMatchInlineSnapshot 结合起来,因此你可以在不使用单独 .snap 文件的情况下对错误消息进行快照:
test('throws on invalid input', () => {
expect(() => parse('')).toThrowErrorMatchingInlineSnapshot(
`[Error: Unexpected end of input at position 0]`
)
})这对于验证错误消息是否清晰且不会被意外更改特别有用。与其他内联快照一样,Vitest 会在第一次运行时填充字符串,并在你按下 u 时更新它。
TIP
有关自定义快照序列化程序、匹配器和高级配置,请参见 快照 指南。
