本节书摘来自异步社区《软件测试技术实战:设计、工具及管理》一书中的第2章,第2.6节软件白盒测试,作者顾翔,更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.6 软件白盒测试
上面介绍的5个测试用例的设计方法大部分都适用于黑盒测试。下面让我们来详细介绍软件白盒测试的一些知识。先来看一下由Main Cohn提出的著名的软件测试金字塔,如图2-14所示。
由于白盒测试是单元测试的主要内容,所以白盒测试在整个软件测试过程中很重要。
白盒测试覆盖包括语句覆盖、分支覆盖、条件覆盖、分支/条件覆盖、MC/DC(修订的条件/判定)覆盖、路径覆盖、控制流覆盖等,这些是白盒测试技术中基本的概念,将在本节中详细介绍。在介绍之前,看一下本节中将要用到的一个程序,如图2-15所示。
这里包括如下。
3个语句:S1、S2、S3。
2个判断:M1、M2。
4条路径:L1:ace、L2:abe、L3:acd、L4:abd。
8个条件:T1:x>3、T2:z<10、T3:x=4、T4:y>5、F1:x<=3、F2:j>=10、F3:x<>4、F4:y>=5。
2.6.1 语句覆盖测试
语句覆盖(Statement Coverage)又叫行覆盖(Line Coverage)。段覆盖(Segment Coverage)。基本块覆盖(Basic Block Coverage),这是最常用、也是最常见的一种覆盖方式,就是度量被测代码中每个可执行语句是否被执行到了。这里说的是“可执行语句”,因此就不会包括像C++的头文件声明、代码注释、空行等。非常好理解,只统计能够执行的代码被执行了多少行。需要注意的是,单独一行的花括号{}也常常被统计进去。语句覆盖常常被人指责为“最弱的覆盖”,它只覆盖代码中的执行语句,不考虑各种分支的组合等。假如只要求达到语句覆盖,那么换来的测试效果的确不明显,很难发现代码中更多的问题。
语句覆盖率的公式可以表示如下。
语句覆盖率=被评价到的语句数量/可执行的语句总数×100%。
下面让我们来看看各种情况下的语句覆盖。
在顺序语句中语句覆盖:在顺序语句中,语句覆盖率最简单,只要把顺序语句中的每个语句都覆盖到。
int f (int a,int b){
int c;
c=a+b;
return c;
}
语句覆盖率100%测试用例:f(1,2)。
在没有else的判断语句中语句覆盖:只要执行if语句中的内容就可以了。
int f (int a){
int b=0;
if (a>0){
b=1;
}
return b;
}
语句覆盖率100%测试用例:f(1)。
在有else的判断语句中语句覆盖:既要执行if语句,也要执行else中的语句。
int f (int a){
int b=0;
if (a>0){
b=1;
} else{
b=2;
}
return b;
}
语句覆盖率100%测试用例:f(1)(执行了b=1语句)、f(0)(执行了b=2语句)两个。
在循环语句中语句覆盖:循环体内的语句必须有且有一次被运行。
int f (int a){
for (i=0;i<=a;i++)
…
printf(“hello”,s);
…
}
语句覆盖率100%测试用例:f(0)。这里需要特别强调的是:测试用例在循环体内语句,必须有且有一次被运行,是因为循环体内的语句可能很长,如果让它执行2次,10次,甚至50次,100次或更多次,这样单元测试的时间会变得很长,而且意义不大。另外,单元测试要求一个测试用例最好在0.5s内能够执行完毕。
在多条件语句中语句覆盖:每一个分支必须执行一次。
int f (int a){
switch (a) {
case:1 { f1(); break;}
case:2 { f2(); break;}
case:3 { f3(); break;}
case:4 { f4(); break;}
}
语句覆盖率100%测试用例:f(1)、f(2)、f(3)、f(4) 共4个。
看看在开始的例子中,设计哪些数据可以达到语句覆盖100%?
案例2-15:语句测试覆盖率。
假设令x=4、y=8、z=5。
- 经过M1判断时,(4>3&&5<10)->(True&&True)->True,所以走b路径,执行语句S1。
- 经过M2判断时,(4=4||8>5)->(True||True)->True,所以走d路径,执行语句S2。
- 最后执行S3语句。
语句覆盖测试用例见表2-18。
由此可见,只需要设计一个测试用例,就可以达到语句覆盖率100%。
语句覆盖毕竟是最简单的覆盖,即使达到语句覆盖100%,软件也会出现问题。
这里举一个不能再简单的例子,看下面的被测试代码。
int foo(int a, int b)
{
return a / b;
}
如果软件测试工程师编写如下软件测试用例。
测试用例:a = 10、b = 5。
软件测试工程师的测试结果会告诉你,代码覆盖率达到了100%,并且所有软件测试用例都通过了。然而,遗憾的是,语句覆盖率达到所谓的100%,但是却没有发现最简单的Bug。比如,当b=0时,会抛出一个除以零的异常。
简而言之,语句覆盖就是设计若干个测试用例,运行被测程序,使得每一个可执行语句至少执行一次。这里的“若干个”意味着使用测试用例越少越好。
2.6.2 分支覆盖测试
分支覆盖又称判定覆盖,就是设计若干个测试用例,运行被测程序,使得程序中每个判定的取真分支和取假分支至少一次。
分支覆盖率的公式可以表示如下:
分支覆盖=被执行的分支数量/所有分支数量×100%
下面来看各种情况下的分支覆盖。
分支覆盖没有else的判断语句:既要执行if语句为True的情况,也要执行if语句为False的情况。
int f (int a){
int b=0;
if (a>0){
b=1;
}
return b;
}
分支覆盖率100%测试用例:f(1)、f(0),这是区别分支覆盖与语句覆盖需要特别注意的地方。
有else的判断语句:既要执行if语句,也要执行else中的语句。
int f (int a){
int b=0;
if (a>0){
b=1;
} else{
b=2;
}
return b;
}
分支覆盖率100%测试用例:f(1)、f(0)。
在循环语句中:循环体内的语句必须有且有一次被运行。
int f (int a){
for (i=0;i<a;i++)
printf(“hello”,s);
}
分支覆盖率100%测试用例:f(1)。
在多条件语句中:每个分支语句必须执行一次,另外还要涉及一种所有case没有覆盖到的情形。
int f (int a){
switch (a) {
case:1 { f1(); break;}
case:2 { f2(); break;}
case:3 { f3(); break;}
case:4 { f4(); break;}
}
分支覆盖率100%测试用例:f(1)、f(2)、f(3)、f(4)、f(5) 5个。
下面来看在开始例子中,设计哪些数据可以达到分支覆盖的100%?
案例2-16:分支测试覆盖率。
令x=4、y=8、z=5。
- 经过M1判断时,(4>3&&5<10)-> (True&&True) ->True,所以走b路径,执行语句S1。
- 经过M2判断时,(4=4||8>5)-> (True||True) ->True,所以走d路径,执行语句S2。
- 最后执行S3语句。这时测试到M1、M2分别为True的情形。
然后令x=2、y=5、z=11。
- 经过M1判断时,(2>3&&11<10)-> (False&&False) ->False,所以走c路径。
- 经过M2判断时,(2=4||5>5)-> (False||False) ->False,所以走e路径。
- 最后执行语句S3。这时测试到M1、M2分别为False的情形。
通过这两组数据,就可以达到该程序分支覆盖率测试100%,见表2-19。
也可以设计另一组测试用例来达到分支覆盖的100%,这里不详细描述,见表2-20。
分支覆盖的优缺点如下。
- 优点:分支覆盖具有比语句覆盖更强的软件测试能力,而且具有和语句覆盖一样的简单性,无需细分每个判定,就可以得到测试用例。
- 缺点:一般情况下,大部分判定语句由多个逻辑条件组合而成(如判定语句中包含AND、OR、CASE),若仅判断其整个最终结果,而忽略每个条件的取值情况,必然会遗漏部分软件测试路径。
如本例中:x=4 || y>5,y>5写成y<5,即使判定覆盖测试用例达到100%,但是软件还是测试不出。
2.6.3 条件覆盖测试
在软件设计过程中,一个判定往往由多个条件组成,判定覆盖仅考虑了判定的结果,而没有考虑每个条件的可能结果。
条件覆盖是指选择足够的测试用例,使得运行这些测试用例时,判定中的每个条件的所有可能结果至少出现一次。
条件覆盖率的公式可以表示如下:
条件覆盖率=被执行的条件数量/所有条件数量×100%。
下面来看条件覆盖的例子:
int f (int a, int b){
int c=0;
if ((a>0) &&(b>0)){
c=1;
}else{
c=2
}return c;
}
表2-21为条件覆盖测试用例。
有些时候条件覆盖是达不到100%的,请看下面的程序:
int f (int a){
int c=0;
if ((a>0) &&(a<5)){
c=1;
}else{
c=2
}return c;
}
由表2-22可以看到,既要达到a≤10又要达到a≥5是不可能的。
下面再来看在开始例子中,设计哪些数据可以达到条件覆盖的100%?
案例2-17:条件测试覆盖率。
令x=4、y=2、z=11。
- 经过M1判断时,(4>3&&11<10)-> (True&&False) ->False,所以走c路径。
- 经过M2判断时,(4=4||2>5)-> (True||False) ->True,所以走d路径,执行语句S2。
- 最后执行S3语句。这时测试到条件判断分别为T1、F2、T3、F4。
令x=2、y=6、z=6。
- 经过M1判断时,(2>3&&6<10)-> (False&&True) ->False,所以走c路径。
- 经过M2判断时,(2=4||6>5)-> (False||True) ->True,所以走d路径,执行语句S2。
- 最后执行S3语句。这时测试到的条件判断分别为F1、T2、F3、T4。
经过以上测试用例,T1、T2、T3、T4、F1、F2、F3、F4都被执行了一次,条件覆盖率达到100%,如表2-23所示。
可以看出,这里虽然条件覆盖达到100%,但是语句覆盖都没有达到100%,S1语句根本没有执行到。
于是,在日常工作中为了弥补分支覆盖的不足,结合条件覆盖的不充分,提出了判定/条件覆盖。
2.6.4 判定/条件覆盖测试
判定/条件覆盖:设计足够的测试用例,使得判断中每个条件的所有可能取值至少执行一次,同时每个判断的所有可能判断结果,即要求各个判断的所有可能的条件取值组合至少执行一次。
我们仍旧以开始的程序为例。
案例2-18:判定/条件测试覆盖率。
令x=4、y=8、z=5。
- 经过M1判断时,(4>3&&5<10)-> (True&&True) ->True,所以走b路径,执行语句S1。
- 经过M2判断时,(4=4||8>5)-> (True||True) ->True,所以走d路径,执行语句S2。
- 最后执行S3语句。这时测试到M1、M2分别为True的情形。条件判断分别为T1、T2、T3、T4。
令x=2、y=5、z=11。
- 经过M1判断时,(2>3&&11<10)-> (False&&False) ->False,所以走c路径。
- 经过M2判断时,(2=4||5>5)-> (False||False) ->False,所以走e路径。
- 最后执行语句S3。这时测试到M1、M2分别为False的情形。条件判断分别为F1、F2、F3、F4。
可以看到:
- 原子条件True,False都达到了:T1、T2、T3、T4、F1、F2、F3、F4。
- 两个判断True,False也达到了:M1=True、False;M2=True、False。
所以,这样既达到了分支覆盖率是100%的情形,也达到了条件覆盖率是100%的情形,见表2-24。
2.6.5 MC/DC(修订的条件/分支软件测试)覆盖测试
MC/DC(修订的条件/分支软件测试)准则是一种实用的软件结构软件测试率软件测试准则,已被广泛应用于软件验证和软件测试过程中。
案例2-19:MC/DC覆盖测试。
condition和decision的概念:
if (A || B && C ) {
语句1;
}
Else{
语句2;
}
A,B,C都是一个条件,而(A || B && C)叫一个Decision,如果是条件软件测试,只需两个CASE,就能软件测试,就是让这个decision为True和False各一次,就能达到。即:
- A=True、B=False、C=True。
- A=False、B=True、C=False。
如果是MC/DC,就得4个测试用例,怎么计算呢?
MC/DC覆盖测试在每个判定中的每个条件都曾独立影响判定的结果至少一次(独立影响意思是在其他条件不变的情况下,改变一个条件):
A || B && C
总结:每个条件对结果都独立起作用。
(1)如果A对结果起作用的话,B必须为False、C必须为True,这样结果就独立受A的值影响。(A||0&&1)->(A||0),(A、B、C取值分别为A=True、B=False、C=True和A=False、B=False、C=True)。
(2)同理,如果B对结果独立起作用,A必须为False、C必须为True,两种情况B为True、False各一个 (0||B&&1) (A、B、C取值分别为A=False、B=True、C=True和A=False、B=False、C=True)。
(3)如果C独立对结果起作用,就是让(A || B) 为True,为了减少用例,上面的用例已经含有这样的用例了,就取A为False、B为True,这样C独立起作用的用例为 (0||1&&C)->(1&&C)。(A、B、C取值分别为A=False、B=True、C=True和A=False、B=True、C=False)。
可以看出,每个条件各走了一次True和False,这样3个变量条件就会有6个用例, 但是其中里面有两个是重复的。
在1、2情形中均出现A=False、B=False、C=True。
在2、3情形中均出现A=False、B=True、C=True。
因此,最后的测试用例为。
- A=True、B=False、C=True。
- A=False、B=False、C=True。
- A=False、B=True、C=True。
- A= False、B=True、C=False。
需要进一步补充说明的是,MC/DC测试的主要目的是为了防止在组合条件表达式中包含副作用(side effect),见以下语句:
if (a() || b() || c()){ ... }
当b函数或c函数产生副作用时,MC/DC软件测试存在非常大的必要性。
原则上不应在组合条件表达式中调用产生副作用的函数。
2.6.6 路径覆盖测试
路径覆盖的含义是:选取足够多的软件测试数据,使程序的每条可能路径都至少执行一次(如果程序图中有环,则要求每个环至少经过一次)。
路径覆盖率的公式可以表示如下:
路径覆盖率=被执行的路径数量/所有路径数量×100%
案例2-20:路径覆盖(图2-16 )测试。
这里存在4条路径,分别为(1,3)、(1,4)、(2,3)、(2,4)。为了达到这些路径,设计测试用例见表2-25。
回看本节开始的例子,设计什么数据可以使路径覆盖达到100%呢?第2.6.2节提到的两组测试用例,第一组测试用例分别执行了路径L4(abd)和L3(acd),第二组测试用例分别执行了路径L2(abe)和L1(ace)。所以,使用这4个测试用例,就可以达到路径覆盖测试100%。
路径覆盖测试用例见表2-26。
以上6种覆盖率强弱关系如图2-17所示。
2.6.7 控制流测试
控制流测试经常用在嵌入式软件系统。
案例2-21:控制流测试。
如图2-18所示。
首先:
- 对经过A点的线进行排序:{1,2}、{1,3}、{1,4}、{6,2}、{6,3}、{6,4}。
- 对经过B点的线进行排序:{2,6}、{3,6}、{4,6}、{2,5}、{3,5}、{4,5}。
然后进行总体排序
{1,2}、{1,3}、{1,4}、{2,5}、{2,6}、{3,5}、{3,6}、{4,5}、{4,6}、{6,2}、{6,3}、{6,4}。
最后依次进行如下操作:
从1开始,5结束的连续序列,一直到把所有序列都输出完毕,见表2-27。
最后得到5个测试用例。
(1){1,2,5}。
(2){1,3,5}。
(3){1,4,5}。
(4){1,2,6,2,5}。
(5){1,3,6,4,6,3,5}。
2.6.8 单元测试中的基于代码的功能测试
在工作中,使用某些工具,可以通过图形化界面来了解各种测试的覆盖率。并且在单元测试中我们除了关心覆盖率的测试,还可以关心函数自身的功能测试。
案例2-22:单元测试中的基于代码的功能测试。
设计了函数float myAve(int32 a,int32 b),需求是获得a和b的平均数,如果输入参数有异常,则返回0.01。
可以设计这样一系列测试用例。
testMyAve (4, myAve(5,3)); //其中myAve(5,3)为要测试的函数,称为被测函数(SUT),4为期望结果,如果实际结果与期望结果相同,测试通过,否则测试不通过。
testMyAve (4.5, myAve(6,3))。
testMyAve (0, myAve(-1,+1))。
testMyAve (-4, myAve(-5,-3))。
testMyAve (-4.5, myAve(-6,-3))。
testMyAve (1.5, myAve(6,-3))。
testMyAve (-1.5, myAve(-6,3))。
testMyAve (32767, myAve(32767, 32767))。
testMyAve (-32767, myAve(-32767,-32767))。
testMyAve (16383.5, myAve(32767, 0))。
testMyAve (32767, myAve(0, 32767))。
testMyAve (-32767, myAve(-32767,0))。
testMyAve (0, myAve(-32767, 32767))。
testMyAve (0.01, myAve(-32768, 32767))。
testMyAve (0.01, myAve(-32767, 32768))。
testMyAve (0.01, myAve(32767,-32768))。
testMyAve (0.01, myAve(32768,-32767))。
testMyAve (0.01, myAve(-32768,-32768))。
testMyAve (0.01, myAve(32768, 32768))。
这样再去查看,测试工具告诉我们各种测试覆盖率是否达到100%。本书第二篇第8.1节“单元测试工具Junit4测试工具”中会介绍Junit 4测试工具。
2.6.9 总结
白盒测试除了上述介绍的动态白盒测试外,还包括静态白盒测试,即代码审核。在静态审核中,代码书写规则非常重要,业界比较流行的编码规则请参看本篇附录C。
最后要指出白盒测试不仅在单元测试时进行,也可以在系统测试时进行,最早在嵌入式软件测试中,通过插桩的技术,通过工具得知各种覆盖率达到百分之几。现在上海有家公司对于非嵌入式产品,如APP程序,在运行系统测试时可以通过监控器,看到当时程序正在执行哪条语句,并且告诉各种覆盖率达到百分之几。这种技术叫作精准软件测试,是现在比较先进的软件测试方法。本书第二篇第11.4节“精准测试工具-星云测试平台”将会详细介绍。