确认:代码覆盖率是无用的管理指标

news2024/10/24 1:52:28

发现拆解代码覆盖率指标的简单证明

代码覆盖率是衡量软件产品质量的一个强有力的指标,多年来,技术领导者们对此深信不疑。从表面上看,其理由似乎很充分:测试越彻底,代码覆盖率就越高,因此,我们的软件就应该越健壮,越能防止错误。这就是我们脑海中根深蒂固的想法。但是,如果我有证据证明代码覆盖率从根本上就是错误的呢?如果我能向你展示这样一个简单的想法,让你不再怀疑呢?那么,请做好准备。

代码覆盖范围

代码覆盖率的最简单形式就是衡量测试 "触及 "或 "覆盖 "了多少代码。我们假设,在我们的产品中,我们至少在每次发布之前都测试并运行了这些测试。当这些测试执行时,它们会对产品执行操作,使代码得以执行。很快,我们就会意识到,如果跟踪测试执行了哪些代码,我们就可以开始衡量执行了多少代码。对于已执行代码与产品中代码总量的比率,我们称之为 "代码覆盖率":

图片

这是一个非常简单的指标。如果我们有100行代码,但测试只执行了其中的75行,那么我们的代码覆盖率就是75%。但很快,我们就意识到了更大的问题。如果代码覆盖率不是100%,那么我们的代码就不会被测试执行,或者换句话说:我们的代码未经测试!

因此,未经测试的代码是危险的,因为它可能包含错误。此外,它还可能包含关键业务功能,如果我们触碰这些代码,就会失去这些功能。所以,代码覆盖率高是必须的。

代码覆盖谬误

但是,现在我们要面对一个谬误:我们知道,揭露代码意味着我们的测试会遗漏重要的情况,但事实并非如此。例如,在前面的例子中,我们的代码覆盖率为75%。换句话说,该指标显示有25%的代码行没有通过任何测试,这就指出了一个风险区域。我们可以肯定地说,任何测试都没有验证这25%的代码库,因此,这可能会成为问题和维护问题的温床。

然而,就在这时,我们有可能陷入谬误:虽然我们可以肯定地说,未经测试的代码隐藏着潜在的错误和对未来开发的阻碍,但我们也可以相信,事实恰恰相反。我们可能会认为,代码被覆盖就意味着它的错误和维护问题更少。但是,这种看似合乎逻辑的直觉可能会被证明是不正确的。现实情况是,我们可以拥有100%的代码覆盖率,但仍然会有错误百出、难以维护的代码。

一个基本例子

试想一个计算两个数字之和的简单函数:


function addition(a, b) {

  return a + b;

}

最简单的测试是什么?只需增加一项,就能执行所有代码:


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% 的软件代码在测试过程中得到验证。这是一种保证软件在不同条件下正确运行的方法。至少,这就是他们的初衷。

所以到底发生了什么?由于没有测试,他们开始创建测试。起初,他们试图创建有意义的测试,彻底检查最关键的功能,验证各种条件下的正确行为。但随着时间的推移,创建这些测试变得越来越困难,代码覆盖率也几乎没有提高。很快,他们意识到自己在与时间赛跑。

绝境需要绝招。他们将重点从创建有价值的测试转移到提高代码覆盖率上。他们执行测试,查看代码覆盖率报告,调整测试以通过代码的最大部分,从而快速提高代码覆盖率。他们放弃了对有用测试的任何考虑,将数量置于质量之上。历时三个月,他形容这是他整个开发人员生涯中最糟糕的经历。

现在,你可能会想,这是一种极端的情况,他们的行为至少是有问题的,而且,这肯定不是软件行业的普遍做法。那么,请再想一想。事实证明,每个开发人员在每次交付时都会遇到同样的定时炸弹。因此,如果开发人员被迫交付带有测试的代码,具有一定的最小代码覆盖率,并满足任意的截止日期要求(即使他们已经估计了截止日期),那么之前的经验也同样适用。

这就是我的第二次经历。前段时间,我的一位客户要求我协助他的团队进行一项测试。当时有很多关于测试的讨论,大家都觉得测试既费钱又费时。公司要求至少有 80% 的代码覆盖率,整个情况让我想起了之前的经历。

于是,我做了唯一合乎逻辑的事情:我下载了代码,查看了测试,一个小时后,我发现自己无法理解其中的任何内容。我进行了测试,测试通过了,于是我开始做实验。因为我不明白测试到底是如何进行的,所以我拿到了代码,并故意把它弄坏了。结果让我大吃一惊:虽然代码被破坏了,但测试仍然通过了。之所以能实现代码覆盖率,不是因为测试工作做得很彻底,而是因为他们不小心运行了代码。这两次经历都给了我一个明确的提示:强迫代码覆盖率并不是一种好的管理方法。

实验

按照约定,我将介绍一个简单而有效的实验,它将证明代码覆盖率作为管理指标毫无疑问是无用的。

它是基于艾伦-霍卢布的以下观点:

这个想法很简单,对吗?正如我前面提到的,要实现100%的代码覆盖率,我们只需满足两条规则:1) 执行所有函数,2) 执行所有分支。事实证明,Allen Holub的建议正是如此:1)让测试执行所有函数/方法;2)使用随机参数覆盖分支。

如果我们这样做了,那么这种测试会对我们的业务目标产生什么影响呢?什么都没有!它只会无情地运行所有代码,而不会考虑我们的业务。这将是懒惰的开发人员的终极表现。

那么,问题来了:Allen Holub说得对吗?创建自动代码覆盖可能很有挑战性,但如果我们仅限于随机输入,而不需要分析代码分支,其复杂性就会大大降低。那么,让我们开始吧!在第一种方法中,我选择了Java。由于具有反射功能,Java是一种相当容易实现自动测试的语言,而且我已经有了一些公共代码库,可以用来检查生成器。因此,我在这里做了第一个概念验证:

 

public class TestStage1 {


    @Test

    public void testEverything() throws Exception {

        String packageName = "com.drpicox.game";


        List<Class<?>> classes = getClassesFromPackage(packageName);


        for (Class<?> clazz : classes) {

            try { executeMethods(clazz); } catch (Throwable t) {}

        }

    }


    private void executeMethods(Class<?> clazz) throws Exception {

        // Get the default constructor of the class

        var constructor = clazz.getConstructor();


        // Instantiate the class using the default constructor

        Object instance = constructor.newInstance();


        // Get all the methods of the class

        Method[] methods = clazz.getDeclaredMethods();


        // Iterate through the methods and invoke them

        for (Method method : methods) {

            // Check if the method is public and has no parameters

            if (method.getParameterCount() == 0 && Modifier.isPublic(method.getModifiers())) {

                System.out.println("Executing method: " + method.getName());

                // Invoke the method on the instance

                method.invoke(instance);

            }

        }

    }


    private static List<Class<?>> getClassesFromPackage(String packageName) throws Exception {

        List<Class<?>> classes = new ArrayList<>();

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

        String path = packageName.replace(".", "/");

        for (java.net.URL resource : java.util.Collections.list(classLoader.getResources(path))) {

            String file = resource.getFile();

            if (file.contains("!")) {

                continue;

            }

            if (file.endsWith(".class")) {

                String className = packageName + "." + file.substring(path.length() + 1, file.length() - 6);

                classes.add(Class.forName(className));

            } else {

                File dir = new File(resource.getFile());

                if (!dir.isDirectory()) {

                    continue;

                }

                // Call the helper method to process the directory

                processDirectory(dir, packageName, classes);

            }

        }

        return classes;

    }


    private static void processDirectory(File dir, String packageName, List<Class<?>> classes) {

        for (var file : dir.listFiles()) {

            if (file.isDirectory()) {

                // If it's a directory, recurse into it

                processDirectory(file, packageName + "." + file.getName(), classes);

            } else if (file.getName().endsWith(".class")) {

                // If it's a .class file, add it to the list of classes

                String className = packageName + "." + file.getName().substring(0, file.getName().length() - 6);

                try {

                    classes.add(Class.forName(className));

                } catch (ClassNotFoundException e) {

                    e.printStackTrace();

                }

            }

        }

    }


}

这段简单的代码只创建所有带有公共构造函数且不带参数的类实例,并执行所有不带参数的方法。虽然它很简单,但已经实现了11%的代码覆盖率。虽然远低于80%,但这是意料之中的。

此时,我需要开始执行带有参数的构造函数和方法。此外,我还可以通过“作弊”的方式直接执行私有方法,使用Spring或JPA所依赖的相同机制。这就打开了一个新的兔子洞。因此,在这一点上,有了正确方向上的第一个概念证明,以及作为大学老师将此实验转化为最终学位项目的机会,我决定将此实验列入提供的最终学位项目中。

在此,我不得不说,我非常感谢Gerard Torrent。他接受了挑战,虽然他们的学位几乎没有编译器理论方面的知识,但他创造了一种不同的方法,让我们更好地理解了编译器理论。他建立了一个代码生成器,为每个方法和可能的参数创建一个测试,而不是做一个走遍所有代码的测试。他不断增加功能,比如当方法需要其他对象时,他就创建这些对象,一次又一次地迭代,从而提高了代码的整体覆盖率。有时,他独自工作。有时,他独自工作;有时,我们联手进一步提高代码覆盖率。

结果

我们实现了80%的代码覆盖率,甚至更高。我请Gerard进行迭代,一步一步地得出结果,以便更深入地了解代码覆盖的工作原理。因此,一步步实现的代码覆盖率是:

  • • 我的首次参考实施:11%

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

  • • 只执行公共无效方法:23%

  • • 执行所有公共方法:50%

  • • 执行所有方法,包括公共方法和私人方法:50%

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

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

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

  • • 尽可能使用Spring对类进行实例化:85%

请注意,测试私有方法是一种反模式,不要这样做,但它是本演示的一部分,因为它有助于人为地增加代码覆盖率。

因此,最终的结果是:85%的代码覆盖率。这就是在不考虑任何业务因素的情况下生成代码。那么,现在怎么办?

结论

Allen Holub之所以在评论中将80%作为目标,并不是因为他认为这是一个合理的目标--他可能会这么认为—— 因为80%是大多数公司的共同要求。他在寻找一种方法来驳斥强制代码覆盖率最低的必要性。因此,现在我们知道,我们可以构建一个简单的库,无论你的业务是什么,它都可以执行大部分代码,并人为地提高代码覆盖率。我们不需要人工智能,不需要花哨的LLM,不需要代码复杂性分析,只需执行随机函数,就能满足任何公司对最低代码覆盖率的大部分要求。

即使在代码覆盖率可以稍高一些的公司,也可以通过手工测试来达到额外的覆盖率要求。那么,将代码覆盖率作为管理指标的结果是什么呢?一无所获。以前,我们知道开发人员可以通过伪造代码来提高代码覆盖率,而无需进行测试。现在,我们也知道快速自动工具可以迅速提高覆盖率。因此,如果仅仅随机执行代码就能达到很高的代码覆盖率,那么这个指标就失去了作用。

下一步

下一步该怎么做?既然代码覆盖率对管理毫无用处,我们现在能做什么?

首先,也是最重要的一点:代码覆盖率对开发人员来说仍然很重要。Martin Fowler等许多人早就说过这一点。他在这篇文章中解释说,代码覆盖的唯一目的是找到未经测试的代码。这有助于开发人员发现自己在创建代码时的错误和错误的假设。如果应用得当,代码覆盖失败可以引发重要的业务对话,从而发现新的功能或误解。

图片

其次是TDD或BDD。毫无疑问,这可能是创建测试的唯一合理方法。当开发人员被迫创建测试,而且是在代码之后创建测试时,主要的问题是没有人能确保这些测试能正确工作。我们需要观察它们的失败,看看新代码是如何纠正这些失败的,只有这样才能让我们确信我们创建的测试是正确的。

最后,我们应该专注于业务。期间。只有当测试有助于验证业务主张是否按预期运行时,测试才有意义。因此,与其依赖只关注代码的晦涩指标,我们可以选择其他更关注业务的指标。业务规则覆盖率就是一个例子:

图片

这是一个相当简单的指标,类似于代码覆盖率,但也存在一些问题。

"工作软件是衡量进步的主要标准"——《敏捷宣言》原则。感谢你的阅读。我通常喜欢通过写故事来思考我们是如何理解和应用软件工程的,并让我们思考可以改进的地方。

最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:【文末自行领取】

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2213053.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据屏蔽与加密:代理用户需要了解的内容

您可能已经意识到数据安全和隐私的重要性。尽管存在各种方法来解决这两个问题&#xff1a;道德考量和监管要求&#xff0c;在本指南中&#xff0c;我们将重点介绍两种流行的策略&#xff1a;屏蔽和加密 - 以及它们的比较。 那么&#xff0c;哪个方法更胜一筹呢&#xff1f;答案…

时间序列预测(三)——激活函数(Activation Function)

激活函数是神经网络中每个神经元的输出函数&#xff0c;用于引入非线性&#xff0c;从而使神经网络能够逼近复杂的非线性关系。没有激活函数的网络只能表示线性变换&#xff08;如上一篇的线性回归不需要激活函数&#xff09;&#xff0c;因此不能解决实际中的非线性问题。激活…

【Python】从零到一,搭建高效Web服务器,轻松上手!Python开发者必备(文末附带源码分享)

CSDN Python源码分享&#xff1a;实现一个简单的Web服务器 在CSDN上&#xff0c;我们经常分享各种技术文章和源码&#xff0c;帮助开发者们不断提升自己的技能。今天&#xff0c;我将为大家分享一个使用Python实现的简单Web服务器源码。这个Web服务器能够处理基本的HTTP GET请…

Java项目实战II基于Java+Spring Boot+MySQL的桂林旅游景点导游平台(源码+数据库+文档)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者 一、前言 桂林&#xff0c;以其独特的喀斯特地貌、秀美的自然风光闻名遐迩&#xff0c;每年吸引着无数国内外游…

(04)python-opencv图像处理——图像阈值、平滑图像、形态转换、图像梯度

目录 前言 一、图像阈值 1.1 简单的阈值法 1.2 自适应阈值 二、平滑图像 2.1 二维卷积(图像滤波) 2.2 图像模糊 2.2.1均值模糊 2.2.2高斯模糊 2.2.3 中值滤波 2.2.4 双边滤波 三、形态转换 1、腐蚀 2、膨胀 3、开运算 4、闭运算 四、图像梯度 Sobel 和 Scharr …

【Pycharm系列】如何使用Windows的pycharm来远程连接linux做开发?

目录 前言一、原因二、步骤2-1、打开配置2-2、新建SFTP连接2-3、添加SSH连接信息2-4、配置连接信息2-5、构建连接2-6、打开远程项目文件目录2-7、配置项目依赖 总结 前言 使用Linux部署&#xff0c;使用Windows远程开发&#xff0c;可以提升开发效率&#xff0c;以及项目运行的…

使用可白嫖的高配置服务器——DAMODEL进行AI开发教程

DAMODEL&#xff1a;DAMODEL 目前DAmodel注册并实名赠送50大洋的免费额度&#xff0c;搭载4090的服务器费用不到2r/h 教程&#xff1a; 完成注册并实名后 在此点击创建实例 选择实例配置 选择镜像&#xff0c;看你使用哪种dl框架 。 实例自带的磁盘会随实例释放。需要自己…

Python获取盘符并创建文件夹-基于window系统

1、问题概述? 提供代码详解及完整应用代码 在项目开发中,我们有时候需要当前计算机的盘符,并判断那个盘符的空间最大,然后再最大空闲盘符中创建目录。实现如下功能 1、获取所有盘符信息。 2、获取盘符的空间,并判断大小 3、在盘符中创建需要的目录 …

osgEarth 键鼠 增删改 feature Node

为了满足shapefile 编辑&#xff0c;实现键鼠对地理要素的增删改。 读取shapefile&#xff0c;用Geometry Feature FeatureNode绘制在osgEarth上&#xff1b; 自定义osgGA::GUIEventHandler&#xff0c;handle函数中监测osgGA::GUIEventAdapter::PUSH 之前疑惑在拾取&#x…

已解决:“发生生成错误,是否继续并运行上次的成功的生成?”无法启动程序,系统找不到指定的文件

版本&#xff1a;Visual Studio 2022用于C开发 目录 问题描述 问题原因 解决办法 问题描述 代码没有问题&#xff0c;运行后出现如下界面&#xff1a; 点击“是”后&#xff0c;又出现如下问题&#xff1a; 问题原因 源程序文件下出现两个main函数。 像我的文件目录下的另…

Allegro PCB中过孔的整体替换

Cadence Allegro PCB中过孔的整体替换 在PCB设计过程中&#xff0c;之前是使用的小的过孔&#xff0c;后面需要替换成大的过孔&#xff0c;一个一个去替换过孔非常麻烦的&#xff0c;这里&#xff0c;讲解一下如何去整体的替换过孔&#xff0c;具体的操作方法如下所示&#xf…

微软推出最新 Azure 虚拟机 ND H200 v5 系列

声明&#xff1a;本文翻译自微软全球官方博客&#xff0c;ND H200 v5 系列虚拟机目前只在 Microsoft Azure 海外版上发布。 随着人工智能领域的高速发展&#xff0c;企业对于可扩展和高性能基础设施的需求呈指数级增长。客户需要 Azure AI 基础设施来开发智能驱动的创新解决方案…

HUAWEI_HCIA_实验指南_Lib2.1_交换机基础配置

1、原理概述 交换机之间通过以太网电接口对接时需要协商一些接口参数&#xff0c;比如速率、双工模式等。交换机的全双工是指交换机在发送数据的同时也能够接收数据&#xff0c;两者同时进行。就如平时打电话一样&#xff0c;说话的同时也能够听到对方的声音。而半双工指在同一…

Linux高性能服务器编程

文章目录 Linux高性能服务器编程一、TCP/IP协议族1.TCP/IP体系结构图2.ARP协议2.1 ARP协议工作原理2.2 以太网ARP请求/应答报文格式2.3 ARP高速缓存的查看与修改 3. DNS协议3.1 DNS 查询和应答报文 二、IP协议详解1.路由表更新 三、TCP1.特点2.字节流3.TCP头部结构4.三次握手与…

双卡双待功能

双卡功能&#xff0c;指的是设备上安装和使用了两张SIM卡的功能&#xff0c;这两张SIM卡可以来自同一运营商&#xff0c;也可以来自不同的运营商。设备可以选择使用其中一张SIM卡&#xff0c;或者两张同时使用。当然&#xff0c;能否两张SIM卡同时使用&#xff0c;还取决于设备…

Python 语言学习——做题记录 2.3

这次主要练习集合这一数据类型。 P1. 洛谷B3633集合运算2 import sys n1input() a1sys.stdin.readline() a2a1.split() A{int(i) for i in a2} #print(A) n2input() b1sys.stdin.readline() b2b1.split() B{int(i) for i in b2} #print(B)print(len(A)) CA&B DA|B Uset(ra…

全球化智能组网基于多技术混合组网,适用于各行业的全球办公组网

在全球化的今天&#xff0c;企业的业务网络不仅需要覆盖更广泛的地理区域&#xff0c;同时也要能够灵活应对各种复杂的业务场景。为此&#xff0c;中国联通国际公司推出了全球化智能组网服务&#xff0c;该服务以中国联通云联网为核心&#xff0c;结合SD-WAN、多云连接&#xf…

echarts图例右侧竖向排列修改图例文字颜色

实操链接戳这里哈 left代表图例在水平放置的位置&#xff0c;有left、center、right top代表图例在垂直方向的位置&#xff0c;有top、middle、bottom width是最主要的&#xff0c;当设置的宽度比较小时&#xff0c;才会迫使图例换行&#xff0c;从而形成竖直排列的现像。 lege…

SpringBoot2核心功能-数据访问

目录 一、数据源的自动配置-HikariDataSource1、导入JDBC场景2、分析自动配置3、修改配置项4、测试 二、使用Druid数据源2.1、druid官方github地址2.2、自定义方式2.2.1、创建数据源2.2.2、StatViewServlet2.2.3、StatFilter 2.3、使用官方starter方式2.3.1、引入druid-starter…

DS线性表之队列的讲解和实现(5)

文章目录 前言一、队列的概念及结构二、队列的实现队列节点和队列初始化销毁判断是否为空入队列出队列获取队头队尾数据获取队列元素个数 三、实际使用效果总结 前言 队列实现源代码   队列是我们遇到的第二个实用数据结构&#xff0c;栈和队列地位等同 一、队列的概念及结构…