快照测试
只要您想确保 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>
组件),应该修复该错误,要么实现已更改,需要更新快照。
快照直接作用于您渲染的数据 - 在我们的示例中,<Link>
组件带有传递给它的 page
属性。这意味着即使任何其他文件在 <Link>
组件中缺少属性(例如,App.js
),它仍然会通过测试,因为测试不知道 <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 响应、UI、日志还是错误消息。与任何测试策略一样,您应该了解一些最佳实践,并遵循一些准则,以便有效地使用它们。
1. 将快照视为代码
提交快照并在常规代码审查过程中对其进行审查。这意味着将快照视为项目中的任何其他类型的测试或代码。
通过保持快照的重点、简短以及使用强制执行这些风格约定工具来确保快照的可读性。
如前所述,Jest 使用 pretty-format
使快照易于阅读,但您可能会发现引入其他工具(如 eslint-plugin-jest
及其 no-large-snapshots
选项,或 snapshot-diff
及其组件快照比较功能)有助于促进提交简短、重点突出的断言。
目标是简化拉取请求中快照的审查,并避免在测试套件失败时重新生成快照的习惯,而应检查其失败的根本原因。
2. 测试应该是确定性的
您的测试应该具有确定性。在未更改的组件上多次运行相同的测试应该始终产生相同的结果。您有责任确保生成的快照不包含特定于平台或其他非确定性数据。
例如,如果您有一个使用Date.now()
的Clock组件,则从该组件生成的快照每次运行测试用例时都会不同。在这种情况下,我们可以模拟 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 开始,Jest 中的快照不会在 Jest 在 CI 系统中运行时自动写入,除非显式传递--updateSnapshot
。预计所有快照都是 CI 上运行的代码的一部分,并且由于新的快照会自动通过,因此它们不应该通过 CI 系统上的测试运行。建议始终提交所有快照并将它们保存在版本控制中。
快照文件应该提交吗?
是的,所有快照文件都应该与它们覆盖的模块及其测试一起提交。它们应该被视为测试的一部分,类似于 Jest 中任何其他断言的值。实际上,快照代表了源模块在任何给定时间点的状态。这样,当源模块被修改时,Jest 可以知道与以前版本相比发生了什么变化。它还可以在代码审查期间提供大量额外的上下文,在代码审查期间,审阅者可以更好地研究您的更改。
快照测试是否只适用于 React 组件?
React 和 React Native 组件是快照测试的一个很好的用例。但是,快照可以捕获任何可序列化值,并且应该在目标是测试输出是否正确时使用。Jest 存储库包含许多测试 Jest 本身输出、Jest 断言库的输出以及 Jest 代码库各个部分的日志消息的示例。请参阅 Jest 存储库中快照 CLI 输出的示例。
快照测试和视觉回归测试有什么区别?
快照测试和视觉回归测试是测试 UI 的两种截然不同的方法,它们服务于不同的目的。视觉回归测试工具会截取网页的屏幕截图,并逐像素比较生成的图像。使用快照测试,值会被序列化,存储在文本文件中,并使用差异算法进行比较。需要考虑不同的权衡,我们在Jest 博客中列出了构建快照测试的原因。
快照测试是否取代了单元测试?
快照测试只是 Jest 附带的 20 多个断言之一。快照测试的目的是不是要取代现有的单元测试,而是要提供额外的价值并使测试变得轻松。在某些情况下,快照测试可能会消除对特定功能集(例如 React 组件)进行单元测试的需要,但它们也可以协同工作。
快照测试在速度和生成文件的规模方面性能如何?
Jest 已经从性能的角度进行了重写,快照测试也不例外。由于快照存储在文本文件中,因此这种测试方法既快速又可靠。Jest 为每个调用toMatchSnapshot
匹配器的测试文件生成一个新文件。快照的大小非常小:作为参考,Jest 代码库本身中所有快照文件的大小不到 300 KB。
如何解决快照文件中的冲突?
快照文件必须始终代表它们覆盖的模块的当前状态。因此,如果您合并两个分支并遇到快照文件中的冲突,您可以手动解决冲突,也可以通过运行 Jest 并检查结果来更新快照文件。
是否可以使用快照测试来应用测试驱动开发原则?
虽然可以手动编写快照文件,但这通常不可取。快照有助于确定测试覆盖的模块的输出是否发生了变化,而不是在设计代码时提供指导。
代码覆盖率是否适用于快照测试?
是的,以及任何其他测试。