白盒测试
一、逻辑覆盖
逻辑覆盖法是最常用的白盒测试方法,它包括以下5种方法:
● 语句覆盖
● 判定覆盖
● 条件覆盖
● 判定-条件覆盖
● 条件组合覆盖
1.语句覆盖
语句覆盖(Statement Coverage)又称行覆盖、段覆盖、基本块覆盖,它是最常见的覆盖方式。
语句覆盖的目的是测试程序中的代码是否被执行,它只测试代码中的执行语句,这里的执行语句不包括头文件、注释、空行等。
语句覆盖在多分支的程序中,只能覆盖某一条路径,使得该路径中的每一个语句至少被执行一次,但不会考虑各种分支组合情况。
在代码中,AND表示逻辑运算&&,OR表示逻辑运算||,第1~2行代码表示如果x>0成立并且y<0成立,则执行z=z-(x-y)语句;第3~4行代码表示如果x>2成立或者z>0成立,则执行z=z+(x+y)语句。
在代码运行流程图中,a、b、c、d、e表示程序执行分支。在语句覆盖测试用例中,使程序中每个可执行语句至少被执行一次。
设计测试用例:Test1:x=1 y=-1 z=2执行测试用例,程序运行路径为acd。可以看出程序中acd路径上的每个语句都能被执行。 但是语句覆盖对多分支的逻辑无法全面反映,仅仅执行一次不能进行全面覆盖,因此,语句覆盖是弱覆盖方法。
语句覆盖虽然可以测试执行语句是否被执行到,但却无法测试程序中存在的逻辑错误。
例如,如果上述程序中的逻辑判断符号“AND”误写了“OR”,使用测试用例Test1同样可以覆盖acd路径上的全部执行语句,但却无法发现错误。同样,如果将x>0误写成x>=0,使用同样的测试用例Test1也可以执行acd路径上的全部执行语句,但却无法发现x>=0的错误。
语句覆盖无需详细考虑每个判断表达式,可以直观地从源程序中有效测试执行语句是否全部被覆盖,由于程序在设计时,语句之间存在许多内部逻辑关系,而语句覆盖不能发现其中存在的缺陷,因此语句覆盖并不能满足白盒测试的测试所有逻辑语句的基本需求。
2.判断覆盖
判定覆盖(Decision Coverage)又称为分支覆盖,其原则是设计足够多的测试用例,在测试过程中保证每个判定至少有一次为真值,有一次为假值。
判定覆盖的作用是使真假分支均被执行,虽然判定覆盖比语句覆盖测试能力强,但仍然具有和语句覆盖一样的单一性。
测试用例
|
x
|
y
|
z
|
执行语句路径
|
test1
|
2
|
-1
|
1
|
acd
|
test2
|
-3
|
1
|
-1
|
abd
|
test3
|
3
|
-1
|
5
|
ace
|
test4
|
3
|
1
|
-1
|
abe
|
上述4个测试用例覆盖了acd、abd、ace、abe四条路径,使得每个判定语句的取值都满足了各有一次“真”与“假”。相比于语句覆盖,判定覆盖的覆盖范围更广泛。判定覆盖虽然保证了每个判定至少有一次为真值,有一次为假值。
判定覆盖并没有考虑到程序内部取值的情况。例如,测试用例test4,没有将x>2作为条件进行判断,仅仅判断了z>0的条件。 此外,若将判定条件z>0误写成z<0,test2测试用例路径将执行abe测试路径语句而不会执行abd路径的语句。
判定覆盖语句一般是由多个逻辑条件组成,如果仅仅判断测试程序执行的最终结果而忽略每个条件的取值,必然会遗漏部分测试路径,因此,判定覆盖也属于弱覆盖。
3.条件覆盖
条件覆盖(Condition Coverage)指的是设计足够多的测试用例,使判定语句中的每个逻辑条件取真值与取假值至少出现一次。
例如,对于判定语句if(a>1 OR c<0)中存在a>1、c<0两个逻辑条件,设计条件覆盖测试用例时,要保证a>1、c<0的“真” “假”值至少出现一次。
以图3.1.1节程序为例,设计条件覆盖测试用例,在该程序中,有2个判定语句,每个判定语句有2个逻辑条件,共有4个逻辑条件,使用标识符标识各个逻辑条件取真值与取假值的情况,如下表。
条件1
|
条件标记
|
条件2
|
条件标记
|
x>0
|
S1
|
x>2
|
S3
|
x<0
|
-S1
|
x<2
|
-S3
|
y<0
|
S2
|
z>0
|
S4
|
y>0
|
-S2
|
z<0
|
-S4
|
使用S1标记x>0取真值(即x>0成立)的情况,-S1标记x>0取假值(即x>0不成立)的情况。同理,使用S2、S3、S4标记y<0、x>2、z>0取真值,使用-S2、-S3、-S4标记y<0、x>2、z>0取假值,最后得到执行条件判断语句的8种状态
设计测试用例时,要保证每种状态都至少出现一次。设计测试用例的原则是尽量以最少的测试用例达到最大的覆盖率。
测试用例
|
x
|
y
|
z
|
条件标记
|
执行路径
|
Test1
|
3
|
1
|
5
|
S1、-S2、S3、S4
|
abe
|
Test2
|
-3
|
1
|
-1
|
-S1、-S2、-S3、-S4
|
abd
|
Test3
|
3
|
-1
|
1
|
S1、S2、S3、-S4
|
ace
|
从条件覆盖的测试用例可知,使用3个测试用例就达到了使每个逻辑条件取真值与取假值都至少出现了一次,但从测试用例的执行路径来看,条件分支覆盖的状态下仍旧不能满足判定覆盖,即没有覆盖acd路径。相比于语句覆盖与判定覆盖,条件覆盖达到了逻辑条件的最大覆盖率,但却不能保证判定覆盖,仍旧不能满足白盒测试覆盖所有分支的需求。
4. 判定-条件覆盖
判定-条件覆盖(Condition/Decision Coverage)要求设计足够多的测试用例,使得判定语句中所有条件的可能取值至少出现
一次,同时,所有判定语句的可能结果也至少出现一次。
例如,对于判定语句if(a>1 AND c<1),该判定语句有a>1、c<1两个条件,则在设计测试用例时,要保证a>1、c<1两个条件取“真”、“假”值至少一次,同时,判定语句if(a>1 AND c<1)取“真”、“假”也至少出现一次。
根据判定-条件覆盖原则,以3.1.1节程序为例设计判定-条件覆盖测试用例。
测试用例
|
x
|
y
|
z
|
条件标记
|
条件1
|
条件2
|
执行路径
|
test1
|
3
|
1
|
5
|
S1、-S2、S3、S4
|
0
|
1
|
abe
|
test2
|
-3
|
1
|
-1
|
-S1、-S2、-S3、-S4
|
0
|
0
|
abd
|
test3
|
3
|
-1
|
1
|
S1、S2、S3、-S4
|
1
|
1
|
ace
|
在判定-条件覆盖中,3个测试用例满足了所有条件可能取值至少出现一次,以及所有判定语句可能结果也至少出现一次的要求。相比于条件覆盖、判定覆盖,判定-条件覆盖弥补了两者的不足之处,但是由于判定-条件覆盖没有考虑判定语句与条件判断的组合情况,其覆盖范围并没有比条件覆盖扩展,判定-条件覆盖也没有覆盖acd路径,因此判定-条件覆盖在仍旧存在遗漏测试的情况。
5.条件组合覆盖
条件组合(Multiple Condition Coverage)指的是设计足够多的测试用例,使判定语句中每个条件的所有可能至少出现一次,并且每个判定语句本身的判定结果也至少出现一次,它与判定-条件覆盖的差别是,条件组合覆盖不是简单地要求每个条件都出现“真”与“假”两种结果,而是要求让这些结果的所有可能组合都至少出现一次。
以3.1.1节程序为例,该程序中共有四个条件:x>0、y<0、x>2、z>0,我们依然用S1、S2、S3、S4标记这四个条件成立,而-S1、-S2、-S3、-S4标记这些条件不成立。由于这四个条件每个条件都有取“真”、“假”两个值,因此所有条件结果的组合有2^4=16种
序号
|
组合
|
含义
|
1
|
S1、S2、S3、S4
|
x>0成立,y<0成立;x>2成立,z>0成立。
|
2
|
-S1、S2、S3、S4
|
x>0不成立,y<0成立;x>2成立,z>0成立。
|
3
|
S1、-S2、S3、S4
|
x>0成立,y<0不成立;x>2成立,z>0成立。
|
4
|
S1、S2、-S3、S4
|
x>0成立,y<0成立;x>2不成立,z>0成立。
|
5
|
S1、S2、S3、-S4
|
x>0成立,y<0成立;x>2成立,z>0不成立。
|
6
|
-S1、-S2、S3、S4
|
x>0不成立,y<0不成立;x>2成立,z>0成立。
|
7
|
-S1、S2、-S3、S4
|
x>0不成立,y<0成立;x>2不成立,z>0成立。
|
8
|
-S1、S2、S3、-S4
|
x>0不成立,y<0成立;x>2成立,z>0不成立。
|
9
|
S1、-S2、-S3、S4
|
x>0成立,y<0不成立;x>2不成立,z>0成立。
|
10
|
S1、S2、-S3、-S4
|
x>0成立,y<0成立;x>2不成立,z>0不成立。
|
11
|
S1、-S2、S3、-S4
|
x>0成立,y<0不成立;x>2成立,z>0不成立。
|
12
|
-S1、-S2、-S3、S4
|
x>0不成立,y<0不成立;x>2不成立,z>0成立。
|
13
|
-S1、-S2、S3、-S4
|
x>0不成立,y<0不成立;x>2成立,z>0不成立。
|
14
|
S1、-S2、-S3、-S4
|
x>0成立,y<0不成立;x>2不成立,z>0不成立。
|
15
|
-S1、S2、-S3、-S4
|
x>0不成立,y<0成立;x>2不成立,z>0不成立。
|
16
|
-S1、-S2、-S3、-S4
|
x>0不成立,y<0不成立;x>2不成立,z>0不成立。
|
经过分析可以发现,第2、6、8、13这4种情况是不存在的,这几种情况要求x>0不成立,x>2成立,这两种结果相悖,因此最终图3-1的所有条件组合情况有12种,根据这12种情况设计测试用例。
条件覆盖测试用例
序号
|
组合
|
测试用例
|
条件1
|
条件2
|
覆盖路径
|
||
x
|
y
|
z
|
|||||
test1
|
S1、S2、S3、S4
|
3
|
-1
|
5
|
1
|
1
|
ace
|
test2
|
S1、-S2、S3、S4
|
3
|
1
|
5
|
0
|
1
|
abe
|
test3
|
S1、S2、-S3、S4
|
1
|
-1
|
3
|
1
|
1
|
ace
|
test4
|
S1、S2、S3、-S4
|
3
|
-1
|
1
|
1
|
1
|
ace
|
test5
|
-S1、S2、-S3、S4
|
-5
|
-2
|
1
|
0
|
1
|
abe
|
test6
|
S1、-S2、-S3、S4
|
1
|
1
|
1
|
0
|
1
|
abe
|
test7
|
S1、S2、-S3、-S4
|
1
|
-1
|
1
|
1
|
0
|
acd
|
test8
|
S1、-S2、S3、-S4
|
6
|
1
|
-2
|
0
|
1
|
abe
|
test9
|
-S1、-S2、-S3、S4
|
-1
|
1
|
1
|
0
|
1
|
abe
|
test10
|
S1、-S2、-S3、-S4
|
1
|
1
|
-2
|
0
|
0
|
abd
|
test11
|
-S1、S2、-S3、-S4
|
-2
|
-1
|
-3
|
0
|
0
|
abd
|
test12
|
-S1、-S2、-S3、-S4
|
-3
|
1
|
-1
|
0
|
0
|
abd
|
与判定-条件覆盖相比,条件组合覆盖包括了所有判定-条件覆盖,因此它的覆盖范围更广。但是当程序中条件比较多时,条件组合的数量会呈指数型增长,组合情况非常多,要设计的测试用例也会增加,这样反而会使测试效率降低。
6 .实例:三角形逻辑覆盖问题
三角形逻辑覆盖:根据三角形三边的关系可将三角形分为4种类型:不构成三角形、一般三角形、等腰三角形、等边三角形。根据该原则实现一个判断三角形的程序。
三角形流程图
对程序进行分析,程序的执行路径如下图。
使用判定覆盖,使程序中每个判定语句至少有一次“真”值,至少有一次“假”值,根据流程图和路径图分析,可设计4个测试用例。
编号
|
测试用例
|
路径
|
预期输出
|
||
A
|
B
|
C
|
|||
test1
|
6
|
6
|
6
|
1-2-3-4-11
|
等边三角形
|
test2
|
6
|
6
|
8
|
1-2-5-6-11
|
等腰三角形
|
test3
|
3
|
4
|
5
|
1-2-7-8-11
|
一般三角形
|
test4
|
3
|
3
|
6
|
1-9-10-11
|
不构成三角形
|
二、插桩法
程序插桩就是往被测试程序中插入测试代码以达到测试目的的方法,插入的测试代码被称为探针。根据测试代码插入的时间可以将插桩法分为目标代码插桩和源代码插桩。
1.目标代码插桩法
目标代码插桩是指向目标代码(二进制代码)插入测试代码,获取程序运行信息的测试方法,也称为动态程序分析方法。
①目标代码插桩法的原理:是在程序运行平台和底层操作系统之间建立中间层,通过中间层检查执行程序、修改指令,开发人员、软件分析工程师等对运行的程序进行观察,判断程序是否被恶意攻击或者出现异常行为,从而提高程序的整体质量。
②目标代码插桩执行模式:
● 即时模式(Just-In-Time):原始的二进制或可执行文件没有被修改或执行,将修改部分的二进制代码生成文件副本存储在新的内存区域中,在测试时仅执行修改部分的目标代码。
● 解释模式(Interpretation Mode):在解释模式中目标代码被视为数据,测试人员插入的测试代码作为目标代码指令的解释语言,每当执行一条目标代码指令,程序就会在测试代码中查找并执行相应的替代指令,测试通过替代指令的执行信息就可以获取程序的运行信息。
● 探测模式(Probe Mode):探测模式使用新指令覆盖旧指令进行测试,这种模式在某些体系结构(如x86)中比较好用。
③目标代码插桩工具 :Pin和DynamoRIO
2.源代码插桩法
源代码插桩是指对源文件进行完整的词法、语法分析后,确认插桩的位置,植入探针代码。相比于目标代码插桩,源代码插桩具有针对性和精确性。
源代码码插桩模型
相比于目标代码插桩,源代码插桩实现复杂程度低。源代码插桩是源代码级别的测试技术,探针代码程序具有较好的通用性,使用同一种编程语言编写的程序可以使用一个探针代码程序来完成测试。
通过一个小案例来讲解源代码插桩。该案例是一个除法运算。
插入宏函数ASSERT(y),当除数为0时打印错误原因、出错文件、出错行数等信息提示。根据除法运算规则设计测试用例。
测试用例
|
数据输入
|
预期输出结果
|
T1
|
1,1
|
1
|
T2
|
1,-1
|
-1
|
T3
|
-1,-1
|
1
|
T4
|
-1,1
|
-1
|
T5
|
1,0
|
错误
|
T6
|
-1,0
|
错误
|
T7
|
0,0
|
错误
|
T8
|
0,1
|
0
|
T9
|
0,-1
|
0
|
总结:程序插桩测试方法有效的提高了代码测试覆盖率,但是插桩测试方法会带来代码膨胀、执行效率低下和HeisenBugs,在一般情况下插桩后的代码膨胀率在20%~40%,甚至膨胀率达到100%导致插桩测试失败。
小提示:HeisenBugs
HeisenBugs称为海森堡bug,它是一种软件缺陷,这种缺陷的重现率很低,当人们试图研究时bug会消失或改变行为。实际开发软件测试中,这种缺陷也比较常见,测试人员测试到一个缺陷提交给开发人员,开发人员执行缺陷重现步骤却得不到报告的缺陷,缺陷已经消失或者出现了其他缺陷。
[]多学一招:黑盒测试与白盒测试异同
1、黑盒测试和白盒测试比较黑盒测试过程中不用考虑内部逻辑结构,仅仅需要验证软件外部功能是否符合用户实际需求。黑盒测试可发现以下缺陷:
● 外部逻辑功能缺陷,如界面显示信息错误等
● 兼容性错误,如系统版本支持、运行环境等。
● 性能问题,如运行速度、响应时间等。
白盒测试与黑盒测试不同,白盒测试可以设计测试用例尽可能覆盖程序中分支语句,分析程序内部结构。白盒测试常用于以下几种情况:
● 源程序中含有多个分支,在设计测试用例时要尽可能覆盖所有分支,提高测试覆盖率。
● 内存泄漏检查迅速,黑盒测试只能在程序长时间运行中发现内存泄漏问题,而白盒测试能立即发现内存泄露问题。
2、测试阶段
测试名称
|
测试对象
|
测试方法
|
单元测试
|
模块功能(函数、类)
|
白盒测试
|
集成测试
|
接口测试(数据传递)
|
黑盒测试和白盒测试
|
系统测试
|
系统测试(软件、硬件)
|
黑盒测试
|
验收测试
|
系统测试(软件、硬件、用户体验)
|
黑盒测试
|
3.实例:求三个数的中间值
#include <stdio.h>int main() {int i,mid,a[3];for(i=0;i<3;i++)scanf("%d",&a[i]);mid=a[2];if(a[1]<a[2]){ if(a[0]<a[1])mid=a[1];else if(a[0]<a[2])mid=a[1];}else{if(a[0]>a[1])mid=a[1];else if(a[0]>a[2])mid=a[0];}printf("中间值是:%d\n",mid);return 0;}
设计测试用例
测试用例
|
测试数据
|
预期输出结果
|
T1
|
1,1,2
|
1
|
T2
|
1,2,3
|
2
|
T3
|
3,2,1
|
2
|
T4
|
3,3,3
|
3
|
T5
|
6,4,5,
|
5
|
T6
|
6,8,4
|
6
|
T7
|
8,4,9
|
8
|
执行测试用例之后发现,T7结果与预期结果不一致。经过分析prop.txt文件和代码,发现程序中存在逻辑错误:只要输入的数据满足a[0]和a[2]大于a[1]且a[0]小于a[3]时,运行结果就会错误。
除了逻辑错误,源程序将程序执行的信息覆盖写入到了prop.txt文件中,这样在查看prop.txt文件时只能看到最近一次的执行过程,这违背了测试可溯源的原则。在修改代码逻辑错误时,同时修改prop.txt的写入方式为追加写入。
#include <stdio.h>#define LINE() fprintf(__POINT__,"%3d",__LINE__)FILE *__POINT__;int i,mid,a[3];int main() {if((__PROBE__=fopen("test.txt","a+"))==NULL)fprintf(stderr,"不能打开test.txt文件");for(LINE(),i=0;i<3;LINE(),i++)LINE(),scanf("%d",&a[i]);LINE(),mid=a[2];if(LINE(),a[1]<a[2]){if(LINE(),a[0]<a[1])LINE(),mid=a[1];else if(LINE(),a[0]<a[2])if(a[0]<a[1])LINE(),mid=a[1] ;elsemid=a[0] ;} else{if(LINE(),a[0]>a[1])LINE(),mid=a[1];else if(LINE(),a[0]>a[2])LINE(),mid=a[0];} LINE(),printf("中间值是:%d\n",mid);fprintf(__POINT__,"\n");fclose(__POINT__);return 0;}