我想我最近一直在进行测试,我保证这篇通讯以后会深入研究更多的东西。但是我看到了Justin Searls的这条推特(他是一个我很欣赏的开发者,有大量的测试经验),认为这将是一个很好的主题,可以写一下。
在那条推文中,Justin分享了他对快照测试的一堆想法的截图。为了方便阅读,我把他写的全部内容打在下面。然后我将谈谈我对他所说的一些想法。

关于快照测试的想法
对快照测试方案的看法,请关注。围绕着快照测试,有许多失败的类别。其中大部分是我在2008-2011年的经验,当时QA团队认为Selenium RC记录回放脚本是万能的,但我也看到了同样的事情,比如Ruby中的VCR,JS测试中的HTML固定装置,以及其他试图 "轻松 "控制API&DB依赖性的尝试。
它们是你不了解的测试,所以当它们失败时,你通常不了解为什么或如何修复它。这意味着你必须做真/假的负面分析,然后在调试如何解决这个问题时承受间接的影响。
好的测试编码了开发者的意图,他们不仅锁定了测试的行为而没有编辑什么是重要的和为什么。快照测试缺乏(或至少未能鼓励)表达作者的意图,即代码做什么(更不用说为什么)。
它们是生成的文件,而开发人员在提交之前往往没有纪律地仔细检查生成的文件,如果不是在一开始,那么肯定是随着时间推移。大多数开发人员在看到快照测试失败后,会很快放弃快照并记录一个新的合格的快照,而不是苦苦思索是什么原因造成的。
因为他们的集成度更高,并试图序列化一个不完整的系统(例如,一个有某种副作用的系统:从浏览器/库/运行时版本到环境到数据库/API变化),他们将倾向于有高的假阴性(失败的测试,生产代码实际上是好的,测试只是需要被改变)。假阴性很快就会侵蚀团队对测试的信任,使其无法真正发现错误,反而会被看作是他们需要满足的清单上的一个苦差事,然后才能继续做下一件事。
这四件事导致了集成/功能测试的预期效用几乎完全丧失:当代码改变时,确保没有损坏。
相反,当代码改变时,测试肯定会失败,但确定是否和什么是由这个失败实际 "破坏 "的,是比简单地重新记录和提交一个新的快照更痛苦的路径。(毕竟,过去的快照并没有被很好地理解或仔细地表达作者的意图)。因此,如果一个快照测试失败是因为一些预期的行为消失了,那么就没有什么明确的意图来描述它,我们宁愿重新生成文件,而不是花大量的时间来苦恼如何再次获得相同的测试绿色。
如果你认识我,你会知道我非常欣赏Jest的快照测试功能。(对于那些在egghead.io上订阅的人来说,请看这个)。 也就是说,我和Justin在很多层面上对他们的感受一样。贾斯汀上面的这段话就是充满了我们不应该忽视的黄金见解。我亲身经历过Justin所说的快照测试中的许多陷阱(包括我自己和其他人)。所以,谢谢你分享你的想法,Justin!

在继续之前,我想明确的一点是,快照测试是一个断言,就像toBe in:expect('foo').toBe('foo') 。我认为在这一点上有时会有混淆,所以我只是想澄清一下。
尽管Justin反对快照,但我认为,如果你有效地使用它们,它们还是有价值的。考虑到这一点,我想我将分享一些快照测试真正闪光的案例,避免使用快照的事情,以及你可以做的事情,使你的快照更有效。
快照测试的闪光点
错误信息和日志
如果你正在为开发人员编写一个工具,你想写一个测试,以确保一个好的错误或警告信息被记录到控制台,供开发人员使用你的工具,这是一个非常常见的情况。在快照测试之前,我总是写一个愚蠢的重构词来获得信息应该说的基本要点,但有了快照测试,就容易多了。
下面是一个来自kcd-scripts 的例子:
babel-plugin-tester
老实说,除了Jest的快照测试,我不知道该如何测试babel插件。如果要对产生的AST进行断言,那将是非常困难的。babel-plugin-tester 使用快照进行断言,它们非常好。由于结果的序列化方式,它们避免了Justin提到的很多陷阱。这里有一个来自import-all.macro的例子。
exports[`importAll.sync uses static imports 1`] = `
import importAll from 'import-all.macro'
const a = importAll.sync('./files/*.js')
↓ ↓ ↓ ↓ ↓ ↓
import * as _filesAJs from './files/a.js'
import * as _filesBJs from './files/b.js'
import * as _filesCJs from './files/c.js'
import * as _filesDJs from './files/d.js'
const a = {
'./files/a.js': _filesAJs,
'./files/b.js': _filesBJs,
'./files/c.js': _filesCJs,
'./files/d.js': _filesDJs,
}
这个快照之所以好,是因为它可以通过快照的标题和前/后(用↓ 把前和后分开)来传达意图。这一点很酷,对于任何使用babel-plugin-tester 的插件,我只要看一下快照文件,就能对该插件的工作原理有一个很好的了解。😎
jest-glamor-react
你是否曾经因为没有正确应用样式而破坏了你的应用程序的用户体验?我有过。编写测试以确保这种信心是非常困难的。即使是E2E测试也不能可靠地测试这种东西。 有一些工具可以进行视觉快照和视觉差异比较。 但这些工具很难设置和运行,而且往往很不稳定。此外,它们基本上是快照测试,所以它们也遭受了许多贾斯汀所指出的关于快照测试的问题
也就是说,我们仍然会遇到这些错误,如果能避免这些错误就更好了。如果你使用CSS-in-JS,有一个很好的方法可以使用快照测试来减少测试这类变化的一些困难。如果你使用像jest-glamor-react这样的工具,那么你就可以把适用的CSS和你渲染的东西一起包括进去。例如。
exports[`enzyme.render 1`] = `
.glamor-1,
[data-glamor-1] {
padding: 4em;
background: papayawhip;
}
.glamor-0,
[data-glamor-0] {
font-size: 1.5em;
text-align: center;
color: palevioletred;
}
<section
class="glamor-1"
>
<h1
class="glamor-0"
>
Hello World, this is my first glamor styled component!
</h1>
</section>
`
这很好,因为现在如果我们改变逻辑,使一些样式不能正确应用,我们就会知道。它仍然受到Justin所提到的问题的影响,但我认为这是值得的。
使用快照需要避免的事情
巨大的快照
这可能是造成Justin所说的所有事情的最大原因。当你的快照超过几十行的时候,它就会出现重大的维护问题,拖累你和你的团队。 请记住,测试的目的是让你有信心,你不会在发货时出现问题。 而且,如果你有巨大的快照,没有人会仔细审查的话,你就不能很好地保证这一点。我亲身经历过这样的情况,一个快照有640多行长。没有人审查它,人们对它的唯一关心是,只要有变化,就把它剔除,然后重新拍摄(就像Justin提到的)。
所以,要避免巨大的快照,而采取较小的、更集中的快照。当你这样做的时候,看看你是否可以把它从一个快照变成一个更明确的断言(因为你可能可以😉)。
我应该补充一点,即使是巨大的快照也不是完全无用的。因为如果快照发生了意外的变化,它可以(而且已经)通知我们,我们做了一个影响比预期更远的改变。
编辑:自从我写了这篇博文后,eslint-plugin-jest中增加了一个新的lint规则,叫做"no-large-snapshots",这对阻止大型快照有很大帮助👍。
让你的快照更有效
自定义序列化器
jest-glamor-react 实际上是一个自定义序列化器。这让我们的快照变得更加有效。编写一个自定义的序列化器其实很简单。 这是我写的一个,它将路径规范化,所以快照中的任何路径都是相对于项目目录的,在windows和mac上看起来都一样。
const projectRoot = path.join(__dirname, '../../')
expect.addSnapshotSerializer({
test: val => typeof val === 'string',
print: val =>
val
.split(projectRoot)
.join('<PROJECT_ROOT>/')
.replace(/\/g, '/'),
})
你可以用自定义序列化器做很多事情。我强烈建议Rogelio Guzman在React Conf 2017上的演讲"Jest Snapshots and Beyond",他在那里谈了更多。
snapshot-diff
我发现在测试可维护性方面最有用的事情之一是,当你有许多看起来相同的测试时,试着让它们的差异突出。 这使人们更容易进入你的代码库,知道什么是重要的部分。因此,你可以尝试把共同的设置/卸载拆成一个小的辅助函数,使测试有更多的差异,而减少彼此之间的共同点。
我见过一些测试,在用户互动之前,你对一个反应组件拍摄一个快照,在用户互动之后拍摄另一个。你想断言的是前后的差异,但你得到的比你讨价还价的要多得多,这就导致了Justin所说的更多的假阴性。然而,如果你能把两个状态之间的差异序列化,那就会更有帮助。而这正是snapshot-diff 可以为你做的。
const React = require('react')
const {toMatchDiffSnapshot} = require('snapshot-diff')
const Component = require('./Component')
expect.extend({toMatchDiffSnapshot})
test('snapshot difference between 2 React components state', () => {
expect(<Component test="say" />).toMatchDiffSnapshot(
<Component test="my name" />,
)
})
而快照将只是两者之间的差异(看起来像git diff)。