# 过度使用快照测试 看看下面的代码。你第一眼能理解这个测试想要做什么吗? ```js await editor.insertBlock( { name: 'core/quote' } ); await page.keyboard.type( '1' ); await page.keyboard.press( 'Enter' ); await page.keyboard.press( 'Enter' ); expect( await editor.getEditedPostContent() ).toMatchSnapshot(); await page.keyboard.press( 'Backspace' ); await page.keyboard.type( '2' ); expect( await editor.getEditedPostContent() ).toMatchSnapshot(); ``` 这段代码改编自Gutenberg项目中的真实代码,移除了测试标题和注释,并重构为Playwright版本。理想情况下,端到端测试应该具备自文档化和最终用户可读性;毕竟它们试图模拟最终用户与应用程序的交互方式。然而,这段代码中存在几个值得警惕的问题。 ## 快照测试存在的问题 由Jest推广的[快照测试](https://jestjs.io/docs/snapshot-testing)在恰当使用时是个很好的工具。但可能因为它功能强大,开发者经常过度使用。已有许多[文章](https://kentcdodds.com/blog/effective-snapshot-testing)讨论过这个问题。在这个具体案例中,快照测试未能清晰体现开发者的意图。如果不查看其他信息,我们无法明确断言的具体内容。这使得代码难以理解,给除原作者外的所有读者增加了认知负担。作为读者,我们不得不反复查看代码才能完全理解它们。增加的代码复杂性阻碍了贡献者根据需求修改测试的意愿。有时甚至会让原作者感到困惑,导致意外[提交错误的快照](https://github.com/WordPress/gutenberg/pull/42780#discussion_r949865612)。 以下是包含测试标题和注释的同一测试。现在你明白这些断言的实际含义了: ```js it( '可以在末尾拆分', async () => { // ... // 期望在引用块外有空段落 expect( await getEditedPostContent() ).toMatchSnapshot(); // ... // 期望段落被合并到引用块中 expect( await getEditedPostContent() ).toMatchSnapshot(); } ); ``` 开发者的意图现在稍微清晰了些,但仍感觉与测试本身脱节。你可能会想尝试[内联快照](https://jestjs.io/docs/snapshot-testing#inline-snapshots),这确实解决了需要在文件间跳转的问题,但它们仍然不具备自文档化特性也不够明确。我们可以做得更好。 ## 解决方案 与其在注释中写断言说明,不如尝试直接明确地写出来。借助`editor.getBlocks`方法,我们可以将其重写为更简单和原子化的断言: ```js // ... // 期望在引用块外有空段落 await expect.poll( editor.getBlocks ).toMatchObject( [ { name: 'core/quote', innerBlocks: [ { name: 'core/paragraph', attributes: { content: '1' }, }, ], }, { name: 'core/paragraph', attributes: { content: '' }, } ] ); // ... // 期望段落被合并到引用块中 await expect.poll( editor.getBlocks ).toMatchObject( [ { name: 'core/quote', innerBlocks: [ { name: 'core/paragraph', attributes: { content: '1' }, }, { name: 'core/paragraph', attributes: { content: '2' }, }, ], } ] ); ``` 这些断言更加可读和明确。你可以添加额外的断言或将现有断言拆分成多个,以突出它们的重要性。是否保留注释取决于你,但当代码本身已经足够可读时,通常可以省略注释。 ## 快照测试的变体 由于Playwright缺乏内联快照功能,一些迁移的测试使用字符串断言(`toBe`)来模拟类似效果,避免创建大量快照文件: ```js expect( await editor.getEditedPostContent() ).toBe( `
段落
` ); ``` 我们可以将这种模式视为快照测试的变体,在编写时应遵循相同的规则。通常最好使用`editor.getBlocks`或其他方法重写它们,以进行明确断言: ```js await expect.poll( editor.getBlocks ).toMatchObject( [ { name: 'core/paragraph', attributes: { content: '段落' }, } ] ); ``` ## 测试覆盖率怎么办? 将明确断言与快照测试比较,我们确实在这个测试中损失了一些测试覆盖率。当我们需要断言块的完整序列化内容时,快照测试仍然有用。但幸运的是,集成测试中的一些测试已经对每个核心块的[完整内容](https://github.com/WordPress/gutenberg/blob/trunk/test/integration/fixtures/blocks/README.md)进行了断言。这些测试在Node.js中运行,比在Playwright中重复相同测试要快得多。在我的机器上运行273个测试用例仅需约5.7秒。这类测试在单元或集成级别效果很好,我们可以在不损失测试覆盖率的情况下更快地运行它们。 ## 最佳实践 在端到端测试中很少需要快照测试,通常有更好的替代方案可以利用明确断言。在确实没有其他合适替代方案时,我们应遵循使用快照测试的最佳实践。 ### 避免巨大的快照 巨大的快照难以阅读和审查。而且,当所有内容都重要时,实际上没有任何内容是重要的。巨大的快照会阻碍我们关注快照的重要部分。 ### 避免重复的快照 如果你发现在同一个测试中创建了多个相似内容的快照,这可能表明你应该进行更原子化的断言。重新思考你想要测试什么,如果第一个快照只是第二个的参考,那么你真正需要的是快照之间的**差异**。将第一个结果存储在变量中,然后断言结果之间的差异。 ## 延伸阅读 - [有效的快照测试 - Kent C. Dodds](https://kentcdodds.com/blog/effective-snapshot-testing) - [常见测试错误 - Kent C. Dodds](https://kentcdodds.com/blog/common-testing-mistakes)