向更多的端到端测试说:“No”

乔梁 | 2022-03-20

在你生命中的某个时刻,你可能会想起一部你和你的朋友都想看,但后来又都后悔看了的电影。或者你还记得某次你的团队以为已经为你们的产品找到了下一个“杀手级功能”,却在发布后看到了这个功能毫不用处。

好的想法往往也会在实践中失败。在测试领域,有一个很普遍的好想法,却往往在实践中失败,那就是:围绕端到端测试而构建出来的测试策略。

测试人员可以投入很多时间编写多种类型的自动化测试,包括单元测试、集成测试和端到端测试,但这种策略主要投资于验证整个产品或服务的端到端测试。通常,这些测试都是模拟真实的用户场景。

理论上的端到端测试

虽然主要依靠端到端测试是一个坏主意,但人们肯定可以说服一个通情达理的人,这个想法在理论上是有道理的。

首先,谷歌价值观 的第一条就是:「关注用户,其他一切都会随之而来。」因此,关注真实用户场景的端到端测试听起来是个好主意。此外,这一策略广泛吸引了许多拥护者:

  • 开发人员 喜欢它,因为它将大部分(如果不是全部的话)测试工作转移给了其他人。
  • 管理者和决策者 喜欢它,因为模拟真实用户场景的测试用例可以帮助他们轻松确定那些失败的测试用例会对用户有什么样的影响。
  • 测试人员喜欢它,因为他们经常担心漏掉了一个 bug ,或编写了一个无法验证真实行为的测试用例;从用户的角度编写测试通常可以避免这两个问题,并给自己带来更大的成就感。

实际上的端到端测试

那么,如果这种测试策略在理论上听起来很好,那么在实践中又哪里出了问题呢?为了演示,我根据自己和其他测试人员都熟悉的一系列真实经验,展示了下面的合成草图。在这个草图中,一个团队正在构建一个在线编辑文档的服务(e.g., Google Docs)。

让我们假设团队已经有了一些很棒的测试基础设施。每天晚上:

  1. 这个软件服务的最新版本被构建出来。
  2. 再将该版本部署到团队的测试环境中。
  3. 然后,所有端到端测试都在该测试环境中运行。
  4. 最后,向团队发送一份总结测试结果的电子邮件报告。

当团队为下一个版本编写新功能时,最后期限很快也就要到了。为了保持产品质量的高标准,他们还要求至少90% 的端到端测试通过,才能认为功能完整。目前,离截止日期只有一天了:

Days LeftPass %Notes
15%一切都坏了!登录该服务已中断。几乎所有的测试都是用户登录的,所以几乎所有的测试都失败了。
04%我们依赖的一个伙伴团队昨天在他们的测试环境中部署了一个质量糟糕的构建。
-154%昨天(或是前天?),某个开发人员破坏了“保存”功能。然而,有一半的测试用例需要在某个时间点保存文档。开发人员花了一整天的大部分时间来确认,这个问题是前端的 Bug ,还是后端的 Bug。
-254%它是前端的 Bug。今天,开发人员花了半天的时间找到了问题所在。
-354%昨天,一个错误的修复补丁被提交了。不过,这个错误很容易被发现,而且今天就提交了一个正确的修复补丁。
-41%我们测试环境的实验室中发生了硬件故障。
-584%许多小 bug 隐藏在了这个大 bug的背后,例如,登录失败、保存失败等。我们仍旧在修复这些 小 bug 。
-687%我们应该在 90 % 以上的,但不知什么原因,还没有达到。
-789.54%(四舍五入到90%,足够接近。)昨天没有提交任何补丁,这说明,昨天的失败是由于测试用例不可靠导致的。

分析

尽管存在许多问题,但这些测试最终还是发现了真正的漏洞。

得到了什么?

  • 影响客户的错误在到达客户之前就被识别并修复。

出了什么问题?

  • 团队延迟了一周,才完成了他们的编码里程碑(并且加了很多班)。
  • 找到端到端测试失败的根本原因是痛苦的,可能需要很长时间。
  • 合作伙伴的问题和实验室的硬件问题破坏了好几天的测试结果。
  • 许多小 Bug 被那个大 Bug 掩盖了。
  • 端到端测试有时是不可靠的。
  • 开发人员必须等到第二天才能知道修复是否有效。

因此,现在我们知道端到端策略出了什么问题,我们需要改变我们的测试方法,以避免这些问题。但正确的方法是什么呢?

测试的真正价值

通常,一旦测试失败,测试人员这次的工作就算结束了。一个 bug 被归档,然后开发者的任务就是修复这个 bug 。然而,为了确定端到端策略的失败之处,我们需要跳出这个框框,从第一原则着手解决问题。如果我们“专注于用户(其他一切都会随之而来)”,我们必须扪心自问,失败的测试对用户有何好处。答案如下:

一个失败的测试用例不会直接让用户受益

虽然这句话乍一看令人震惊,但却是真的。如果一个产品有效的,那么它就是有效的,无论对它的测试结果是否有效。如果一个产品坏了,它就是坏了,无论测试是否表明它坏了。那么,如果失败的测试用例对用户没有好处,那么用户收益又在哪里呢?

一个修复好的 bug 直接让用户受益

只有当这种意外行为(即 bug )消失时,用户才会感到高兴。显然,要修复 bug ,必须知道 bug 的存在。为了知道 bug 的存在,理想情况下,您需要一个捕获 bug 的测试用例(因为如果测试没有捕获 bug ,用户将找到 bug )。但在整个过程中,从测试失败到错误修复,只有最后一步,才有价值增加。

阶段失败的测试用例找到 Bug 了Bug 被修复了
增加价值么?NoNoYes

因此,要评估任何测试策略,您不能只评估它是如何发现 bug 的。您还必须评估它如何使开发人员能够修复(甚至预防)bug。

建立正确的反馈环

测试会创建一个反馈循环,通知开发人员产品是否正常工作。理想反馈回路有几个特性:

  • 它是快速的。 没有开发人员愿意等待数小时或数天,来确定他们的变更是否OK。有时,这种变更会引起问题(没有人是完美的),反馈循环需要运行多次。更快的反馈循环会带来更快的修复。如果循环足够快,开发人员甚至可以在提交变更之前运行这些测试用例。
  • 它是可靠的。 没有开发人员愿意花几个小时调试一个测试用例,结果却发现它是一个不可靠的测试用例。不可靠的测试降低了开发人员对测试用例的信任,因此不可靠的测试用例经常被忽略,即使它们发现了真正的产品问题。
  • 它隔离了失败。要修复一个 Bug,开发人员需要找到导致 Bug 的特定代码行。当一个产品包含数百万行代码时,漏洞可能在任何地方,就像大海捞针一样。

想得更小,而不是更大

那么,我们如何创建理想的反馈循环呢?通过更小的思考,而不是更大。

单元测试

单元测试只取产品的一小片代码,并单独测试。他们倾向于创建一个比较理想的反馈循环:

  • 单元测试是快速的。我们只需要构建一个小单元来测试它,而且测试也往往非常小。事实上,对于单元测试,十分之一秒都被认为是很慢的。
  • 单元测试是可靠的。一般来说,简单的系统和小型装置受不稳定性的影响要小得多。此外,单元测试的最佳实践——特别是与密闭测试相关的实践——将完全消除不稳定性。
  • 单元测试是隔离了失败的。 即使一个产品包含数百万行代码,如果单元测试失败,你只需要搜索被测试的那个小单元,就可以找到 Bug。

编写有效的单元测试需要依赖关系管理、模拟和密封测试等领域的技能。我不会在这里介绍这些技能,但作为开始,一个给新谷歌(或Noogler)提供的典型例子是谷歌如何构建测试 秒表。

单元测试 vs. 端到端测试

对于端到端测试,您必须等待:先要构建整个产品,然后部署它,最后再运行所有端到端测试。当测试的确在运行时,脆弱的测试往往成为现实。即使测试发现了一个 bug ,这个 bug 也可能会出现在产品的任何地方。 尽管端到端测试在模拟真实用户场景方面做得更好,但这一优势很快会被端到端反馈回路的所有缺点所抵消:

测试测试

单元测试的确有一个主要缺点:即使每个单元在隔离状态下都工作良好,您也不知道它们是否一起工作良好。但是,即便如此,您也不一定需要端到端测试。为此,可以使用集成测试。集成测试需要一小群单元,通常是两个单元,并将它们作为一个整体进行测试,以验证它们是否协调地协同工作。

如果两个单元不能正确集成,为什么要编写一个端到端测试呢?你完全可以编写一个更小、更集中的集成测试来检测同一个 bug 呀?虽然你确实需要想得更大一些,但你只需要想得大一点点儿,就可以验证各个单元是否能协同工作。

测试金字塔

即使有单元测试和集成测试,你可能仍然需要少量的端到端测试来验证整个系统。 为了在所有三种测试类型之间找到适当的平衡,最好使用的视觉辅助工具是「测试金字塔」。

你的大部分测试是金字塔底部的单元测试。 随着您向上移动金字塔,您的测试用例会变大,但同时测试的数量(金字塔的宽度)会变小。

作为一个很好的初步猜测,Google 通常建议 70/20/10 拆分:70% 的单元测试、20% 的集成测试和 10% 的端到端测试。 每个团队的确切组合会有所不同,但总的来说,它应该保持金字塔形状。 尽量避免这些反模式:

  • 倒金字塔/冰淇淋蛋筒。 团队主要依赖端到端测试,使用很少的集成测试,甚至更少的单元测试。

  • 沙漏。 团队从大量单元测试开始,然后在应该使用集成测试的地方使用端到端测试。 沙漏的底部有许多单元测试,而在顶部有许多端到端测试,但中间很少有集成测试。

就像规则金字塔往往是现实生活中最稳定的结构一样,测试金字塔也往往是最稳定的测试策略。

参考阅读

  1. 测试金字塔及其反模式

原文作者:Mike Wacker
原文链接:Just Say No to More End-to-End Tests
发表时间:April 22, 2015