跳至主内容
版本:29.7

快照测试

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

快照测试是确保 UI 不会发生意外变更的实用工具。

典型的快照测试用例会渲染 UI 组件、生成快照,然后将其与测试文件旁存储的基准快照进行比对。如果两个快照不匹配,测试将失败:可能是意外变更,也可能是基准快照需要更新到 UI 组件的新版本。

使用 Jest 进行快照测试

测试 React 组件可采用类似方法。无需渲染图形化 UI(这需要构建整个应用),您可以使用测试渲染器快速生成 React 树的可序列化值。参考这个 Link 组件示例测试

import renderer from 'react-test-renderer';
import Link from '../Link';

it('renders correctly', () => {
const tree = renderer
.create(<Link page="http://www.facebook.com">Facebook</Link>)
.toJSON();
expect(tree).toMatchSnapshot();
});

首次运行此测试时,Jest 会创建如下所示的快照文件

exports[`renders correctly 1`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;

快照产物应与代码变更一同提交,并作为代码审查流程的一部分进行评审。Jest 使用 pretty-format 使快照在代码审查时具备良好可读性。在后续测试运行中,Jest 会将渲染输出与之前的快照进行比对。若匹配则测试通过;若不匹配,可能是测试运行器发现了代码错误(本例中的 <Link> 组件),或是实现已变更需要更新快照。

备注

快照范围严格限定于您渲染的数据——本例中是传入 page 属性的 <Link> 组件。这意味着即使其他文件(例如 App.js)的 <Link> 组件缺少属性,测试仍会通过,因为测试并不知晓 <Link> 组件的使用场景,其作用域仅限于 Link.js。此外,在其他快照测试中使用不同属性渲染相同组件也不会影响当前测试,因为测试之间相互独立。

信息

关于快照测试工作原理及设计初衷的更多信息,请参阅发布博客文章。建议阅读这篇博客文章了解适用场景,并观看此egghead 视频了解 Jest 快照测试。

更新快照

当快照测试因代码缺陷失败时,问题定位通常很直观。此时应修复问题并确保测试通过。现在讨论因预期变更导致快照测试失败的情况。

例如我们主动修改示例中 Link 组件指向的地址时:

// Updated test case with a Link to a different address
it('renders correctly', () => {
const tree = renderer
.create(<Link page="http://www.instagram.com">Instagram</Link>)
.toJSON();
expect(tree).toMatchSnapshot();
});

此时 Jest 将输出:

由于我们刚刚更新组件指向新地址,预期该组件的快照会发生变化。测试失败是因为更新后组件的快照不再匹配当前测试用例的快照基准。

解决方法需要更新快照基准。您可以通过带标志运行 Jest 来重新生成快照:

jest --updateSnapshot

请运行上述命令接受更改。您也可以使用等效的单字符 -u 标志重新生成快照(如果您更倾向此方式)。这将为所有失败的快照测试重新生成快照文件。如果存在因意外错误导致的其他失败快照测试,我们需要在重新生成快照前修复错误,避免记录错误行为的快照。

如需限制重新生成的快照测试用例范围,可添加 --testNamePattern 标志,仅重新生成匹配指定模式的测试用例快照。

您可以通过克隆快照示例,修改其中的 Link 组件并运行 Jest 来体验此功能。

交互式快照模式

失败的快照也可以在监视模式下交互式更新:

进入交互式快照模式后,Jest 会逐个引导您查看失败的快照,并让您有机会审查失败输出。

在此界面可选择更新当前快照或跳至下一项:

操作完成后,Jest 会在返回监视模式前显示摘要:

内联快照

内联快照的行为与外部快照(.snap 文件)完全相同,区别在于快照值会自动写回源代码。这意味着您无需切换至外部文件验证正确值,即可享受自动生成快照的便利。

示例:

首先编写测试,调用无参数的 .toMatchInlineSnapshot()

it('renders correctly', () => {
const tree = renderer
.create(<Link page="https://example.com">Example Site</Link>)
.toJSON();
expect(tree).toMatchInlineSnapshot();
});

下次运行 Jest 时,tree 会被执行求值,生成的快照将作为参数写入 toMatchInlineSnapshot

it('renders correctly', () => {
const tree = renderer
.create(<Link page="https://example.com">Example Site</Link>)
.toJSON();
expect(tree).toMatchInlineSnapshot(`
<a
className="normal"
href="https://example.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Example Site
</a>
`);
});

操作完成!您甚至可以使用 --updateSnapshot 或在 --watch 模式下按 u 键更新快照。

默认情况下,Jest 负责将快照写入源代码。但如果项目中使用了 prettier,Jest 会检测并委托 prettier 处理该操作(同时遵守您的配置)。

属性匹配器

当快照对象包含动态生成的字段(如 ID 和日期)时,尝试快照这些对象会导致每次测试运行失败:

it('will fail every time', () => {
const user = {
createdAt: new Date(),
id: Math.floor(Math.random() * 20),
name: 'LeBron James',
};

expect(user).toMatchSnapshot();
});

// Snapshot
exports[`will fail every time 1`] = `
{
"createdAt": 2018-05-19T23:36:09.816Z,
"id": 3,
"name": "LeBron James",
}
`;

对此类情况,Jest 允许为任何属性提供非对称匹配器。这些匹配器会在快照写入或测试前进行检查,并替代实际值保存到快照文件中:

it('will check the matchers and pass', () => {
const user = {
createdAt: new Date(),
id: Math.floor(Math.random() * 20),
name: 'LeBron James',
};

expect(user).toMatchSnapshot({
createdAt: expect.any(Date),
id: expect.any(Number),
});
});

// Snapshot
exports[`will check the matchers and pass 1`] = `
{
"createdAt": Any<Date>,
"id": Any<Number>,
"name": "LeBron James",
}
`;

所有非匹配器的给定值都会进行精确检查并存入快照:

it('will check the values and pass', () => {
const user = {
createdAt: new Date(),
name: 'Bond... James Bond',
};

expect(user).toMatchSnapshot({
createdAt: expect.any(Date),
name: 'Bond... James Bond',
});
});

// Snapshot
exports[`will check the values and pass 1`] = `
{
"createdAt": Any<Date>,
"name": 'Bond... James Bond',
}
`;
技巧

若需处理字符串而非对象,您需要在测试快照前自行替换字符串中的随机部分。
可使用 replace()正则表达式实现:

const randomNumber = Math.round(Math.random() * 100);
const stringWithRandomData = `<div id="${randomNumber}">Lorem ipsum</div>`;
const stringWithConstantData = stringWithRandomData.replace(/id="\d+"/, 123);
expect(stringWithConstantData).toMatchSnapshot();

其他解决方案包括使用快照序列化器模拟生成快照代码随机部分的库。

最佳实践

快照是识别应用程序中意外界面变更的绝佳工具——无论这个界面是 API 响应、用户界面、日志还是错误信息。与任何测试策略一样,为了有效使用快照,您需要了解一些最佳实践并遵循相关准则。

1. 将快照视为代码

提交快照并将其纳入常规代码审查流程。这意味着您应该像对待项目中其他类型的测试或代码一样对待快照。

通过保持快照内容聚焦、简短,并使用强制实施这些风格规范的工具,确保快照的可读性。

如前所述,Jest 使用 pretty-format 使快照易于阅读。但您可能会发现引入额外工具很有帮助,例如启用 no-large-snapshots 选项的 eslint-plugin-jest,或具备组件快照比对功能的 snapshot-diff,这些工具能促进提交简短聚焦的断言。

这样做的目标是便于在拉取请求中审查快照,并避免在测试套件失败时直接重新生成快照而不检查根本原因的习惯。

2. 测试应具备确定性

您的测试必须具有确定性。对未变更的组件重复运行相同测试时,每次都应产生相同结果。您需要确保生成的快照不包含平台特定数据或其他非确定性数据。

例如,若您的 Clock 组件使用了 Date.now(),每次运行测试时生成的快照都会不同。此时我们可以模拟 Date.now() 方法,使其在每次测试运行时返回固定值:

Date.now = jest.fn(() => 1_482_363_367_071);

现在每次快照测试运行时,Date.now() 都将固定返回 1482363367071。这样无论何时运行测试,该组件生成的快照都会保持一致。

3. 使用描述性快照名称

始终为快照使用描述性的测试名称和/或快照名称。最佳命名应体现预期的快照内容,这便于审查者在评审时验证快照,也有助于任何人在更新前判断过期快照是否仍符合预期行为。

例如,比较以下两种命名:

exports[`<UserName /> should handle some test case`] = `null`;

exports[`<UserName /> should handle some other test case`] = `
<div>
Alan Turing
</div>
`;

与:

exports[`<UserName /> should render null`] = `null`;

exports[`<UserName /> should render Alan Turing`] = `
<div>
Alan Turing
</div>
`;

后者明确描述了输出预期,因此能更清晰地发现错误:

exports[`<UserName /> should render null`] = `
<div>
Alan Turing
</div>
`;

exports[`<UserName /> should render Alan Turing`] = `null`;

常见问题解答

持续集成 (CI) 系统会自动写入快照吗?

不会。从 Jest 20 开始,在未显式传递 --updateSnapshot 参数的情况下,Jest 在 CI 系统中运行时不会自动写入快照。所有快照都应属于在 CI 上运行的代码部分,由于新快照会自动通过测试,它们不应在 CI 系统上通过测试运行。我们建议始终提交所有快照并将其纳入版本控制。

快照文件需要提交吗?

是的,所有快照文件都应与其覆盖的模块及对应测试一同提交。它们应被视为测试的一部分,类似于 Jest 中任何其他断言的值。实际上,快照代表了源代码模块在特定时间点的状态。这样当源代码模块被修改时,Jest 可以识别出与之前版本的差异。在代码审查期间,快照还能提供丰富的额外上下文,帮助审查者更好地理解您的变更。

快照测试仅适用于 React 组件吗?

ReactReact Native 组件是快照测试的理想应用场景。不过,快照可以捕获任何可序列化的值,只要测试目标是验证输出是否正确,就应使用快照测试。Jest 代码库中包含许多测试 Jest 自身输出的示例,包括 Jest 断言库的输出结果以及代码库各部分的日志信息。请参阅 Jest 仓库中的 CLI 输出快照示例

快照测试与视觉回归测试有何区别?

快照测试和视觉回归测试是两种不同的 UI 测试方法,服务于不同目的。视觉回归测试工具会对网页截图,并逐像素比较生成的图像;而快照测试则将值序列化后存储在文本文件中,使用差异比较算法进行比对。两者存在不同的权衡取舍,我们在 Jest 博客中阐述了开发快照测试的原因。

快照测试会取代单元测试吗?

快照测试只是 Jest 内置的 20 多种断言之一,其目的并非取代现有单元测试,而是提供额外价值并使测试更轻松。在某些场景下,快照测试可能消除对特定功能集(例如 React 组件)进行单元测试的需求,但它们也可以协同工作。

快照测试在速度和生成文件大小方面表现如何?

Jest 在设计时已充分考虑性能,快照测试也不例外。由于快照存储在文本文件中,这种测试方式快速可靠。Jest 会为每个调用 toMatchSnapshot 匹配器的测试文件生成新快照文件。快照文件体积很小:作为参考,Jest 自身代码库中所有快照文件的总大小不足 300 KB。

如何解决快照文件中的冲突?

快照文件必须始终反映其所覆盖模块的当前状态。因此,当合并两个分支遇到快照文件冲突时,您可以选择手动解决冲突,或通过运行 Jest 并检查结果来更新快照文件。

能否在快照测试中应用测试驱动开发(TDD)原则?

虽然可以手动编写快照文件,但这通常不可行。快照的作用是检测测试覆盖模块的输出是否发生变化,而非在代码设计阶段提供指导。

快照测试支持代码覆盖率统计吗?

是的,与其他测试类型完全兼容。