代码覆盖率

1 介绍

代码覆盖率,是一种通过计算测试过程中被执行的源代码占全部源代码的比例,进而间接度量软件质量的方法。

全量覆盖率:基于全部代码的代码覆盖率

增量覆盖率:基于代码增量的代码覆盖率

按性质,它属于白盒测试的范畴,即主要依据源代码的内部结构来设计测试用例,通过设计不同的输入来测试软件的不同部分。

常见的编程语言,如C/C++,python和Java等,都有相应的代码覆盖率测试工具。

2 意义

  1. 提升开发者测试意识,可以基于此在程序中寻找没有被测试用例测试过的地方,创建新的测试用例来增加覆盖率,从而提高软件质量;
  2. 也为代码管理提供了新的手段,降低管理成本。

注意:代码覆盖率不是灵丹妙药,它只是告诉我们有哪些代码没有被测试用例“执行到”而已,高百分比的代码覆盖率不等于高质量的有效测试。

举例:假设代码覆盖率只在某一些模块代码覆盖率很高,但在一些关键模块并没有足够的测试用例覆盖,那样虽然代码覆盖率很高,但并不能说明产品质量就很高。

Martin Fowler 测试覆盖率 写到:

代码覆盖率是查找代码库中未测试部分的有用工具,然而它作为一个数字说明你的测试有多好用处不大。

3 分类

3.1 语句覆盖(statement coverage)

程序中的语句有多少被执行。它是最常用也是最简单的一种代码覆盖率度量方式,就是度量被测代码中每个可执行语句是否被执行到了。“可执行语句”,并不包括C++的头文件声明、代码注释和空行等。但是,单独一行的花括号{} 常常也被统计进去。

3.2 行覆盖率

有多少行的源代码被测试过。感觉和语句覆盖一样。

3.3 判定覆盖率(decision coverage)or 分支覆盖

又称分支覆盖,所有边界覆盖,基本路径覆盖,判定路径覆盖,它度量程序中每一个判定的分支是否都被测试到了。所谓判定,是指一条判断语句的结果,而不考虑其中包含的子判断的结果和组合情况。

3.4 条件覆盖(condition coverage)

它报告每一个子表达式的结果的true 或false 是否测试到了。即构造测试用例时,要使得每个判定语句中每个逻辑条件的可能值至少满足一次(即每一个被“逻辑与”或“逻辑非”分开的布尔表达式真假值情况)。但是,需要注意的是,条件覆盖不是将判定中的每个条件表达式的结果进行排列组合,而是只要每个条件表达式的结果true和false测试到了就可以了。

3.5 修正条件判定覆盖(modified condition / decision coverage)

前面提到的条件覆盖比语句覆盖和判定覆盖都要严格,但是由于它只关注每个条件表达式的结果是否都被测试到,而不要求对每个条件表达式的解果进行排列组合,所以它也只能覆盖一部分的情况。如果真要做到全覆盖,随着子表达式的增加,不仅测试用例设计的难度会越来越大,而且用例的数量也会指数级增长。

修正条件判定覆盖,要求在一个程序中每一种输入输出至少得出现一次,在程序中的每一个条件必须产生所有可能的输出结果至少一次,并且每一个判定中的每一个条件必须能够独立影响一个判定的输出,即在其他条件不变的前提下仅改变这个条件的值,而使判定结果改变。显然,修正条件判定覆盖的定义有点抽象,接下来还是以前面的代码为例介绍。

修正条件判定覆盖要求在每个判定中,每个条件都独立地影响判定结果至少一次(所谓独立影响就是在其他条件不变的情况下,改变该条件可以改变判定结果)。例如,要想a对判定独立影响,则b或function的结果必须为true;要想b对判定独立影响,则a必须为true;同理,要想function对判定独立影响,a也必须为true。如果列出测试用例表,则不难发现满足以上条件的测试用例的数量并不需要很多!主要因为有些用例是重复的。

3.6 条件判定组合覆盖(condition decision coverage)

3.7 路径覆盖(path coverage)

3.8 多条件覆盖(multi-condition coverage)

3.9 方法覆盖率

程序中的方法/函数有多少被执行。

3.10 类覆盖率

程序中的类有多少被执行。

4 工作原理

代码覆盖率测量主要有以下三种方式:

4.1 Source code instrumentation - 源代码检测

将检测语句添加到源代码中,并使用正常的编译工具链编译代码以生成检测的程序集。这是我们常说的插桩,Gcov 是属于这一类的代码覆盖率工具。

4.2 Runtime instrumentation - 运行时收集

这种方法在代码执行时从运行时环境收集信息以确定覆盖率信息。以我的理解 JaCoCo 和 Coverage 这两个工具的原理属于这一类别。

4.3 Intermediate code instrumentation - 中间代码检测

通过添加新的字节码来检测编译后的类文件,并生成一个新的检测类。说实话,我 Google 了很多文章并找到确定的说明哪个工具是属于这一类的。机器码(machine code)和字节码(byte code)是什么?


了解这些工具的基本原理,结合现有的测试用例,有助于正确的选择代码覆盖率工具。比如:

  • 产品的源代码只有 E2E(端到端)测试用例,通常只能选择第一类工具,即通过插桩编译出的可执行文件,然后进行测试和结果收集。
  • 产品的源代码有单元测试用例,通常选择第二类工具,即运行时收集。这类工具的执行效率高,易于做持续集成。

5 当前主流代码覆盖率工具

代码覆盖率的工具有很多,以下是我用过的不同编程语言的代码覆盖率工具。在选择工具时,力求去选择那些开源、流行(活跃)、好用的工具。

编程语言 代码覆盖率工具
C/C++ Gcov
Java JaCoCo
JavaScript Istanbul
Python Coverage.py
Golang cover

5.1 Gcov

Gcov 工作流程图:

图片名称

主要分三步:

  1. 在 GCC 编译的时加入特殊的编译选项-ftest-coverage,生成可执行文件和 .gcno
  2. 运行(测试)生成的可执行文件,生成了 .gcda 数据文件;(.gcda的生成是因为程序在编译的时候引入了 -fprofile-arcs选项)
  3. 有了 .gcno.gcda,通过源码生成 .gcov 文件,最后生成代码覆盖率报告。

用GCC编译的时候加上-fprofile-arcs -ftest-coverage选项,链接的时候也加上。

fprofile-arcs参数使gcc创建一个程序的流图,之后找到适合图的生成树。只有不在生成树中的弧被操纵(instrumented):gcc添加了代码来清点这些弧执行的次数。当这段弧是一个块的唯一出口或入口时,操纵工具代码(instrumentation code)将会添加到块中,否则创建一个基础块来包含操纵工具代码。

gcov主要使用.gcno.gcda两个文件:

  1. .gcno是由-ftest-coverage产生的,它包含了重建基本块图和相应的块的源码的行号的信息。
  2. .gcda是由加了-fprofile-arcs编译参数的编译后的文件运行所产生的,它包含了弧跳变的次数和其他的概要信息(而gcda只能在程序运行完毕后才能产生的)

参考

实践部分详见 博客:C/C++项目的全量覆盖率和增量覆盖率

5.2 Jacoco

X 参考