快照测试

每当你想要确保你的UI不会有意外的改变,快照测试是非常有用的工具。

典型的做法是在渲染了UI组件之后,保存一个快照文件, 检测他是否与保存在单元测试旁的快照文件相匹配。 若两个快照不匹配,测试将失败:有可能做了意外的更改,或者UI组件已经更新到了新版本。

Jest快照测试

测试React组件可以采用类似的方法。 你只需要测试对应的React树的序列号值即可,而不需要渲染整个React程序。 Consider this example test for a Link component:

  1. import renderer from 'react-test-renderer';
  2. import Link from '../Link';
  3. it('renders correctly', () => {
  4. const tree = renderer
  5. .create(<Link page="http://www.facebook.com">Facebook</Link>)
  6. .toJSON();
  7. expect(tree).toMatchSnapshot();
  8. });

The first time this test is run, Jest creates a snapshot file that looks like this:

  1. exports[`renders correctly 1`] = `
  2. <a
  3. className="normal"
  4. href="http://www.facebook.com"
  5. onMouseEnter={[Function]}
  6. onMouseLeave={[Function]}
  7. >
  8. Facebook
  9. </a>
  10. `;

快照文件应该和项目代码一起提交并做代码评审。 Jest uses pretty-format to make snapshots human-readable during code review. 在随后的单元测试例子中,Jest会对比上一个快照和渲染输出。 如果它们相匹配,则测试通过。 若未能匹配,要么是单元测试发现了你代码中的Bug,要么是代码实现已经变了,需要更新测试快照文件。 <Link>

小提示:快照测试只会测试数据渲染本身,在我们的例子中是测试向 <Link />组件 传递了 page 属性的Prop后的渲染。 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.

更多关于“快照如何工作”和“我们为什么需要快照”的信息可以在 这些博客查看 我们推荐你阅读 另外一些博客来知晓如何更好的使用快照。 我们也推荐您观看 egghead video 这个视频来学习如何使用快照。

更新快照

在代码引入错误后,很容易就通过快照发现为何单元测试失败了。 发生这种情况时,需要解决以使你的快照测试再次通过。 现在,让我们讨论一个故意使快照测试失败的案例。

如果我们有意的更改上述示例中的page属性,就会出现这种情况。

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

这个例子中,Jest将会打印出:

快照测试 - 图1

由于我们更改了测试用例中传入的page地址,我们也希望快照能因此改变。 快照测试失败了,原因是我们更新后的组件快照和之前形成的快照不一致。

要解决这个问题,我们需要更新我们已存储的快件文件。 你可以运行Jest 加一个标识符来重新生成快照文件。

  1. jest --updateSnapshot

运行上面的命令行,并接受更改。 你可以用单个字符完成同样的事情 ,如 jest -u。 这将为所有失败的快照测试重新生成快照文件。 如果我们无意间产生了Bug导致快照测试失败,应该先修复这些Bug,再生成快照文件,以避免用快照录制了错误的行为。

如果你想限制只重新生成一部分的快照文件,你可以使用--testNamePattern 来正则匹配想要生成的快照名字。

You can try out this functionality by cloning the snapshot example, modifying the Link component, and running Jest.

交互式快照模式

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

快照测试 - 图2

进入交互式快照模式后,Jest会让你浏览失败的快照,并让你可以看到失败用例的输出日志。

这样,你就可以选择更新快照,或者跳至下一步。

快照测试 - 图3

完成后,Jest在返回监听模式前输出一份总结。

快照测试 - 图4

内联快照

内联快照和普通快照(.snap 文件)表现一致,只是会将快照值自动写会源代码中。 这意味着你可以从自动生成的快照中受益,并且不用切换到额外生成的快照文件中保证值的正确性。

示例:

首先,你写一个测试,并调用 .toMatchInlineSnapshot() ,不需要传递参数。

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

下次运行Jest时,tree 会被重新评估,此次的快照会被当作一个参数传递到 toMatchInlineSnapshot:

  1. it('renders correctly', () => {
  2. const tree = renderer
  3. .create(<Link page="https://example.com">Example Site</Link>)
  4. .toJSON();
  5. expect(tree).toMatchInlineSnapshot(`
  6. <a
  7. className="normal"
  8. href="https://example.com"
  9. onMouseEnter={[Function]}
  10. onMouseLeave={[Function]}
  11. >
  12. Example Site
  13. </a>
  14. `);
  15. });

这就是所有的了。 你甚至可以在--watch 模式中使用--updateSnapshot-u 来更新快照。

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).

属性匹配器

项目中常常会有不定值字段生成(例如IDs和Dates)。 如果你试图对这些对象进行快照测试,每个测试都会失败。

  1. it('will fail every time', () => {
  2. const user = {
  3. createdAt: new Date(),
  4. id: Math.floor(Math.random() * 20),
  5. name: 'LeBron James',
  6. };
  7. expect(user).toMatchSnapshot();
  8. });
  9. // Snapshot
  10. exports[`will fail every time 1`] = `
  11. Object {
  12. "createdAt": 2018-05-19T23:36:09.816Z,
  13. "id": 3,
  14. "name": "LeBron James",
  15. }
  16. `;

针对这些情况,Jest允许为任何属性提供匹配器(非对称匹配器)。 在快照写入或者测试前只检查这些匹配器是否通过,而不是具体的值。

  1. it('will check the matchers and pass', () => {
  2. const user = {
  3. createdAt: new Date(),
  4. id: Math.floor(Math.random() * 20),
  5. name: 'LeBron James',
  6. };
  7. expect(user).toMatchSnapshot({
  8. createdAt: expect.any(Date),
  9. id: expect.any(Number),
  10. });
  11. });
  12. // Snapshot
  13. exports[`will check the matchers and pass 1`] = `
  14. Object {
  15. "createdAt": Any<Date>,
  16. "id": Any<Number>,
  17. "name": "LeBron James",
  18. }
  19. `;

任何其他不是匹配器的值都会被准确检查并保存到快照文件中。

  1. it('will check the values and pass', () => {
  2. const user = {
  3. createdAt: new Date(),
  4. name: 'Bond... James Bond',
  5. };
  6. expect(user).toMatchSnapshot({
  7. createdAt: expect.any(Date),
  8. name: 'Bond... James Bond',
  9. });
  10. // Snapshot
  11. exports[`will check the values and pass 1`] = `
  12. Object {
  13. "createdAt": Any<Date>,
  14. "name": 'Bond... James Bond',
  15. }
  16. `;

快照测试 - 图5tip

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.

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

Another way is to mock the library responsible for generating the random part of the code you’re snapshotting.

最佳实践

快照是一个神奇的工具,可以识别你的应用程序中的意外界面变化—无论这个界面是API响应、用户界面、日志还是错误信息。 和其他的测试策略一样,逆序遵循一些准则来有效的利用他们,以完成最佳实践。

1. 视快照如代码

像提交并审核常规代码一样去提交和审核快照。 这就意味着要像你在项目中对待其他测试和代码一样对待快照。

确保你的快照更易读,方法是保持聚焦,简短,并使用工具遵循规范以保持代码风格。

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.

目标是能让我们轻松的在代码PR中审核快照,改掉测试快照失败时就重新生成快照的坏习惯,正确的做法是查找错误的根源

2. 测试应是确定性的

你所有的测试都应该是确定的。 任何时候测试未改变的组件都应该产生相同的结果。 你需要确保你的快照测试与平台和其他不相干数据无关。

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. 这种情况,我们可以使用 mock the Date.now() method 重写Date.now, 去返回确定值。

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

现在,每次快照运行时,Date.now() 都会返回确定的值。 这样每次快照在不同时间运行就依然能产生相同的快照了。

3. 使用合理的快照描述

总是尽量好好描述你的测试和快照。 最好的描述名称是写出期望的返回内容。 这能让审查人员在审查过程中更容易验证快照,也能让任何人在更改代码之前知道这个快照已过时。

例如,对比以下例子:

  1. exports[`<UserName /> should handle some test case`] = `null`;
  2. exports[`<UserName /> should handle some other test case`] = `
  3. <div>
  4. Alan Turing
  5. </div>
  6. `;

到:

  1. exports[`<UserName /> should render null`] = `null`;
  2. exports[`<UserName /> should render Alan Turing`] = `
  3. <div>
  4. Alan Turing
  5. </div>
  6. `;

Since the latter describes exactly what’s expected in the output, it’s more clear to see when it’s wrong:

  1. exports[`<UserName /> should render null`] = `
  2. <div>
  3. Alan Turing
  4. </div>
  5. `;
  6. exports[`<UserName /> should render Alan Turing`] = `null`;

常见问题

持续集成系统(CI) 中也会生成快照文件吗?

不。截止到Jest 20 版本,如果没有明确传入 --updateSnapshot,快照是不会自动写入CI系统的。 预计所有快照都是在CI上运行的代码的一部分,并且由于新快照会自动通过,因此它们不应通过在CI系统上运行的测试。 建议始终提交所有快照并将其保留在版本控制中。

Should snapshot files be committed?

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. 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.

Does snapshot testing only work with React components?

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?

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. 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?

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. 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 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.

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

Snapshot files must always represent the current state of the modules they are covering. 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.

快照测试是否有可能也适用于测试驱动开发的原则?

虽然可以手动写入快照文件,但这通常是不可访问的。 快照有助于找出测试覆盖的模块的输出是否发生了变化,而不是首先为代码的设计提供指导。

代码覆盖对快照测试有用吗?

当然,和其他测试一样。