1. 什么是代码覆盖率
代码覆盖率测试技术是一种常见的白盒测试技术,是衡量软件测试工作充分性和完整性的重要指标之一。
简单来说,代码覆盖率就是测试过程中已经被执行过的代码占准备测试总代码量的比例和程度,它关注的是在执行用例时,有哪些代码被执行到了,有哪些代码没有被执行到。
根据运行测试手段不同,代码覆盖率分成了单元测试代码覆盖率、接口测试代码覆盖率和功能测试代码覆盖率。代码覆盖率最终的目的是找到那些没有被覆盖到的代码。
但是没有覆盖到的代码就一定有问题吗?不一定有问题,但要知道,一个完整的测试运行之后,有哪些代码没有跑到。这些代码有可能是有问题的,也有可能是没有问题的,有可能是冗余的,也有可能架构设计有问题,都有可能。我们不能说它是不好的代码,但至少是有问题的代码,这是我们做代码覆盖率的目的,而非去度量某一个质量指标。
2. 代码覆盖率常用统计指标
2.1 语句覆盖 (StatementCoverage)
语句覆盖 (StatementCoverage) 又称行覆盖 (LineCoverage),指已经被执行到的语句占总可执行语句(不包含类似C++的头文件声明、代码注释、空行等等)的百分比。这里说的是“可执行语句”,不包括头文件声明,代码注释,空行等。只统计能够执行的代码被执行了多少行。简单来说,就是这行代码只要被覆盖到了就是1,没有被覆盖到就是2。
if (a < 10 || a / b == 2) // 判定
{
return 0;// 语句1
}
else
{
return 1;// 语句2
}
上面这段代码,这里有价值的语句一共有3条语句:判断语句是一条,return 0是一条,return 1是一条。很多代码覆盖率会认定else不算,因为它就是一个关键字,实际上没有什么语句的价值。
如果每一行都执行到了,行覆盖率就是1,总共3行,3行都执行到了,行覆盖率就是100%。稍微有点代码常识的人看到这里就能看出问题了,如果这个if语句我确实执行到了,但是它明显地有判断条件。
如果第一个判断条件执行了,就能判断出这个语句的真假,后面的判断条件不执行了,继续往下走。这个语句确实叫执行了,true/ false 都没有完全覆盖到而已,所以我们说行覆盖是最弱的代码覆盖。
语句覆盖常常被人指责为“最弱的覆盖”,它只管覆盖代码中的执行语句,却不考虑各种分支的组合等等。
2.2 判定覆盖 (DecisionCoverage)
判定覆盖(DecisionCoverage)又称分支覆盖(BranchCoverage),度量程序中每一个判定的分支是否都被测试到了,即代码中每个判定的"真"和"假"至少执行一次。
比如我们刚才的例子里有一个if语句,行覆盖就是一行行往下走,判定覆盖是指有分支的语句,每一个语句是否都被走到了。再看下之前这个例子
if (a < 10 || a / b == 2) // 判定
{
return 0;// 语句1
}
else
{
return 1;// 语句2
}
这个例子完整地有if有 else,所以行一旦覆盖成功了,三句都走完了,那么分支覆盖肯定也是百分之百走完,也是成功的,因为if走了,else也走了。
但如果把下面的else盖住的话,没有这个else,只有if,如果语句覆盖里面的if走到了,return走到了,行覆盖率是百分之百,但是else的条件并没有测到,这个时候只覆盖到了if为true的情况,没有覆盖到else分支,虽然else分支里面没有代码。
这个例子就说明判定覆盖和行覆盖还是有本质区别的。
2.3 条件覆盖 (ConditionCoverage)
度量判定中的每个子表达式结果true和false是否被测试到了。
为了说明判定覆盖和条件覆盖的区别,我们来举一个例子,假如被测代码如下:
int foo (int a, int b)
{
if (a < 10 || b < 10) // 判定
{
return 0; //分支一
}
else
{
return 1; //分支二
}
}
在刚才的例子里,如果if、else都走到了,那么分支覆盖肯定也是百分之百,但是条件覆盖不一定是百分之百。if整体为true,确实走到了,if整体为false,也就是else,确实也走到了,对于if判断语句来说,两个分支确实都走到了。
所以在设计判定覆盖的测试用例时,我们只需要考虑到判定结果为true和false两种情况,因此我们只需要设计如下的case,就能达到判定覆盖率100%:
- Test case1: a = 5, b = 任意数字
当a=5的时候,a<10这个条件成立,对于逻辑或来说,前面的条件为真,后面的条件就不用判断了,必然是为真的,覆盖了分支一。
- Test case2: a = 15, b = 15
当a=15时,a<10不成立,进而去判断第二个条件,当b=15,b<10也不成立的时候,会走到else这里,覆盖了分支二。
但这里面还有一个问题,if后面是有两个判定条件的,稍微有点代码常识的朋友都知道,不管什么语言,都有逻辑判断短路的问题。两个and判断,如果有一个and判断为false了,后面就不会判断了,所以那个就不走了,不管走不走,并没有覆盖到两个并列条件,两个子条件都为true或都为false的情况并没有走到。
整体的判断语句走到了,但是里面两个具体的分支判断条件分别的true、false没有走到。这个时候分支覆盖率是百分之百了,但是条件覆盖率并不是百分之百。
所以设计条件覆盖案例时,我们需要考虑到判定中每个表达式的结果,为了达到覆盖率100%,设计了如下案例:
-
Test case1: a = 5,b = 任意数字 => ture, X
-
Test case2: a = 15, b = 5 => false, true
-
Test case2: a = 15, b = 15 => false, false
第一条,先让a=5,b=任意数字,这个时候a<10这个条件为true的情况已经被覆盖。
接下来通过a = 15, b = 5和a = 15, b = 15 这两组数据,让a<10这个条件为false时,b<10这个条件的true和false,都覆盖到了
通过上面的例子,应该很清楚的了解了判定覆盖和条件覆盖的区别。
需要注意的是:条件覆盖不是将判定中的个条件表达式的结果进行排列组合,而是只要每个条件表达式的结果true和false测试到就可以了。
同时,我们可以这样推论:完全的条件覆盖并不能保证完全的判定覆盖。
2.4 路径覆盖 (PathCoverage)
度量是否函数的每一个分支都被执行了。有多个分支嵌套时,需要对多个分支进行排列组合。
因为前面的例子一个if条件,所以不好看路径。举个新例子:
int foo (int a, int b) {
{
int nReturn = 0;
if (a < 10)
{//分支一
nReturn += 1;
}
if (b < 10)
{//分支二
nReturn += 10;
}
return nReturn;
}
两个判定条件,两个if,只覆盖了第一个if判定条件的true和false以及第二个判定条件的true和false,但是两个并没有分别来走。
比如第一个是true,第二个是false,或者第一个是false,第二个是true的这种交叉情况。也就是没有把包含所有的if、else条件的整体当成一个完整的路径去处理,这个路径就有多种组合了。这就是路径覆盖。
对于这样两个并列的if条件来说,怎么来写语句测试用例以做到语句覆盖率100%、判定覆盖率100%、条件覆盖率100%、路径覆盖率100%。
- Test case1: a = 5, b = 5 nReturn = 11
语句覆盖率100%
-
Test case1: a = 5, b = 5 nReturn = 11
-
Test case2: a = 15, b = 15 nReturn = 0
判定覆盖率100%,条件覆盖率100%
-
Test case1: a = 5, b = 5 nReturn = 0
-
Test case2: a = 15, b = 5 nReturn = 1
-
Test case3: a = 5, b = 15 nReturn = 10
-
Test case4: a = 15, b = 15 nReturn = 11
路径覆盖率100%
从这里可以看出,如果想达到路径覆盖率100%的话,测试用例一定是比前三种要多的。所以我们最终得出的结论是:路径覆盖率 > 判定(分支)覆盖 > 语句覆盖。
3. 代码覆盖率的价值
代码覆盖率的分析在一定程度上能够评判代码质量,一般覆盖率高的代码出错的几率会相对低一些。但是高覆盖率的代码只能表示执行了很多的代码,并意味着这些代码被很好的执行了。
所以代码覆盖率很高的分析价值:
-
于测试来说,代码覆盖率最主要的意义是帮助我们了解测试情况,可以通过分析未覆盖部分的代码,从而反推在前期测试设计是否充分,没有覆盖到的代码是否是测试设计的盲点,之前为什么没有考虑到?或许是需求/设计不够清晰,测试设计的理解有误,工程方法应用后的造成的策略性放弃等等,之后进行补充测试用例设计。
-
其有助于发现多个测试用例都覆盖不到的代码,可以逆向反推在代码设计中思维混乱点,提醒设计/开发人员理清代码逻辑,提升代码质量;同时为废弃代码提供依据。
-
代码覆盖率可以度量单元/自动化测试用例,提供覆盖率统计情况,可以通过分析覆盖率报告,完善用例。
-
代码覆盖率利于精准回归,通过构建代码调用关系,精准的确定回归测试范围,避免了全量回归造成的测试资源浪费。
4. 常见工具
根据使用语言的不同,一些流行的工具:
- Java:Atlassian Clover、Cobertura、JacoCo
- Javascript:istanbul
- PHP:PHPUnit
- Python:Coverage.py
- Ruby:SimpleCOV