# 过度使用快照测试

看看下面的代码。你第一眼能理解这个测试想要做什么吗？

```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( `<!-- wp:paragraph -->
<p>段落</p>
<!-- /wp:paragraph -->` );
```

我们可以将这种模式视为快照测试的变体，在编写时应遵循相同的规则。通常最好使用`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)