AICon上海|与字节、阿里、腾讯等企业共同探索Agent 时代的落地应用 了解详情
写点什么

代码覆盖率是一个无用的管理指标

  • 2023-10-06
    北京
  • 本文字数:5359 字

    阅读完需:约 18 分钟

大小:2.61M时长:15:10
代码覆盖率是一个无用的管理指标

多年来,技术领导者们普遍认为代码覆盖率是衡量软件产品质量的一个强有力的指标。这种观点表面上看起来合理:测试越彻底,代码覆盖率就越高,软件就应该更加健壮、更不容易出错。这个想法已经深深地植根于我们的脑海中。但是,如果我有证据证明代码覆盖率本质上是错误的呢?如果我能通过向你展示一个简单的想法让你不再怀疑我的观点呢?

代码覆盖率

 

简单地说,代码覆盖率是衡量测试“触及”或“覆盖”了多少代码的一种指标。假设我们的软件产品包含了测试用例,并至少在每次发布之前都执行这些测试。在执行这些测试时,它们会对产品执行操作,从而让代码跑起来。很快,我们就意识到,如果跟踪哪些代码被测试用例执行了就可以度量执行了多少代码。我们把被执行的代码与产品中总代码量的比例叫作“代码覆盖率”。

 


 这是一个非常简单的度量指标。如果我们有 100 行代码,但测试只执行了其中的 75 行,那么我们的代码覆盖率就是 75%。

 

很快,我们就意识到了更重要的事情:如果代码覆盖率不是 100%,那就还有没有被测试执行的代码,就是未经测试的代码!

 

拥有未经测试的代码是危险的,因为它们可能存在缺陷。此外,它们还可能包含关键的业务功能,如果我们修改了这些代码,可能会丢掉这些功能。所以,拥有高代码覆盖率是必须的。

代码覆盖率谬论

 

但现在我们的面前摆着一个谬论:我们知道,拥有未覆盖的代码意味着我们的测试可能遗漏了重要的场景,但反过来并不成立。

 

例如,在前面的例子中,我们的代码覆盖率是 75%。换句话说,这表明 25%的代码行根本没有被任何测试执行过,这显然向我们指出了一个风险点。我们可以肯定地说,这 25%的代码没有经过任何测试验证,因此可能成为缺陷和维护问题的温床。

 

然而,这也是我们可能会陷入谬论陷阱的地方:虽然我们可以自信地说未经测试的代码隐藏着潜在的错误和对未来开发的阻碍,但我们可以相信反过来也是对的。我们可能会认为覆盖了代码意味着它有更少的错误和更少的维护问题。但是,那只是一种直觉,甚至看起来似乎合理,而事实却并非如此。

 

事实是,我们可以拥有 100%的代码覆盖率,但仍然有满是错误和难以维护的代码。

一个简单的例子

 

假设我们有一个简单的计算两数之和的函数:

function addition(a, b) {  return a + b;}
复制代码

 

能覆盖 100%代码的最简单的测试是怎样的?只需要进行一次加法运算就能覆盖到所有代码:

test('the addition function', () => {  addition(3, 4);});
复制代码

 

这个测试覆盖了 100%的代码。然而,它是无用的。为什么?如果我们把加法实现改成这样:

function addition(a, b) {  return a - b;}
复制代码

 

测试仍然可以通过!

 

如果你是一名程序员,你可能已经知道问题出在哪里。问题不在于代码覆盖率,而在于测试本身。测试确实覆盖了 100%的代码,但它并没有断言或检查任何东西。这就是为什么错误地实现逻辑(用减法代替加法)仍然能够通过测试。所以,这似乎是一个糟糕的例子……不是的。

 

事实证明,对于这个非常简单的小例子,我们可以很容易地看到测试中的问题。但如果代码库里有成千上万行代码呢?有人能轻松地找出一个没有正确验证其结果的测试吗?这是极不可能的。

 

所以,测试可能是错误的,断言也可能是错误的,场景可能被忽略,但我们仍然可以吹嘘拥有 100%的代码覆盖率。问题正好出在这里。

问题的根源

 

这个问题的根源在于,代码覆盖率是关于代码的度量指标,而不是关于业务的。

 

尽管代码覆盖率可能是揭示未被测试的代码的一个很好的指标,但它并不能告诉我们与业务相关的东西以及项目是如何满足业务目标的。

 

代码覆盖率关注软件测试的技术层面,但不一定会考虑在构建软件时需要满足的更广泛的业务目标和需求。它衡量了被测试代码的范围,但并不能提供关于软件是否真正达到其预期目的、满足用户需求或与更广泛的业务战略保持一致的见解。

 

代码覆盖率唯一做到的就是评估你是否在测试期间执行了所有代码,而这很容易就可以达到。

 

规则 1:执行所有方法。为每一个函数编写一个测试用例。这将会覆盖到所有方法。所以,如果你有两个函数,就写两个测试用例。

function one() {  // ...}test('function one', () => {  one();});

function two() { // ...}test('function two', () => { two();});
复制代码

 

规则 2:执行所有分支。为每一个条件语句创建一个额外的测试用例,确保它满足条件。这将覆盖所有分支内的所有代码。

function conditional(condition) {  if (condition) {    // ...  } else {    // ...  }}

test('condition true', () => { conditional(true);});test('condition false', () => { conditional(false);});
复制代码

 

到需要注意的是,要达到 100%的代码覆盖率并不总是需要编写额外的测试:

function conditional(condition) {  if (condition) {    // ...  }   // ...}

test('conditional', () => { conditional(true);});
复制代码

 

不需要更多的规则了。我已经展示了“if”语句,但“while”和“switch”也是如此。对其他函数的调用已经被规则 1 覆盖了,所以,就些就够了。

 

那么这些规则与业务有什么关系呢?没有。而这就是问题所在。

 

真实的经历

 

我想讨论两种不同的情况,代码覆盖率在其中扮演了欺骗性的角色。

 

几年前,在一个聚会上,我遇到了一位在软件开发公司工作的开发人员,他向我讲述了他为 FDA(美国卫生与公众服务部下属的联邦机构食品和药物管理局)开发软件产品的经历。

 

情况是这样的:FDA 要求 60%的代码覆盖率,而他们的产品没有测试用例,所以代码覆盖率是 0%。

 

当 FDA 要求 60%的代码覆盖率时,意味着他们希望看到至少 60%的代码在测试期间被执行。这是一种保证软件在不同条件下可以正常运行的方式,或者至少他们希望如此。

 

那么真实发生了什么?

 

因为他们没有测试用例,所以开始着手创建测试用例。最初,他们试图创建有意义的测试,彻底检查最关键的功能,并在各种条件下验证正确行为。但随着时间的推移,继续创建测试变得越来越困难,而代码覆盖率几乎没有增加。很快,他们意识到自己在与时间赛跑。

 

绝望的时刻需要绝望的措施。他们将注意力从创建有价值的测试转移到简单地增加代码覆盖率百分比上。他们执行测试,查看代码覆盖率报告,然后调整测试,最大程度地执行更多的代码,以此来快速提升代码覆盖率。他们不再考虑测试是否有价值,因为他们将数量置于质量之上。

 

这花了他们 3 个月,他说这是他整个开发职业生涯中最糟糕的经历。

 

你可能在想,这是一种极端的情况,至少,他们的行为是值得怀疑的,而且,这肯定这不是软件行业的常见做法。别着急,再仔细想想。

 

事实证明,每一个开发人员在每一次交付时都会踩到同样的定时炸弹。

 

所以,如果一个开发人员被迫交付带有测试用例的代码,并要求具备一定的最低代码覆盖率,并且需要在某个截止日期内完成(即使是他们自己估计的),那么前面经历的教训也同样适用。

 

我的第二次经历就是这样的。不久前,我的一个客户要求我协助他的一个团队进行测试。关于测试有很多需要讨论的地方,感觉测试既费钱又费时间。这家公司要求至少 80%的代码覆盖率,这让我想起了之前的经历。

 

所以,我做了唯一一件合理的事情:我下载了代码,查看了测试。一个小时后,我意识到我无法理解其中的任何一个测试用例。

 

我运行了测试,它们通过了,然后我开始尝试做一些试验。因为我不明白这些测试用例是如何工作的,所以我拿到了代码并故意破坏它们,结果让我吃惊:尽管代码被破坏了,测试仍然可以通过。

 

代码覆盖率之所以达到要求,并不是因为测试得彻底,而是因为它们只是偶然地执行到了代码。

 

这两次经历都在告诉我同一个道理,强制要求代码覆盖率可能不是一个好的管理实践。

 

实验

 

正如之前所承诺的,我将展示一个实验,一个简单而有效的实验,它将毫无疑问地证明作为管理指标的代码覆盖率是无用的。

 

它基于 Allen Holub 的观察:

 


推文内容:我想过写一个自动代码覆盖率生成器,它只创建测试,这些测试通过随机参数调用程序中的每一个函数/方法,并且总是能通过。达到 80%的覆盖率小菜一碟。可见代码覆盖率并不是一个有用的指标。

 

这个想法很简单,对吧?正如我前面提到的,要达到 100%的代码覆盖率,我们只需要满足两个规则:一个是执行所有函数,一个是执行所有分支。事实证明 Allen Holub 也是这么想的:一个是让测试执行所有函数/方法,一个是通过使用随机参数来覆盖分支。

 

如果我们真是这么做的,那么这些测试与我们的业务目标会有什么关系呢?一点都没有!它只会毫不留情地运行所有代码,而不考虑我们的业务。

 

所以,问题是:Allen Holub 说的是对的吗?

 

自动化生成代码覆盖率可能有点困难,但如果我们限定在随机输入的前提下,而不需要分析代码分支,那么它的复杂性就会大大降低。那么,让我们开始实验吧!

 

在我的第一次尝试中,我选择了 Java。因为 Java 具有反射能力,所以它是一种非常容易用于自动化测试的语言,并且我已经有一些公共代码库可以用来检查生成器。所以,我在这里做了第一次概念验证:

 

https://github.com/drpicox/classroom--cards-game--2022/blob/feature/autotest/src/test/java/com/drpicox/stage1/TestStage1.java

 

这段简单的代码只创建了具有公共构造函数和无参数的类的所有实例,并执行了所有没有参数的方法。

 

尽管它很简单,但已经达到了 11%的代码覆盖率。这远低于 80%,但这是预料之中的。

 

到了这里,我意识到我需要执行带有参数的构造函数和方法,而且可以通过“作弊”直接执行私有方法,采用与 Spring 或 JPA 所采用的相同的机制。这打开了一个新的兔子洞。所以,有了指明了正确方向的第一次概念验证,以及可以将这个实验作为学位项目的可能性,我决定将这个实验列入学位项目的通过资格中。

 

在这里,我必须感谢 Gerard Torrent。他接受了挑战,尽管他们的学位内容几乎与编译器理论无关,但他还是创造了一种不一样的更容易被人们理解的方法。

 

他不是通过一个单一的测试来遍历所有代码,而是构建了一个代码生成器,为每一个方法和可能的参数创建一个测试。他还添加了其他功能,例如,如果一个方法需要其他对象,就创建它们,经过一轮又一轮的迭代,整体代码覆盖率得到了提升。有时侯他独自工作,有时候与我联手进一步提高覆盖率。我们做到了。

结果

 

是的,我们做到了。我们达到了 80%的代码覆盖率,甚至更高。

 

我让 Gerard 进行逐步迭代,并获取结果,以便能够更深入地了解代码覆盖率是如何实现的。

 

代码覆盖率是这样逐步实现的:

 

  • 我的第一个参考实现:11%

  • 执行所有以 null 作为参数的构造函数:20%

  • 只执行 public void 方法:23%

  • 执行所有的 public 方法:50%

  • 执行所有 public 和 private 方法:50%

  • 创建所需参数的实例(不再有 null):65%

  • 为所需的实例创建实例(嵌套):69%

  • 每个参数测试三个不同的值:69%

  • 在可能的情况下使用 Spring 实例化类:85%

 

需要注意的是,测试私有方法是一种反模式,请不要这么做。它只是本演示的一部分,因为它可以帮助人为地提高代码覆盖率。

 

所以,最后的结果是:85%的代码覆盖率

 

这是在不考虑业务逻辑的情况下生成代码,然后呢?

 

结论

 

Allen Holub 之所以提出 80%的覆盖率,并不是因为他认为这是一个合理的目标,而是因为 80%是大多数公司的普遍要求。事实上,他正在寻找一种方法来证明强制代码覆盖率最低要求是错误的。

 

现在我们知道,我们可以构建一个简单的库,无论业务是什么,它都可以执行大部分代码,并人为地提高代码覆盖率。我们不需要 AI、花哨的 LLM、代码复杂度分析,只需要随机地执行函数,你就能满足任何一家公司对最低代码覆盖率的要求。

 

即使在那些代码覆盖率可能略高一些的公司,你也可以通过抛出几个手写的手动测试用例来达到额外的覆盖率要求。

 

那么,将代码覆盖率作为管理指标会为我们带来什么?什么也没有。

 

以前,我们知道开发人员可以在不执行测试的情况下通过伪造来达到更高的代码覆盖率,而现在我们知道我们也可以通过自动化工具来快速提高覆盖率。

 

所以,如果只是通过随机执行代码就能达到很高的代码覆盖率,那么这个指标就没什么用了。

 

下一步

 

下一步是什么?现在我们知道代码覆盖率对管理无用,那么我们能做些什么?

 

首先也是最重要的是:代码覆盖率对开发人员仍然很重要。这已经被包括 Martin Fowler 在内的许多人讨论了很长时间。他在一篇文章中解释说,代码覆盖率的唯一目的是找到未经测试的代码。这有助于开发人员发现他在写代码时犯下的错误和错误假设。如果用对了代码覆盖率,当它处于较低的水平时,有助于引发重要的业务讨论,并揭示新的功能可能性或被人们误解的东西。



代码覆盖率的作用(Martin Fowler)

 

其次,我们有 TDD 或 BDD,它们可能是创建测试的唯一合理的方法。开发人员可能被要求在写好代码之后创建测试,其主要问题在于没有人能够确保这些测试可以正确执行。我们需要看到它们失败,并看着新代码如何纠正它们,只有这样才能让我们确信我们正确地创建了这些测试。

 

最后,我们应该关注业务。测试只有在能够直接帮助我们验证业务逻辑按照预期执行时才有意义。因此,与其依赖只关注代码的晦涩指标,不如选择更关注业务的其他指标,例如业务规则覆盖率:



这也是一个相当简单的指标,与代码覆盖率非常相似,也存在一些问题,但由于它更多地关注业务,所以比代码覆盖率更加有效。


原文链接:

https://drpicox.medium.com/confirmed-code-coverage-is-a-useless-management-metric-35afa05e8549

2023-10-06 07:005121

评论 1 条评论

发布
用户头像
这个指标是必要不充分。
2023-10-09 11:18 · 福建
回复
没有更多了
发现更多内容

干货 | 如何实现软件自动化部署?

嘉为蓝鲸

运维 IT 应用发布 应用部署

大规模数据如何实现数据的高效追溯

华为云开发者联盟

大数据 后端 华为云 企业号九月金秋榜

什么是数据质量管理?企业怎样做好数据质量管理?

雨果

数据质量

NFT软件开发:什么是数字藏品?

开源直播系统源码

NFT 数字藏品 数字藏品系统软件开发 数字藏品开发

TiFlash 源码解读(八)TiFlash 表达式的实现与设计

PingCAP

源码阅读 TiDB TiDB 源码解读

OneFlow源码解析:Tensor类型体系与Local Tensor

OneFlow

深度学习 源码解析 算子

Android技术分享| Activity 过渡动画 — 让切换更加炫酷

anyRTC开发者

android 音视频 动画 移动开发 Activity

微服务治理热门技术揭秘:动态读写分离

阿里巴巴中间件

数据库 阿里云 微服务 云原生

写出优秀的产品手册文档的技巧

Baklib

文档 产品手册

Java开发培训的就业方向有哪些?

小谷哥

太牛了,这是我见过把微服务讲的最全最好的SpringCloud架构进阶

程序知音

Java 架构 微服务 SpringCloud 后端技术

融云视频会议,助力政企高效协同

融云 RongCloud

会议 音视频技术 政企

Java培训学习技术需要具备哪些能力

小谷哥

学员在web前端培训机构应该怎么学习

小谷哥

Linux vim的使用和配置

挚爱光小胖

Linux vim教程

elasticsearch的字符串动态映射

程序员欣宸

elasticsearch 9月月更

零信任态势评估:安全控制自动化

权说安全

零信任 动态评估

Baklib|FAQ常见问题对产品推广的重要性

Baklib

产品 FAQ

设计模式的艺术 第十四章享元设计模式练习(开发一个多功能文档编辑器,在文本文档中可以插入图片、动画、视频等多媒体资料。为了节省系统资源,相同的图片、动画和视频在同一个文档中只需保存一份,但是可以多次重复出现,而且它们每次出现时位置和大小均可不同)

代廉洁

设计模式的艺术

贝斯的圆桌趴 |科技公司内部 SaaS 工具大公开

Bytebase

Linux系统安装配置Tomcat

Linux Tomccat 9月月更

软件测试 | 测试开发 | Jenkins 踩坑(四)|基于接口自动化测试完成 Jenkins+GitHub+Allure 的结合

测吧(北京)科技有限公司

测试

画一个 “月饼” 陪我过中秋,使用 ESP32-C3 制作炫彩月饼

矜辰所致

电路设计 ESP32-C3 9月月更

python os模块

zxhtom

9月月更

知识管理对企业的作用不容小觑

Baklib

知识管理 企业

如何选择比较靠谱的数据培训班?

小谷哥

如何在 ACK 中使用 MSE Ingress

阿里巴巴中间件

阿里云 容器 微服务 云原生 ingress

5 分钟比较理解 require() vs import()

掘金安东尼

前端 9月月更

怎么来选择大数据培训课程

小谷哥

干货 | 企业数字化转型过程中,传统IT和数字型IT能否严格区分?

嘉为蓝鲸

运维 转型 IT 数字化 研发

BI系统的分布式部署原理和技术实现

葡萄城技术团队

分布式 BI 部署 可视化数据

代码覆盖率是一个无用的管理指标_管理/文化_David Rodenas_InfoQ精选文章