快照测试
每当你想要确保 UI 不会意外更改时,快照测试都是一个非常有用的工具。
典型的快照测试用例会渲染 UI 组件、拍摄快照,然后将其与与测试一起存储的参考快照文件进行比较。 如果两个快照不匹配,测试将失败: 更改是意外的,或者参考快照需要更新到 UI 组件的新版本。
使用 Jest 进行快照测试
在测试 React 组件时可以采用类似的方法。 你可以使用测试渲染器为 React 树快速生成可序列化的值,而不是渲染图形 UI(这需要构建整个应用)。 将此 示例测试 视为 链接组件:
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>
组件中缺少 props(例如 App.js
),它仍然会通过测试,因为测试不知道 <Link>
组件的用法,并且其范围仅限于 Link.js
。 此外,在其他快照测试中使用不同的 props 渲染相同的组件不会影响第一个测试,因为测试彼此不了解。
更新快照
在引入错误后,可以很容易地发现快照测试何时失败。 发生这种情况时,请继续解决问题并确保快照测试再次通过。 现在,我们来讨论一下由于有意的实现更改而导致快照测试失败的情况。
如果我们故意更改示例中链接组件指向的地址,则可能会出现一种这样的情况。
// 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
使快照易于理解,但你可能会发现引入其他工具很有用,例如带有 no-large-snapshots
选项的 eslint-plugin-jest
或带有组件快照比较功能的 snapshot-diff
,以促进提交简短、集中的断言。
目标是使审查拉取请求中的快照变得容易,并反对在测试套件失败时重新生成快照而不是检查失败的根本原因的习惯。
2. 测试应该是确定性的
你的测试应该是确定性的。 在未更改的组件上多次运行相同的测试每次都会产生相同的结果。 你有责任确保生成的快照不包含特定于平台的数据或其他不确定性数据。
例如,如果你有一个使用 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 开始,当 Jest 在 CI 系统中运行而未显式传递 --updateSnapshot
时,不会自动写入 Jest 中的快照。 预计所有快照都是在 CI 上运行的代码的一部分,并且由于新快照会自动通过,因此它们不应通过 CI 系统上的测试运行。 建议始终提交所有快照并将它们保留在版本控制中。
是否应该提交快照文件?
是的,所有快照文件都应该与它们所覆盖的模块及其测试一起提交。 它们应该被视为测试的一部分,类似于 Jest 中任何其他断言的值。 事实上,快照代表了源模块在任何给定时间点的状态。 这样,当源模块被修改时,Jest 可以知道与之前版本相比发生了什么变化。 它还可以在代码审查期间提供大量附加上下文,使审查者可以更好地研究你的更改。
快照测试只适用于 React 组件吗?
React 和 React Native 组件是快照测试的一个很好的用例。 但是,快照可以捕获任何可序列化的值,并且应该在目标是测试输出是否正确的任何时候使用。 Jest 存储库包含许多测试 Jest 本身的输出、Jest 断言库的输出以及来自 Jest 代码库各个部分的日志消息的示例。 请参阅 Jest 存储库中的 快照 CLI 输出 示例。
快照测试和视觉回归测试有什么区别?
快照测试和视觉回归测试是测试 UI 的两种不同方法,它们具有不同的目的。 视觉回归测试工具截取网页屏幕截图并逐像素比较结果图片。 通过快照,测试值被序列化,存储在文本文件中,并使用 diff 算法进行比较。 有不同的权衡需要考虑,我们列出了在 Jest 博客 中构建快照测试的原因。
快照测试会取代单元测试吗?
快照测试只是 Jest 附带的 20 多个断言之一。 快照测试的目的不是取代现有的单元测试,而是提供额外的价值并使测试变得轻松。 在某些情况下,快照测试可能会消除对一组特定功能(例如 React 组件)进行单元测试的需要,但它们也可以一起工作。
关于生成文件的速度和大小,快照测试的性能如何?
Jest 在重写时考虑到了性能,快照测试也不例外。 由于快照存储在文本文件中,因此这种测试方式快速且可靠。 Jest 为调用 toMatchSnapshot
匹配器的每个测试文件生成一个新文件。 快照的大小非常小: 作为参考,Jest 代码库本身中所有快照文件的大小均小于 300 KB。
如何解决快照文件中的冲突?
快照文件必须始终代表它们所覆盖的模块的当前状态。 因此,如果你在合并两个分支时遇到快照文件冲突,你可以手动解决冲突,也可以通过运行 Jest 并检查结果来更新快照文件。
是否可以通过快照测试应用测试驱动开发原则?
尽管可以手动写入快照文件,但这通常是不可实现的。 快照有助于确定测试所覆盖的模块的输出是否发生更改,而不是首先指导设计代码。
代码覆盖率是否适用于快照测试?
是的,以及任何其他测试。