首页 / 文章 / 超越覆盖率:测试用户实际遇到的故障情况

超越覆盖率:测试用户实际遇到的故障情况

为何较高的代码覆盖率会掩盖未被测试的故障模式,它所催生的三种徒有其表的测试类型,以及如何编写能够保障实际行为的测试。

920 词

当所有测试都通过且代码覆盖率超过98%时,人们会认为系统是安全的。然而,一旦API返回了UI预期中的对象应该是的null值,或在移动网络延迟的情况下请求处理顺序出错,又或是有人误双击了提交按钮,前端就会立刻变成空白屏幕。事后分析时人们总会问同一个问题:既然代码覆盖率这么高,为什么还会出故障?本文将解释覆盖率究竟衡量的是什么,那些只会提高覆盖率却无法提供实际保护的测试模式,以及如何让测试套件更专注于那些真正重要的故障场景。

覆盖率衡量什么,又不能衡量什么

代码覆盖率工具仅记录测试运行时执行了哪些行、分支和函数,仅此而已。它无法判断是否有断言对结果进行了验证,输入数据是否类似于真实场景的数据,或者当依赖项出现异常时代码是否能正常运行。执行与验证是两个不同的概念,而覆盖率工具仅能报告前者。

一个恰当的类比是建筑检查:检查人员会用手电筒逐一查看每个房间。虽然所有房间都被检查过了,但没人会在暴风雨中测试屋顶的稳定性。高覆盖率只能说明测试覆盖了代码,却不能表明这些测试真正对代码进行了严格验证。

三种会提升覆盖率却无法增强安全性的模式

一旦将百分比设定为目标,人们就会致力于达成这一比例。结果就是人们会用最少的精力编写出能让工具满意的测试。总有三种模式反复出现。

理想化的美好幻象

想象这样一个辅助函数,它负责解析用户输入并更新状态。它的测试会使用格式正确、结构完好的字符串,检查预期输出,并实现完全的分支覆盖。但它从未尝试过处理空字符串、异常字符、undefined类型的参数、响应延迟或格式错误的JSON数据。虽然所有代码行都得到了执行,但那些可能导致实际问题出现的输入却从未被测试过。

被过度模拟的组件

在这里,每一个 API 调用、上下文提供者以及嵌套子组件都会被替换为模拟对象。整个测试流程在几毫秒内完成,且能覆盖所有渲染路径。但在实际生产环境中,真实接口返回的数据结构可能与模拟对象假设的略有不同,或者库在升级后会改变事件触发方式。由于这些测试仅基于开发者自身的假设,因此依然能够通过,而真正的集成测试则是在用户的浏览器中进行的。

缺乏有效断言的测试

最糟糕的情况出现在有严格要求的场景下,比如要求整个团队测试通过率必须达到 90%。这类测试仅仅调用函数来记录代码执行行数,几乎不进行任何断言。尽管没有任何检查机制隔在代码与未来的逻辑变更之间,测试报告仍会显示为通过状态。

如何测试行为而非代码行数

那些能在用户发现之前捕获错误的测试,关注的是系统的功能表现,而非它涉及了哪些代码行。

  • 测试状态与转换,而非单个函数。 用户并不关心某个辅助函数是否被执行。他们关心的是在提交表单过程中网络突然中断,或是在文件上传仍在进行时试图离开页面时会发生什么。需要测试加载指示器、错误状态、重试机制以及错误处理边界。
  • 在可行情况下优先采用集成测试。 对真正的外部系统,如第三方支付网关进行模拟是合理的做法。而对自己内部的辅助函数或数据层进行模拟,往往只能掩盖它们之间的缺陷。应在成本允许的范围内让各个组件和模块共同运行。
  • 让生产环境中的缺陷帮助完善测试套件。当有缺陷出现时,先编写能够复现该缺陷且在修复前会失败的测试,然后再修改代码直至测试通过。如此一来,测试套件中积累的就会是系统实际遇到的边界情况,而非他人臆想的情况。
  • 一种相关且成熟的技术是变异测试,它通过刻意修改代码(如改变条件、删除某行代码)来检查是否有测试会失败。那些仍然通过测试的变异直接指向了那些被执行但未被验证的代码,而这正是覆盖率无法显示的内容。关于组件层面的具体内容,请参阅这些会带来错误信任感的 React 测试反模式。

    将覆盖率视为信号而非目标

    代码覆盖率并非毫无用处。关键模块的较低覆盖率确实是一个警示信号,而测试报告还能揭示那些根本无人测试过的代码路径。问题在于当人们将覆盖率视为唯一目标时,就会更注重测试数量而非测试质量。

    如果选择覆盖率约为65%的精简测试套件,并将重点放在高风险工作流程、复杂的业务规则以及故障恢复机制上,其预防事故的效果远胜于那种仅基于正常流程和模拟数据构建、覆盖率高达95%却脆弱不堪的测试套件。在添加新测试之前,与其问“哪些代码行未被覆盖?”,不如问问“这种测试能否捕捉到哪种真实的故障?”

    核心要点

    • 代码覆盖率仅能显示哪些代码被执行过,无法表明其行为是否经过验证。
    • 基于正常流程的输入数据、大量的内部模拟以及不包含断言的测试,虽然会提升覆盖率,却无法降低风险。
  • 目标状态转换、错误处理以及各模块之间的集成。
  • 先将每个已发现的漏洞转化为失败的测试用例,然后再进行修复。
  • 记录测试套件所防范的故障类型,并将覆盖率数据视为辅助参考。
  • 相关阅读