扫码阅读
手机扫码阅读

关于代码覆盖率,看这一篇足矣!

1519 2023-07-13

导读:关于代码覆盖率的话题,在之前我分别翻译了四篇相关的文章(详见下面链接),不过每一篇都没有达到自己心中的要求。作为对细节要求有“洁癖”的我,还是愿意再花时间去好好的梳理和总结,希望带给读者非常清晰和富有逻辑的文章,这就是这篇文章的意义所在。
相关阅读:

代码覆盖率和测试覆盖率到底是不是一回事?

用测试覆盖率度量代码质量真的靠谱?

不要被100%的代码覆盖率所欺骗

测试覆盖率必须100%吗?听老马怎么说

01
什么是代码覆盖率
在单元测试中,代码覆盖率主要用来衡量代码质量好坏的指标,代码覆盖率表示测试用例通过手动测试和使用自动化测试所覆盖的代码百分比。计算公式如下:
(A)你正在测试的软件的总代码行数
(B)所有测试用例当前执行的代码行数
(B除以A)乘以100,这将是您的代码覆盖率%。
例如,如果系统组件中的代码行总数为1000,并且所有现有测试用例中实际执行的行数为650,那么您的代码覆盖率为:
(650 / 1000) * 100 = 65%
 
02
代码覆盖率与测试覆盖率不同
除了代码覆盖率之外,我们常常还听到另一个概念叫测试覆盖率。很多人认为代码覆盖率和测试覆盖率是一回事,所以经常这两个概念混用,但其实两者之间是有差别的。代码覆盖率和测试覆盖率度量的对象是完全不同的。
代码覆盖率是通过测试执行期间覆盖的代码百分比来度量的,是度量多少代码被执行;而测试覆盖率是通过测试所覆盖的需求来度量的,是度量有多少的特性/功能被执行。代码覆盖是一种白盒方法,而测试覆盖是一种黑盒方法。
例如,如果您要执行web应用程序的跨浏览器测试,以确保应用程序在不同浏览器中的呈现是否良好。您的测试覆盖率将围绕您验证web应用程序的浏览器兼容性的浏览器+操作系统组合的数量为20个,你已经测试了15个组合,那么您的测试覆盖率为:
(15 / 20)*100=75%
03
代码覆盖率的实现原理
在我们检查和统计代码覆盖的时候,一般需要使用一种称为插装的方法,检测可用于监视性能、插入跟踪信息和诊断源代码中的任何类型的错误。目前大多数代码覆盖工具都使用插装方式,其中监视执行的语句被插入到代码中必要的位置。
尽管添加插装代码会导致总体应用程序大小和执行时间的增加,但是与通过执行插装代码生成的信息相比,开销是最小的。输出由一个报告组成,该报告详细描述了测试套件的代码覆盖率。
一般有三种主要的插装类型:
代码插装——在这里,源代码是在添加插装语句之后编译的。编译应该使用常规的工具链完成,成功的编译会生成插装的程序集。例如,为了检查代码中执行特定函数所花费的时间,可以在函数的Start和End中添加插装语句。
运行时插装——与代码插装方法相反,这里的信息是从运行时环境中收集的,即当代码正在执行时插装。
中间代码插装——在这种类型的插装中,插装类是通过向编译后的类文件添加字节码来生成的。
根据您的测试需求,您应该选择正确的代码覆盖工具和该工具支持的最佳方法。
04
不要被100%的代码覆盖率所欺骗
 
很多人在进行单元测试的时候,都会制定一个度量指标:代码覆盖率达到100%,也就是保证每一行可执行的代码都要被测试覆盖到。这个理念是好,但是会存在以下几个问题:
100%的代码覆盖率不代表代码就没有问题
这一点可能会让人有疑义,明明每一行代码我都有测试用例覆盖了,为什么还会有问题?让我们来看下面的例子。
比如我们有一个除法函数,它接受两个浮点参数x和y,并在它们之间执行除法,代码段在下面。请注意,我们对我们的代码没有任何形式的保护。
 
float divide ( float x, float y )
{
return x / y;
}
 
对于这个除法divide函数,我们还提供了一个简单的单元测试,以确保我们的函数能够完成任务。有了这个测试,我们就有了100%的代码覆盖率。这意味着我们的代码是没问题的,对吗?
 
@test
public void divide_with_valid_arguments()
{
assertThat(new Calculator().divide(10,2)).isEqualTo(5);
}
 
当然不对,我们虽然已经达到了100%的代码覆盖率,这是事实。但是代码本身是存在问题的。或许有人发现了,如果除以0的话会怎么办?是的,这个函数就存在问题了,它需要增加处理异常的代码来解决除以0的情况,而代码覆盖率并不会告诉你这些。还有那种1==1的断言或者根本没有断言的代码,代码覆盖率也不能发现问题,所以这也意味着100%的代码覆盖率并不能保证您的代码是没问题的。
 
有些语句并没有需要覆盖它的价值
有些语句是不需要去覆盖它的。比如私有方法。我们需要坚持一个实现类就有一个测试类的法则,一个单元测试类至少应该测试这个类的公共接口。但是单元测试没有必要对私有方法进行测试。因为编写单元测试有一条细则:它们不应该和代码的实现有太紧密的耦合。
测试如果与产品代码耦合太紧,很快就会令人讨厌。当重构代码时(重构意味着改变代码的内部结构而不改变其对外的行为)你的单元测试就会挂掉,这样的话你就损失了单元测试的一大好处:充当代码变更的保护网。你很快就会厌烦这些测试用例,而不会感到它能带来好处,因为你每次重构测试就会挂掉,带来更多的工作量。
那么正确的做法是什么?是不要在你的单元测试里耦合实现代码的内部结构。私有方法应该被视为实现细节。这就是为什么你不应该有去测试他们的冲动。
所以你应该只需要关注测试公共接口。但是更重要的是,不要为了达到100%的代码覆盖率而去测试微不足道的代码,包括敏捷XP的专家Kent Beck也认可这样的观点。你不会因为测试 getter、setter或是其他简单的实现(比如没有任何条件逻辑的实现)而得到任何价值。
 
100%的代码覆盖率会让人迷失目标
当我们设置了100%的代码覆盖作为度量指标后,很有可能会让人因为要这个指标而失去了测试真正的“初心”。比如开发人员如果达不到100%覆盖率的话就无法提交代码到配置库中,在这种情况下,我们都知道人类是很聪明的,开发人员会马上针对没有覆盖的地方设计出重复或者冗余或者没有太大价值的测试用例来满足这个指标,但是却忘记了测试的目的是为了发现重要的错误。
正如敏捷大师Brian Marrick所说:设计您的初始测试套件来达到100%的覆盖率是一个更糟糕的主意,它不擅长发现那些非常重要的遗漏错误。
所以高代码覆盖率与代码质量没有直接关系,不能把追求100%代码覆盖率作为关键的度量标准或目标。
 
05
代码覆盖率的真正意义
 
如果我们不能追求100%代码覆盖率,那么代码覆盖率的存在还有什么意义呢?关于代码覆盖率的价值,Martin Fowler曾在他的博客写过:我不时听到人们问代码覆盖率的价值是什么,或者自豪地陈述他们的覆盖率水平。这种说法没有抓住问题的关键。代码覆盖率是发现代码库中未测试部分的有用工具,而代码覆盖率作为测试好坏的数字陈述几乎没有用处。
诚然,我们可以把一定程度的覆盖率作为一个目标,并且团队会努力实现它,这没错。毕竟覆盖率高总比覆盖率低(比如低于50%)好。但是正如上面所说,高覆盖率的数字太容易通过低质量的测试得到。甚至在最荒谬的情况下,你有很多没有断言的测试(AssertionFreeTesting)。但是即使没有这些,你也会制造大量的测试来寻找那些很少出错的东西,从而分散你对真正重要的东西的测试。
以此同时,Martin Fowler还认为100%的目标设置会让人怀疑,它听起来像是有人在写测试来让覆盖率数字满意,但是却没有考虑到他们在做什么。所以他推荐如果我们的测试是经过深思熟虑的,那么覆盖率在80%或90%以上即可。
所以我们更应该关注测试的充分性,而不是代码覆盖率。测试的充分性是一个比覆盖率更复杂的属性。如果以下是正确的,那么说明我们已经做了充分而且足够的测试:
很少有bug会逃到生产环境中,
而且您很少会因为担心导致产生bug而犹豫是否还要更改代码。
那么是否可以通过更多的测试来提高测试充分性吗?当然可以。但是如果您能够在删除某些测试后发现仍然有足够的测试,那么说明您就测试得太多了。这是一件很难感知的事情。测试太多的一个坏处是你的测试减慢了你的速度。如果只是对代码的简单更改但却会导致对测试的更改过长,那么这就是测试存在问题的迹象。这有可能不是因为您测试了太多的东西,而是因为您的测试中存在重复。
最后我们再来总结一下:代码覆盖率分析的价值是什么呢?它可以帮助你发现你的代码哪些部分没有被测试,从而提高测试的充分性。所以通过经常运行代码覆盖工具并发现和查看这些未经测试的代码是非常值得的。

相关阅读:

代码覆盖率和测试覆盖率到底是不是一回事?

用测试覆盖率度量代码质量真的靠谱?

不要被100%的代码覆盖率所欺骗

测试覆盖率必须100%吗?听老马怎么说

关于作者

原文链接: https://mp.weixin.qq.com/s?__biz=MzIxNzc4ODgxMg==&mid=2247484185&idx=1&sn=12d3007b2b0ab7760e49030c28145074