快照测试
每当你想要确保 UI 不会意外更改时,快照测试都是一个非常有用的工具。
¥Snapshot tests are a very useful tool whenever you want to make sure your UI does not change unexpectedly.
典型的快照测试用例会渲染 UI 组件、拍摄快照,然后将其与与测试一起存储的参考快照文件进行比较。如果两个快照不匹配,测试将失败:更改是意外的,或者参考快照需要更新到 UI 组件的新版本。
¥A typical snapshot test case renders a UI component, takes a snapshot, then compares it to a reference snapshot file stored alongside the test. The test will fail if the two snapshots do not match: either the change is unexpected, or the reference snapshot needs to be updated to the new version of the UI component.
使用 Jest 进行快照测试
¥Snapshot Testing with Jest
在测试 React 组件时可以采用类似的方法。你可以使用测试渲染器为 React 树快速生成可序列化的值,而不是渲染图形 UI(这需要构建整个应用)。将此 示例测试 视为 链接组件:
¥A similar approach can be taken when it comes to testing your React components. Instead of rendering the graphical UI, which would require building the entire app, you can use a test renderer to quickly generate a serializable value for your React tree. Consider this example test for a Link component:
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 创建一个如下所示的 快照文件:
¥The first time this test is run, Jest creates a snapshot file that looks like this:
exports[`renders correctly 1`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;
快照工件应与代码更改一起提交,并作为代码审查过程的一部分进行审查。Jest 使用 pretty-format 使快照在代码审查期间易于阅读。在后续测试运行中,Jest 会将渲染的输出与之前的快照进行比较。如果它们匹配,测试就会通过。如果它们不匹配,则测试运行程序在你的代码中发现了应该修复的错误(在本例中为 <Link>
组件),或者实现已更改并且需要更新快照。
¥The snapshot artifact should be committed alongside code changes, and reviewed as part of your code review process. Jest uses pretty-format to make snapshots human-readable during code review. On subsequent test runs, Jest will compare the rendered output with the previous snapshot. If they match, the test will pass. If they don't match, either the test runner found a bug in your code (in the <Link>
component in this case) that should be fixed, or the implementation has changed and the snapshot needs to be updated.
快照的范围直接限定为你渲染的数据 - 在我们的示例中,<Link>
组件传递了 page
属性。这意味着即使任何其他文件在 <Link>
组件中缺少 props(例如 App.js
),它仍然会通过测试,因为测试不知道 <Link>
组件的用法,并且其范围仅限于 Link.js
。此外,在其他快照测试中使用不同的 props 渲染相同的组件不会影响第一个测试,因为测试彼此不了解。
¥The snapshot is directly scoped to the data you render – in our example the <Link>
component with page
prop passed to it. This implies that even if any other file has missing props (say, App.js
) in the <Link>
component, it will still pass the test as the test doesn't know the usage of <Link>
component and it's scoped only to the Link.js
. Also, rendering the same component with different props in other snapshot tests will not affect the first one, as the tests don't know about each other.
有关快照测试如何工作以及我们为何构建它的更多信息可以在 发布博文 上找到。我们建议阅读 这篇博文 以充分了解何时应该使用快照测试。我们还建议监视 书呆子视频 有关 Jest 快照测试的内容。
¥More information on how snapshot testing works and why we built it can be found on the release blog post. We recommend reading this blog post to get a good sense of when you should use snapshot testing. We also recommend watching this egghead video on Snapshot Testing with Jest.
更新快照
¥Updating Snapshots
在引入错误后,可以很容易地发现快照测试何时失败。发生这种情况时,请继续解决问题并确保快照测试再次通过。现在,我们来讨论一下由于有意的实现更改而导致快照测试失败的情况。
¥It's straightforward to spot when a snapshot test fails after a bug has been introduced. When that happens, go ahead and fix the issue and make sure your snapshot tests are passing again. Now, let's talk about the case when a snapshot test is failing due to an intentional implementation change.
如果我们故意更改示例中链接组件指向的地址,则可能会出现一种这样的情况。
¥One such situation can arise if we intentionally change the address the Link component in our example is pointing to.
// 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 将打印以下输出:
¥In that case, Jest will print this output:
由于我们刚刚更新了组件以指向不同的地址,因此可以合理地预期该组件的快照会发生变化。我们的快照测试用例失败,因为更新后的组件的快照不再与该测试用例的快照工件匹配。
¥Since we just updated our component to point to a different address, it's reasonable to expect changes in the snapshot for this component. Our snapshot test case is failing because the snapshot for our updated component no longer matches the snapshot artifact for this test case.
为了解决这个问题,我们需要更新我们的快照工件。你可以使用一个标志来运行 Jest,告诉它重新生成快照:
¥To resolve this, we will need to update our snapshot artifacts. You can run Jest with a flag that will tell it to re-generate snapshots:
jest --updateSnapshot
继续并通过运行上述命令接受更改。如果你愿意,还可以使用等效的单字符 -u
标志来重新生成快照。这将为所有失败的快照测试重新生成快照工件。如果由于无意的错误而导致任何其他失败的快照测试,我们需要在重新生成快照之前修复该错误,以避免记录错误行为的快照。
¥Go ahead and accept the changes by running the above command. You may also use the equivalent single-character -u
flag to re-generate snapshots if you prefer. This will re-generate snapshot artifacts for all failing snapshot tests. If we had any additional failing snapshot tests due to an unintentional bug, we would need to fix the bug before re-generating snapshots to avoid recording snapshots of the buggy behavior.
如果你想限制重新生成哪些快照测试用例,你可以传递一个额外的 --testNamePattern
标志,以便仅为那些与模式匹配的测试重新记录快照。
¥If you'd like to limit which snapshot test cases get re-generated, you can pass an additional --testNamePattern
flag to re-record snapshots only for those tests that match the pattern.
你可以通过克隆 快照示例、修改 Link
组件并运行 Jest 来尝试此功能。
¥You can try out this functionality by cloning the snapshot example, modifying the Link
component, and running Jest.
交互式快照模式
¥Interactive Snapshot Mode
失败的快照也可以在监视模式下交互式更新:
¥Failed snapshots can also be updated interactively in watch mode:
一旦你进入交互式快照模式,Jest 将引导你一次测试一个失败的快照,并让你有机会查看失败的输出。
¥Once you enter Interactive Snapshot Mode, Jest will step you through the failed snapshots one test at a time and give you the opportunity to review the failed output.
从这里你可以选择更新该快照或跳到下一个:
¥From here you can choose to update that snapshot or skip to the next:
完成后,Jest 会在返回监视模式之前向你提供摘要:
¥Once you're finished, Jest will give you a summary before returning back to watch mode:
内联快照
¥Inline Snapshots
内联快照的行为与外部快照(.snap
文件)相同,只是快照值会自动写回到源代码中。这意味着你可以获得自动生成的快照的好处,而无需切换到外部文件来确保写入正确的值。
¥Inline snapshots behave identically to external snapshots (.snap
files), except the snapshot values are written automatically back into the source code. This means you can get the benefits of automatically generated snapshots without having to switch to an external file to make sure the correct value was written.
示例:
¥Example:
首先,编写一个测试,不带参数调用 .toMatchInlineSnapshot()
:
¥First, you write a test, calling .toMatchInlineSnapshot()
with no arguments:
it('renders correctly', () => {
const tree = renderer
.create(<Link page="https://example.com">Example Site</Link>)
.toJSON();
expect(tree).toMatchInlineSnapshot();
});
下次运行 Jest 时,将评估 tree
,并将快照作为参数写入 toMatchInlineSnapshot
:
¥The next time you run Jest, tree
will be evaluated, and a snapshot will be written as an argument to 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
键更新快照。
¥That's all there is to it! You can even update the snapshots with --updateSnapshot
or using the u
key in --watch
mode.
默认情况下,Jest 负责将快照写入源代码。但是,如果你在项目中使用 prettier,Jest 将检测到这一点并将工作委托给 prettier(包括尊重你的配置)。
¥By default, Jest handles the writing of snapshots into your source code. However, if you're using prettier in your project, Jest will detect this and delegate the work to prettier instead (including honoring your configuration).
属性匹配器
¥Property Matchers
通常,你想要快照的对象中会生成一些字段(例如 ID 和日期)。如果你尝试对这些对象进行快照,它们将强制快照在每次运行时失败:
¥Often there are fields in the object you want to snapshot which are generated (like IDs and Dates). If you try to snapshot these objects, they will force the snapshot to fail on every run:
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 允许为任何属性提供不对称匹配器。在写入或测试快照之前检查这些匹配器,然后将其保存到快照文件而不是接收到的值:
¥For these cases, Jest allows providing an asymmetric matcher for any property. These matchers are checked before the snapshot is written or tested, and then saved to the snapshot file instead of the received value:
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",
}
`;
任何不是匹配器的给定值都将被精确检查并保存到快照中:
¥Any given value that is not a matcher will be checked exactly and saved to the snapshot:
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()
和 常用表达。
¥If the case concerns a string not an object then you need to replace random part of that string on your own before testing the snapshot.\ You can use for that e.g. replace()
and regular expressions.
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();
实现此目的的其他方法是使用 快照序列化器 或 mocking 库,该库负责生成要快照的代码的随机部分。
¥Other ways this can be done is using the snapshot serializer or mocking the library responsible for generating the random part of the code you're snapshotting.
最佳实践
¥Best Practices
快照是一个很棒的工具,用于识别应用中意外的接口更改 - 无论该接口是 API 响应、UI、日志还是错误消息。与任何测试策略一样,你应该了解一些最佳实践以及应该遵循的指南,以便有效地使用它们。
¥Snapshots are a fantastic tool for identifying unexpected interface changes within your application – whether that interface is an API response, UI, logs, or error messages. As with any testing strategy, there are some best-practices you should be aware of, and guidelines you should follow, in order to use them effectively.
1. 将快照视为代码
¥ Treat snapshots as code
提交快照并作为常规代码审查流程的一部分进行审查。这意味着像对待项目中任何其他类型的测试或代码一样对待快照。
¥Commit snapshots and review them as part of your regular code review process. This means treating snapshots as you would any other type of test or code in your project.
通过保持快照集中、简短并使用强制执行这些风格约定的工具,确保快照可读。
¥Ensure that your snapshots are readable by keeping them focused, short, and by using tools that enforce these stylistic conventions.
如前所述,Jest 使用 pretty-format
使快照易于理解,但你可能会发现引入其他工具很有用,例如带有 no-large-snapshots
选项的 eslint-plugin-jest
或带有组件快照比较功能的 snapshot-diff
,以促进提交简短、集中的断言。
¥As mentioned previously, Jest uses pretty-format
to make snapshots human-readable, but you may find it useful to introduce additional tools, like eslint-plugin-jest
with its no-large-snapshots
option, or snapshot-diff
with its component snapshot comparison feature, to promote committing short, focused assertions.
目标是使审查拉取请求中的快照变得容易,并反对在测试套件失败时重新生成快照而不是检查失败的根本原因的习惯。
¥The goal is to make it easy to review snapshots in pull requests, and fight against the habit of regenerating snapshots when test suites fail instead of examining the root causes of their failure.
2. 测试应该是确定性的
¥ Tests should be deterministic
你的测试应该是确定性的。在未更改的组件上多次运行相同的测试每次都会产生相同的结果。你有责任确保生成的快照不包含特定于平台的数据或其他不确定性数据。
¥Your tests should be deterministic. Running the same tests multiple times on a component that has not changed should produce the same results every time. You're responsible for making sure your generated snapshots do not include platform specific or other non-deterministic data.
例如,如果你有一个使用 Date.now()
的 钟 组件,则每次运行测试用例时,从此组件生成的快照都会不同。在这种情况下,我们可以 模拟 Date.now() 方法 在每次运行测试时返回一致的值:
¥For example, if you have a Clock component that uses Date.now()
, the snapshot generated from this component will be different every time the test case is run. In this case we can mock the Date.now() method to return a consistent value every time the test is run:
Date.now = jest.fn(() => 1_482_363_367_071);
现在,每次运行快照测试用例时,Date.now()
都会一致返回 1482363367071
。这将导致无论测试何时运行,都会为此组件生成相同的快照。
¥Now, every time the snapshot test case runs, Date.now()
will return 1482363367071
consistently. This will result in the same snapshot being generated for this component regardless of when the test is run.
3. 使用描述性快照名称
¥ Use descriptive snapshot names
始终努力对快照使用描述性测试和/或快照名称。最好的名称描述了预期的快照内容。这使得审阅者可以更轻松地在审阅期间验证快照,并且任何人都可以在更新之前了解过时的快照是否是正确的行为。
¥Always strive to use descriptive test and/or snapshot names for snapshots. The best names describe the expected snapshot content. This makes it easier for reviewers to verify the snapshots during review, and for anyone to know whether or not an outdated snapshot is the correct behavior before updating.
例如,比较:
¥For example, compare:
exports[`<UserName /> should handle some test case`] = `null`;
exports[`<UserName /> should handle some other test case`] = `
<div>
Alan Turing
</div>
`;
到:
¥To:
exports[`<UserName /> should render null`] = `null`;
exports[`<UserName /> should render Alan Turing`] = `
<div>
Alan Turing
</div>
`;
由于后者准确地描述了输出中的预期内容,因此可以更清楚地看出何时出错:
¥Since the latter describes exactly what's expected in the output, it's more clear to see when it's wrong:
exports[`<UserName /> should render null`] = `
<div>
Alan Turing
</div>
`;
exports[`<UserName /> should render Alan Turing`] = `null`;
常见问题
¥Frequently Asked Questions
快照是否会自动写入持续集成 (CI) 系统?
¥Are snapshots written automatically on Continuous Integration (CI) systems?
不,从 Jest 20 开始,当 Jest 在 CI 系统中运行而未显式传递 --updateSnapshot
时,不会自动写入 Jest 中的快照。预计所有快照都是在 CI 上运行的代码的一部分,并且由于新快照会自动通过,因此它们不应通过 CI 系统上的测试运行。建议始终提交所有快照并将它们保留在版本控制中。
¥No, as of Jest 20, snapshots in Jest are not automatically written when Jest is run in a CI system without explicitly passing --updateSnapshot
. It is expected that all snapshots are part of the code that is run on CI and since new snapshots automatically pass, they should not pass a test run on a CI system. It is recommended to always commit all snapshots and to keep them in version control.
是否应该提交快照文件?
¥Should snapshot files be committed?
是的,所有快照文件都应该与它们所覆盖的模块及其测试一起提交。它们应该被视为测试的一部分,类似于 Jest 中任何其他断言的值。事实上,快照代表了源模块在任何给定时间点的状态。这样,当源模块被修改时,Jest 可以知道与之前版本相比发生了什么变化。它还可以在代码审查期间提供大量附加上下文,使审查者可以更好地研究你的更改。
¥Yes, all snapshot files should be committed alongside the modules they are covering and their tests. They should be considered part of a test, similar to the value of any other assertion in Jest. In fact, snapshots represent the state of the source modules at any given point in time. In this way, when the source modules are modified, Jest can tell what changed from the previous version. It can also provide a lot of additional context during code review in which reviewers can study your changes better.
快照测试只适用于 React 组件吗?
¥Does snapshot testing only work with React components?
React 和 React Native 组件是快照测试的一个很好的用例。但是,快照可以捕获任何可序列化的值,并且应该在目标是测试输出是否正确的任何时候使用。Jest 存储库包含许多测试 Jest 本身的输出、Jest 断言库的输出以及来自 Jest 代码库各个部分的日志消息的示例。请参阅 Jest 存储库中的 快照 CLI 输出 示例。
¥React and React Native components are a good use case for snapshot testing. However, snapshots can capture any serializable value and should be used anytime the goal is testing whether the output is correct. The Jest repository contains many examples of testing the output of Jest itself, the output of Jest's assertion library as well as log messages from various parts of the Jest codebase. See an example of snapshotting CLI output in the Jest repo.
快照测试和视觉回归测试有什么区别?
¥What's the difference between snapshot testing and visual regression testing?
快照测试和视觉回归测试是测试 UI 的两种不同方法,它们具有不同的目的。视觉回归测试工具截取网页屏幕截图并逐像素比较结果图片。通过快照,测试值被序列化,存储在文本文件中,并使用 diff 算法进行比较。有不同的权衡需要考虑,我们列出了在 Jest 博客 中构建快照测试的原因。
¥Snapshot testing and visual regression testing are two distinct ways of testing UIs, and they serve different purposes. Visual regression testing tools take screenshots of web pages and compare the resulting images pixel by pixel. With Snapshot testing values are serialized, stored within text files, and compared using a diff algorithm. There are different trade-offs to consider and we listed the reasons why snapshot testing was built in the Jest blog.
快照测试会取代单元测试吗?
¥Does snapshot testing replace unit testing?
快照测试只是 Jest 附带的 20 多个断言之一。快照测试的目的不是取代现有的单元测试,而是提供额外的价值并使测试变得轻松。在某些情况下,快照测试可能会消除对一组特定功能(例如 React 组件)进行单元测试的需要,但它们也可以一起工作。
¥Snapshot testing is only one of more than 20 assertions that ship with Jest. The aim of snapshot testing is not to replace existing unit tests, but to provide additional value and make testing painless. In some scenarios, snapshot testing can potentially remove the need for unit testing for a particular set of functionalities (e.g. React components), but they can work together as well.
关于生成文件的速度和大小,快照测试的性能如何?
¥What is the performance of snapshot testing regarding speed and size of the generated files?
Jest 在重写时考虑到了性能,快照测试也不例外。由于快照存储在文本文件中,因此这种测试方式快速且可靠。Jest 为调用 toMatchSnapshot
匹配器的每个测试文件生成一个新文件。快照的大小非常小:作为参考,Jest 代码库本身中所有快照文件的大小均小于 300 KB。
¥Jest has been rewritten with performance in mind, and snapshot testing is not an exception. Since snapshots are stored within text files, this way of testing is fast and reliable. Jest generates a new file for each test file that invokes the toMatchSnapshot
matcher. The size of the snapshots is pretty small: For reference, the size of all snapshot files in the Jest codebase itself is less than 300 KB.
如何解决快照文件中的冲突?
¥How do I resolve conflicts within snapshot files?
快照文件必须始终代表它们所覆盖的模块的当前状态。因此,如果你在合并两个分支时遇到快照文件冲突,你可以手动解决冲突,也可以通过运行 Jest 并检查结果来更新快照文件。
¥Snapshot files must always represent the current state of the modules they are covering. Therefore, if you are merging two branches and encounter a conflict in the snapshot files, you can either resolve the conflict manually or update the snapshot file by running Jest and inspecting the result.
是否可以通过快照测试应用测试驱动开发原则?
¥Is it possible to apply test-driven development principles with snapshot testing?
尽管可以手动写入快照文件,但这通常是不可实现的。快照有助于确定测试所覆盖的模块的输出是否发生更改,而不是首先指导设计代码。
¥Although it is possible to write snapshot files manually, that is usually not approachable. Snapshots help to figure out whether the output of the modules covered by tests is changed, rather than giving guidance to design the code in the first place.
代码覆盖率是否适用于快照测试?
¥Does code coverage work with snapshot testing?
是的,以及任何其他测试。
¥Yes, as well as with any other test.