title: "过度使用快照" post_status: publish comment_status: open taxonomy: category: - gutenberg-docs post_tag: - E2E - Code - Contributors
过度使用快照
看看下面的代码。你能一眼看出这个测试试图做什么吗?
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 版本。理想情况下,端到端测试应该是自文档化且对最终用户可读的;毕竟,它们试图模拟最终用户与应用程序的交互方式。然而,这段代码中存在一些值得警惕的问题。
Problems with snapshot testing
Popularized by Jest, snapshot testing is a great tool to help test our app when it makes sense. However, probably because it's so powerful, it's often overused by developers. There are already multiple articles about this. In this particular case, snapshot testing fails to reflect the developer's intention. It's not clear what the assertions are about without looking into other information. This makes the code harder to understand and creates a mental overhead for all the other readers other than the one who wrote it. As readers, we have to jump around the code to fully understand them. The added complexity of the code discourages contributors from changing the test to fit their needs. It could sometimes even confuse the authors and make them accidentally commit the wrong snapshots.
Here's the same test with the test title and comments. Now you know what these assertions are actually about.
it( 'can be split at the end', async () => {
// ...
// Expect empty paragraph outside quote block.
expect( await getEditedPostContent() ).toMatchSnapshot();
// ...
// Expect the paragraph to be merged into the quote block.
expect( await getEditedPostContent() ).toMatchSnapshot();
} );
The developer's intention is a bit more readable, but it still feels disconnected from the test. You might be tempted to try inline snapshots, which do solve the issue of having to jump around files, but they're still not self-documented nor explicit. We can do better.
解决方案
与其在注释中编写断言,我们可以尝试直接明确地写出它们。借助 editor.getBlocks,我们可以将其重写为更简单、更原子化的断言。
// ...
// 期望引用块外存在空段落。
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)来模拟类似效果,而无需创建大量快照文件。
expect( await editor.getEditedPostContent() ).toBe( `<!-- wp:paragraph -->
<p>Paragraph</p>
<!-- /wp:paragraph -->` );
我们可以将这种模式视为快照测试的一种变体,编写时应遵循相同的规则。通常最好使用 editor.getBlocks 或其他方法重写它们,以进行显式断言。
await expect.poll( editor.getBlocks ).toMatchObject( [ {
name: 'core/paragraph',
attributes: { content: 'Paragraph' },
} ] );
测试覆盖率如何?
将显式断言与快照测试对比,我们确实在这个测试中损失了部分覆盖率。当我们需要断言区块完整序列化内容时,快照测试依然有效。不过值得庆幸的是,集成测试中的某些用例已经对每个核心区块的完整内容进行了断言。这些测试在 Node.js 环境中运行,速度远快于在 Playwright 中重复执行相同测试——在我的机器上运行 273 个测试用例仅需约 5.7 秒。这类测试在单元或集成层面表现优异,我们既能大幅提升执行速度,又不会牺牲测试覆盖率。
最佳实践
在端到端测试中应尽量避免使用快照测试,通常有更好的替代方案可以利用显式断言。当确实没有其他合适选择时,我们应遵循使用快照测试的最佳实践。
避免庞大的快照
庞大的快照难以阅读和审查。此外,当所有内容都重要时,就没有什么是重要的了。庞大的快照会阻碍我们关注快照的重要部分。
避免重复的快照
如果你发现自己在同一个测试中为相似内容创建多个快照,这可能意味着你应该进行更原子化的断言。重新思考你的测试目标——如果第一个快照仅作为第二个的参照,那么你真正需要的是快照之间的差异。将第一个结果存储在变量中,然后断言结果之间的差异。