看看下面的代码。你能一眼就看懂这个测试想做什么吗?

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 版本。理想情况下,E2E 测试应当是自文档化的,并且对最终用户而言易于阅读;归根结底,它们的目标是模拟最终用户与应用交互的方式。然而,这段代码中存在几个危险信号。

快照测试的问题

快照测试由 Jest 推广开来,在合理的场景下是帮助我们测试应用的绝佳工具。然而,也许正因为它太过强大,开发者常常会过度使用它。关于这一点,已经有多篇文章讨论过。在这个具体案例中,快照测试未能体现开发者的意图。如果不查看其他信息,就无法弄清这些断言究竟在验证什么。这使得代码更难理解,也给除编写者之外的所有读者带来了额外的认知负担。作为读者,我们不得不在代码之间来回跳转才能完全理解它们。代码复杂度的增加也打击了贡献者按自身需求修改测试的积极性。有时它甚至会让作者本人感到困惑,导致他们不小心提交错误的快照。

下面是带测试标题和注释的同一个测试。现在你应该知道这些断言实际上在验证什么了。

it( 'can be split at the end', async () => {
    // ...

    // 期望引用区块外有一个空段落。
    expect( await getEditedPostContent() ).toMatchSnapshot();

    // ...

    // 期望该段落被合并到引用区块中。
    expect( await getEditedPostContent() ).toMatchSnapshot();
} );

这样一来,开发者的意图可读性略有提升,但仍然让人感觉与测试本身脱节。你可能会想尝试内联快照,它确实解决了在文件之间来回跳转的问题,但它既不自文档化,也不够显式。我们可以做得更好。

解决方案

与其把断言写在注释里,我们可以尝试直接将它们显式地写出来。借助 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 秒。这类测试在单元或集成层面表现出色,我们可以在不损失测试覆盖率的情况下大幅提升它们的运行速度。

最佳实践

在 E2E 测试中,很少真正需要快照测试,通常会有利用显式断言的更好替代方案。在没有其他合适替代方案的情况下,我们在使用快照测试时应当遵循最佳实践。

避免庞大的快照

庞大的快照难以阅读,也难以审查。此外,当一切都重要时,就没有什么是真正重要的了。庞大的快照使我们无法专注于快照中真正重要的部分。

避免重复的快照

如果你发现自己在同一个测试中为相似内容创建了多个快照,那么这很可能是一个信号,说明你更应该编写更原子化的断言。重新思考你想要测试什么:如果第一个快照只是作为第二个快照的参照,那么你想要验证的很可能是快照之间的差异。把第一个结果存储到一个变量中,并改为断言结果之间的差异。

延伸阅读