我记得快照测试刚出现时的情景,那时是 Enzyme 的时代。它是最新的酷技术,许多人纷纷开始在他们的 React 应用程序中广泛使用它。
从表面上看,快照测试似乎提供了很多好处,但在我看来,它们只是前端的一时风潮,其使用方式并没有被充分理解。在许多情况下,它们实际上降低了应用程序的测试质量……现在已经是2025年了,我仍然看到一些例子,快照测试仍然是应用程序组件测试覆盖率的一个重要部分。
以下是我认为快照测试对测试质量产生负面影响的原因,以及为什么我认为你应该停止使用它们(如果你仍在使用的话):
1. 快照假设组件在做正确的事情
当快照被创建时,它假设组件在做正确的事情。这是一个很大的假设,而快照本身很难让这一点变得明显。这种假设有多常见?非常常见!我见过许多快照把错误当作正确的输出进行验证。🐛
2. 快照过载
我观察到一些团队在其测试策略中严重依赖快照测试。他们为组件中的每一个路径都创建了快照。快照文件变得非常庞大,没人愿意去看它,甚至 GitHub 可能也会试图隐藏它,因为它不想处理文件的大小!内联快照可能会有所帮助,但我从未看到过很好的应用。现实是,我们有一堆机器生成的代码,期望工程师在软件交付过程中必须翻阅这些代码。😫
3. 快照失败很难理解
当一个快照失败时,很难快速有效地理解失败的原因。是回归问题吗?如果是,具体是哪里出了问题?我们只知道输出发生了变化。如果不明显,并且你不走运,可能得翻遍快照来更好地理解问题,同时祈祷快照尽可能简洁。🙏
4. 快照失败可能不是回归
快照并不符合测试驱动开发(如果你追求的是这一点)。你只有在代码写好后才能创建快照。因此,快照失败的原因有很多,可能并非回归问题。也许你重构了组件的类名,那就是失败。你修改了 HTML 元素以改善可访问性,那也是失败。你的测试现在与 DOM 结构耦合了!这不是理想的状态。⚠️
5. 简单的变化可能导致代码库中的大量失败
React Testing Library 提倡将一个组件和所有其子组件一起进行测试。我见过类似的做法应用于快照测试。实际上,这意味着当一个子组件发生微小变化(例如类名的简单变化)时,我们会看到代码库中大量的测试失败。然后,这需要时间和精力来解决。维护这种方式的价值值得怀疑。🚧
6. 没有工程师愿意花时间阅读快照
即使是最勤奋、最注重质量的工程师,也不会愿意定期花时间翻阅计算机生成的快照。大快照很常见,失败也很频繁,这就像是在稻草堆里找针一样。错误是常见的,当工程师的眼睛因为又要翻阅400行快照而烧灼时,运行 jest --u
的冲动就会很强烈。😱
7. 工程师变得对真实的回归问题视而不见
这就像是狼来了的故事。快照测试失败的频率很高,加上审查失败的时间和精力,导致工程师变得条件反射地在快照失败时仅仅更新快照。失败的情况审查得越来越少,bug 就悄悄漏网了,如果其他测试覆盖面假设快照“已覆盖”了这些问题的话。🐛
8. 测试意图不明确
在我看来,组件测试应该是一个很好的文档,供其他工程师快速理解组件的工作方式。大多数我见过的快照测试根本没有提供这种帮助。
在这些测试中,我经常看到一句话:“应该正确渲染”。“正确”到底意味着什么?没有任何先验知识,其他工程师怎么理解什么是“正确”?嗯,我得回到快照中,解读那段机器生成的代码,看看“正确”到底是什么意思……同时祈祷在创建快照时没有做出错误的假设。😱
// 这个测试并没有记录组件实际做了什么
// 我们需要查看快照才能更好地理解
describe("我的复杂组件", () => {
it("应该正确渲染", () => {
const { container } = render(<MyComponent />);
expect(container).toMatchSnapshot();
});
});
9. 增加代码覆盖不完整的可能性
当快照被过度依赖且测试意图不明确、文档不清晰时,很可能导致代码覆盖不完整。
一个笼统的“应该正确渲染”测试通常会掩盖组件可能采取的不同分支和条件,通常只会覆盖到“快乐路径”。这会导致一种虚假的安全感。如果代码中有任何分支,它们往往会被遗漏或未完全覆盖,对于审查的工程师来说,这通常不明显。
测试覆盖受到影响,测试质量下降,bug 也就悄悄溜入了。🐛
10. 替代方案是什么?
那么,如果我们要放弃快照测试,替代方案是什么呢?答案其实并不复杂,关键是依赖明确的测试断言,并结合良好的测试标准。不要把组件的细节埋在大量的快照输出中。为下一个开发者编写测试。
良好的标准意味着:
- 明确组织的测试,清楚地知道:在测试什么,测试的具体场景是什么,预期的结果是什么
- 清晰的测试语句和断言
- 集中的测试范围,保持测试的专注性,不要让测试被过多的断言所干扰
- 测试所有代码路径(在类型检查可以验证的情况下,不必让测试过于冗长)
- 以用户的方式测试组件
一个简化的例子: 停止这样做……
it('应该正确渲染', () => {
const { container } = render(<MyComponent {...props} />);
expect(container).toMatchSnapshot();
});
it('当按钮被点击时,应该正确渲染', () => {
const { container } = render(<MyComponent {...props} />);
const button = screen.getByRole('button', { name: /add/i });
userEvent.click(button);
expect(container).toMatchSnapshot();
});
改成这样做……
it('应该渲染一个添加按钮', () => {
render(<MyComponent {...props} />);
expect(screen.getByRole('button')).toHaveTextContent('Add')
});
it('当按钮被点击时,按钮应该被禁用', () => {
render(<MyComponent {...props} />);
const button = screen.getByRole('button', { name: /add/i });
userEvent.click(button);
expect(screen.getByRole('button')).toBeDisabled()
});
组件测试应该容易编写,而不需要依赖快照(如果你依赖快照,你应该认真考虑重构)。实际上,在当前的 AI 环境中,像 Copilot 这样的工具意味着你甚至不需要自己编写它们!没有借口了。
其他人可能会提出有效的观点,认为快照可以捕捉到你的 DOM 中一些微妙的回归问题,而这些问题可能会被你的替代测试忽略。但在我看来,它们只是半心半意的测试覆盖,和像视觉回归测试这样从用户角度出发的测试相比,它们提供的信心有限,而且弊大于利。
节省时间和精力,去除快照 组件快照所带来的任何价值,都被它们给团队带来的维护麻烦和低效所大大抵消。
测试质量下降,测试覆盖受到影响,bug 也就悄悄进入。用全面且容易理解的高质量测试替代你的快照,我相信你的团队会变得更加高效、快乐,产出更高质量的成果。