第6章 白盒测试技术
6.1 白盒测试基本技术
一、词法分析与语法分析
通过词法分析与语法分析可获取软件组成的重要基本因数,例如:变量标识符、过程标识符、常量等,组合这些基本因数可得到软件的基本信息如下:
1.标号交叉引用表
列出在各模块中出现的全部标号,在表中标出标号的属性,包括已说明、未说明、已使用、未使用。表中还包括在模块以外的全局标号、计算标号等。
2.变量交叉引用表(变量定义与引用表)
在表中应标明各变量的属性,包括已说明、未说明、隐式说明、类型及其使用情况,进一步还可区分是否出现在了赋值语句的右边,是否属于COMMON变量、全局变量或特权变量等。
3.子程序、宏和函数表
在表中列出以下内容:
(1)各个子程序、宏和函数的属性,包括已定义、未定义、定义类型;
(2)参数表、输入参数的个数、顺序、类型,输出参数的个数、顺序、类型;
(3)已引用、未引用、引用次数等。
4.等价表
表中列出在等价语句或等值语句中出现的全部变量和标号。
5.常数表
表中列出全部数字常数和字符常数,并指出其在哪些语句中首先被定义。
6.引用表
按功能分类,引用表的作用有以下3种:
(1)直接从表中查出说明或使用的错误,如循环层次表、变量交叉引用表等;
(2)为用户提供辅助信息,如子程序(宏、函数)引用表、等价表、常数表;
(3)用来做错误预测和程序复杂度计算,如操作符和操作数的统计表等。
二、静态错误分析
静态错误分析用于确定在源程序中是否存在某类错误或“危险”结构。它有以下几种:
1.类型和单位分析
为强化对源程序中数据类型的检查,在程序设计语言中扩充一些新的数据类型,这样可以静态预处理程序,分析程序中的类型错误。
2.引用分析
(1)简介
发现引用异常为静态错误分析中使用最广泛的技术。如果沿着程序的控制路径,变量在赋值以前被引用,或变量在赋值以后未被引用,此时就发生了引用异常。为了检测引用异常,需要检查通过程序的每条路径。
(2)原理
通常采用类似深度优先的方法遍历程序流图的每一条路径,也可以建立引用异常的探测工具,该工具包括两个表:定义表和未引用表,每张表中都包含一组变量表。未引用表中包括已被赋值但还未被引用的一些变量。
当扫描抵达一个出度大于1的节点V时,深度优先探测算法要求先检查最左分支的那一部分程序流图,然后再检查其他分支。在最左分支检查完之前,应把定义表与未引用表的当前内容用一个栈暂时存储起来,当最左分支检查完之后,算法控制返回到节点V,从栈中恢复该节点的定义表和未引用表的副本,然后再去遍历该节点的下一个分支,该过程要延续到全部分支检查完为止。
3.表达式分析
对表达式进行分析,以发现并纠正在表达式中出现的错误。包括以下内容:
(1)在表达式中不正确地使用了括号造成的错误;
(2)数组下标越界造成的错误;
(3)除数为零造成的错误;
(4)对负数开平方,或对π求正切造成的错误。
最复杂的一类表达式分析是对浮点数计算的误差进行检查。比如使用二进制数不精确地表示十进制浮点数,常常使计算结果出乎意料。
4.接口分析
接口一致性是程序的静态错误分析和设计分析共同研究的题目。接口一致性的设计可以分析检查模块之间接口的一致性和模块与外部数据库之间接口的一致性。程序关于接口的静态错误分析检查过程与实参在类型、函数过程之间接口的一致性,因此要检查形参与实参在类型、数量、维数、顺序、使用上的一致性和全局变量和公共数据区在使用上的一致性。
三、程序插桩技术
程序插桩是软件动态测试一种基本的测试手段。
1.方法简介
程序插桩方法是借助往被测程序中插入操作,来实现测试目的的方法。在调试程序时,常常要在程序中插入一些打印语句。目的在于,希望执行程序时,打印出最为关心的信息。进一步通过这些信息了解执行过程中程序的一些动态特性。从这一思想发展出的程序插桩技术能够按用户的要求,获取程序的各种信息,成为测试工作的有效手段。
如果想要了解一个程序在某次运行中所有可执行语句被覆盖(或称被经历)的情况,或是各语句的实际执行次数,最好的办法是利用插桩技术。这里仅以计算整数X和整数Y的最大公约数程序为例,说明插桩方法的要点。该程序的流程图如图6-1所示。图中虚线框并不是源程序的内容,而是为了记录语句执行次数而插入的。
图6-1 插桩后求最大公约数程序的流程图
(1)计数语句的简介
以上虚线框要完成的操作都是计数语句,其形式为:
C(i)=C(i)+1,i=1,2,…,6
程序从入口开始执行,到出口结束。凡经历的计数语句都能记录下该程序点的执行次数。如果在程序的入口处还插入了对计数器C(i)初始化的语句,在出口处插入了打印这些计数器的语句,构成了完整的插桩程序,便能记录并输出在各程序点上语句的实际执行次数。插桩后的程序如图6-2所示,图中箭头所指均为插入的语句(源程序语句略)。
图6-2 插桩程序中插入的语句
(2)插入的语句
通过插入的语句可获取程序执行过程中的动态信息。运行程序以后,一方面可检验测试的结果数据,另一方面还可借助插入语句给出的信息了解程序的执行特性。因此,有时把插入的语句称为“探测器”,借以实现“探查”或“监控”的功能。
在程序的特定部位插入记录动态特性的语句,最终是为了把程序执行过程中发生的一些重要历史事件记录下来。例如,记录在程序执行过程中某些变量值的变化情况,变化的范围等。像前面所讨论的程序逻辑覆盖的情况,只有通过程序插桩才能取得覆盖信息。程序插桩方法是应用很广的技术,特别是在完成程序的测试和调试时非常有效。
(3)设计插桩程序需考虑的问题
①探测哪些信息;
②在程序的什么部位设置探测点;
③需要设置多少个探测点。
其中前两个问题需要结合具体情况解决,第三个问题则需要考虑如何设置最少探测点的方案。一般情况下,在没有分支的程序段中只需一个计数语句。但程序中如果出现了多种控制结构,使得整个结构十分复杂,那么为了在程序中设计最少的计数语句,就需要针对程序的控制结构进行具体的分析。
(4)举例说明计数语句的设置部位
以下以Fortran程序为例,说明至少应在哪些部位设置计数语句:
①程序块的第一个可执行语句之前;
②entry语句的前后;
③有标号的可执行语句处;
④do、do while、do until及do终端语句之后;
⑤block-if,else if、else及endif语句之后;
⑥logical if语句处;
⑦输入/输出语句之后;
⑧call语句之后;
⑨计算go to语句之后。
2.断言语句
(1)简介
在程序中的特定部位插入某些用以判断变量特性的语句,使得程序执行中这些语句得以证实,从而使程序的运行特性得到证实,插入的这些语句称为断言(assertions)。该做法是程序正确性证明的基本步骤。
(2)举例说明
下面以求两个非负数NUM和DEN之商的Wensley迭代算法为例,对断言语句的作用作一简要说明。假定两个非负数中,NUM小于M(即所得之商小于1),算法中只用到加、减及除2的运算。该迭代算法的程序如图6-3所示。
图6-3 计算两非负数之商的迭代程序
①从程序中可以看出,在每次迭代中由分母得到的变量B以及权增量W都要缩小一半,而且变量A随着迭代次数的增加将接近分子。这些粗略的观察和分析可以用以下4个断言语句表达,在每次迭代开始时4个断言必定为真:
a.W=2-K(K是迭代次数,并且K≥0);
b.A=DEN*Q;
c.B=DEN*W/2;
d.NUM/DEN-W<Q≤NUM/DEN。
此外,在循环外W<E,而且结果Q总是从下面逼近真正的商,因此得到了输出断言:NUM/DEN-E<Q≤NUM/DEN,其和上面的第④断言很相似。
②假定所用的编译系统能够处理表达式形式的断言语句,插入断言以后的程序如图6-4所示,其中带有标记@的语句是断言语句。新增加的变量K只是在计算第①断言时用到。
a.首先检验初始化以后的断言
第一,由于K=0,所以是初值。W=2-K=1。
第二,由于Q=0,A的初始A=DEN*Q=0。
第三,将W的值代入DEN*W/2,则得B的初值:B=DEN/2。
第四,曾假定0≤NUM<DEN,NUM/DEN-1(W的值)必定小于零,因而也就小于Q(是零)。而且,NUM/DEN必定大于Q,由于NUM和DEN均为正,Q为零。
图6-4 插入断言后的迭代程序
以上说明这些断言在初始状态下为真。
b.继续迭代情况下断言为真的证明
如果继续迭代,要证明断言为真,就必须证明无论if-then结构中执行什么路径,这些断言都为真。先考虑在初始测试中NUM-A-B≥0为假,即检验失败的情况,然后给出程序向量的新值(A′,B′,W′,Q′和K′),有:
A*=A
B*=B/2
W*=W/2
Q*=Q
K*=K+1
c.再来检验4个断言
第一,W*=W/2=1/2**K*。
第二,A*=A=DEN*Q=DEN*Q*。
第三,B*=B/2=DEN*W/4=DEN*W*/2。
第四,把A和B代入(NUM-A-B<0),得到NUM-DEN*Q-DEN*W/2<0,对此关系式两端除以DEN,并加Q,得到,由于Q′=Q,W′=W/2,有NUM/DEN-W′<Q′,且Q≤NUM/DEN。
d.如果if-then检验成立,再来看4个断言
使用A″,B″,W″,Q″,和K″作为新的程序向量,有:
第一,W′=W/2=1/2**K″。
第二,A″=DEN*Q+DEN*W/2=DEN*(Q+W/2)=DEN*Q″。
第三,B″=B/2=DEN*W/4=DEN*W″/2。
第四,代入(NUM-A-B≥0),并作同前的变换,得到NUM/DEN-W″/2<Q″+W″/2或:NUM/DEN-W″<Q″,并且Q″≤NUM/DEN。
总之,无论执行哪一路径,在每一迭代的开始,4个断言均为真。尽管并未考虑输出断言,但是知道,第④断言成立,由于W<E,NUM/DEN-E<Q和Q≤NUM/DEN必定为真,故必定满足输出断言。
6.2 白盒测试方法
一、代码检查法
1.概述
代码检查包括桌面检查、代码审查和走查等,主要检查代码和设计的一致性,代码对标准的遵循,可读性,代码逻辑表达的正确性,代码结构的合理性等方面。发现违背程序编写标准的问题,程序中不安全、不明确和模糊的部分,找出程序中不可移植的部分、违背程序编程风格的问题,包括变量检查、命名、类型审查、程序逻辑审查、程序语法检查和程序结构检查等内容。
2.代码检查方式
(1)桌面检查
一种传统的检查方法,由程序员检查自己编写的程序。程序员在程序通过编译之后,对源程序代码进行分析、检验,并补充相关的文档,目的是发现程序中的错误。由于程序员熟悉自己的程序及其程序设计风格,桌面检查由程序员自己进行可以节省很多的检查时间,但应避免主观片面性。
(2)代码审查
①简介
代码审查是由若干程序员和测试员组成一个审查小组,通过阅读、讨论和争议,对程序进行静态分析的过程。
②步骤
a.第一步
小组负责人提前把设计规格说明书、控制流程图、程序文本及有关要求、规范等分发给小组成员,作为审查的依据。
b.第二步
小组成员在充分阅读这些材料后,进入审查的第二步,召开程序审查会。
在会上,首先由程序员逐句讲解程序的逻辑。在此过程中,程序员或其他小组成员可以提出问题,展开讨论,审查错误是否存在。实践表明,程序员在讲解过程中能发现许多原来自己没有发现的错误,而讨论和争议则促进了问题的暴露。
另外,在会前,应当给审查小组各成员准备一份常见错误的清单,把以往所有可能发生的常见错误罗列出来,供与会者对照检查,以提高审查的实效。
注意:该常见错误清单也称为检查表,把程序中可能发生的各种错误进行分类,对每一类列举出尽可能多的典型错误,然后把其制成表格,供再审查时使用。
(3)走查
①步骤
a.第一步
把材料先发给走查小组每个成员,成员们认真研究程序后再开会。
b.第二步
首先由测试组成员为所测程序准备一批有代表性的测试用例,提交给走查小组。走查小组开会,集体扮演计算机角色,让测试用例沿程序的逻辑运行一遍,随时记录程序的踪迹,供分析和讨论用。
②补充说明
a.借助测试用例的媒介作用,对程序的逻辑和功能提出各种疑问,结合问题开展热烈的讨论,发现更多的问题。
b.代码检查应在编译和动态测试之前进行,在检查前,应准备好需求说明文档、程序设计文档、程序的源代码清单、代码编码标准和代码缺陷检查表等。
c.在实际使用中,代码检查能快速找到缺陷,发现30%~70%的逻辑设计和编码缺陷,且代码检查看到的是问题本身而非征兆。但代码检查非常耗费时间,且代码检查需要知识和经验的积累。
d.代码检查可以使用测试软件进行自动化测试,以利于提高测试效率,降低劳动强度,或者进行人工测试,以充分发挥人力的逻辑思维能力。
3.代码检查项目
(1)检查变量的交叉引用表
重点是检查未说明的变量和违反类型规定的变量;还要对照源程序,逐个检查变量的引用、变量的使用序列、临时变量在某条路径上的重写情况,局部变量、全局变量与特权变量的使用情况。
(2)检查标号的交叉引用表
验证所有标号的正确性,检查所有标号的命名是否正确,转向指定位置的标号是否正确。
(3)检查子程序、宏、函数
验证每次调用与所调用位置是否正确,确认每次所调用的子程序、宏、函数是否存在,检验调用序列中调用方式与参数顺序、个数、类型的一致性。
(4)等价性检查
检查全部等价变量的类型的一致性,解释所包含的类型差异。
(5)常量检查
确认常量的取值、数制和数据类型,检查常量每次引用同其取值、数制和类型的一致性。
(6)标准检查
用标准检查工具软件或手工检查程序中违反标准的问题。
(7)风格检查
检查发现程序在设计风格方面的问题。
(8)比较控制流
比较由程序员设计的控制流图和由实际程序生成的控制流图,寻找和解释每个差异,修改文档并修正错误。
(9)选择、激活路径
在程序员设计的控制流图上选择路径,再到实际的控制流图上激活这条路径。如果选择的路径在实际控制流图上不能被激活,则源程序可能有错。
(10)比较发现差异
对照程序的规格说明,详细阅读源代码,逐字逐句进行分析和思考,比较实际的代码和期望的代码,从其差异中发现程序的问题和错误。
(11)补充文档
桌面检查的文档是一种过渡性的文档,不是公开的正式文档。通过编写文档,可以帮助程序员发现和抓住更多的错误。管理部门也可以通过审查桌面检查文档,了解模块的质量、完全性、测试方法和程序员的能力。
根据检查项目可以编制代码规则、规范和检查表等,还可作为测试用例,如编码规范、代码检查规则、缺陷检查表等。
4.编码规范
编码规范是程序编写过程中必须遵循的规则,一般会详细规定代码的语法规则、语法格式等,如表6-1所示。
表6-1 编码规范
5.代码检查规则
在代码检查中,需要依据被测软件的特点,选用适当的标准与规范。在使用测试软件进行自动化代码检查时,测试工具一般会内置许多的编码规则。根据编程语言以及被测程序的特点,挑选适当的规则进行检查。例如,在测试中,可以选择如表6-2所示的规则进行自动化测试,在此基础上使用桌面检查、代码走查、代码审查等人工检查的方法,仔细检查程序的结构、逻辑等方面的缺陷。
表6-2 代码检查规则
6.缺陷检查表
在进行人工代码检查时,代码缺陷检查表是用到的测试用例,一般包括容易出错的地方和在以往的工作中遇到的典型错误,如下所示:
(1)格式部分
①嵌套的if是否正确地缩进;
②注释是否准确并有意义;
③是否使用有意义的标号;
④代码是否基本上与开始时的模块模式一致;
⑤是否遵循全套的编程标准。
(2)入口和出口的连接
①初始入口和最终出口是否正确;
②当对另一个模块的每一次调用时,所需的全部参数是否已传送给每一个被调用的模块;
③被传送的参数值是否已正确地设置;
④对关键的被调用模块的意外情况(如丢失、混乱)是否有处理。
(3)程序语言的使用
①是否使用一个或一组最佳的动词;
②模块中是否使用完整定义的语言的有限子集;
③是否使用了适当的跳转语句。
(4)存储器使用
①每一个域在第一次使用前是否已正确地初始;。
②规定的域是否正确;
③每个域是否有正确的变量类型声明。
(5)判断和转移
①是否有判断正误的条件;
②用于判断的是否是正确的变量;
③每个转移目标是否正确并至少执行了一次。
(6)性能
①逻辑是否被最佳地编码;
②是否提供正式的错误/例外子程序。
(7)可维护性
①清单格式是否适于提高可读性;
②标号和子程序是否符合代码的逻辑意义。
(8)逻辑
①全部设计是否已实现;
②代码做的是否是设计规定的内容;
③每一个循环是否执行正确的次数。
(9)可靠性
①对从外部接口采集的数据是否有确认;
②是否遵循可靠性编程要求。
对应于不同的编程语言,代码缺陷检查表的具体内容将会不同。例如,针对广泛使用的C/C++语言,代码缺陷检查表如表6-3所示。
表6-3 代码缺陷检查表
二、静态结构分析法
静态结构分析中,测试者通过使用测试工具分析程序源代码的系统结构、数据结构、数据接口、内部控制逻辑等内部结构,生成函数调用关系图、模块控制流图、内部文件调用关系图、子程序表、宏和函数参数表等各类图形图表,可以清晰地标识整个软件系统的组成结构,通过分析这些图表,检查软件有没有存在缺陷或错误。
1.函数调用关系图
通过应用程序中各函数之间的调用关系展示系统结构。通过查看函数调用关系图,可以检查函数之间的调用关系是否符合要求,是否存在递归调用,函数的调用层次是否过深,有没有存在孤立的没有被调用的函数。从而可以发现系统是否存在结构缺陷以及一些内部结构信息。
2.模块控制流图
与程序流程图相类似的由许多节点和连接结点的边组成的一种图形,其中一个节点代表一条语句或数条语句(图中的各种图形符号代表不同的意义),边表示节点间的控制流向,它显示了一个函数的内部逻辑结构。模块控制流图可以直观地反映出一个函数的内部逻辑结构,通过检查这些模块控制流图,能够很快地发现软件的错误与缺陷。
(1)测试工具Logiscope所使用的基本图例如图6-5所示,典型的逻辑结构所对应的控制流图的基本结构如图6-6(a)、(b)、(c)所示
图6-5 Logiscope基本图例
图6-6 控制流图基本结构
(2)某GIS软件中某个函数的模块控制流图如图6-7所示
图6-7 GIS模块控制流图
该函数的结构存在重大缺陷,如下:
①可以看到在该函数中存在无法执行的死代码,即图中最右边的孤立出口;
②该函数有多达8个出口,其中一个属于无法到达的出口。由于可能没有在所有的出口进行动态内存的释放与回收操作,因此此结构存在内存泄漏的可能。
(3)某MIS软件中某个函数的模块控制流图如图6-8所示
图6-8 MIS模块控制流图
该函数的结构也同样存在重大缺陷,如下:
①该函数有多个出口,所以存在内存泄漏的可能;
②该函数结构复杂,有超过10个逻辑判断结点。在这些结点上出现逻辑错误的概率将大大增加,将降低其可靠性,而且过多的逻辑判断结点可能会破坏对CPU操作进行优化的处理,影响其运行性能。
三、静态质量度量法
1.软件质量内容
根据ISO/IEC9126国际标准的定义,软件的质量包括以下六个方面:
(1)功能性(FUNCTIONALITY);
(2)可靠性(RELIABILITY);
(3)可用性(USABILITY);
(4)有效性(EFFICIENCY);
(5)可维护性(MAINTAINABILITY);
(6)轻便性(PORTABILITY)。
2.相关术语
以ISO9126质量模型作为基础,可以构造质量度量模型,用于评估软件的各个方面。例如,按以下方法构造的质量模型可以度量程序的可维护性。首先,该模型从上到下分为以下三层:
(1)度量规则(Metrics)
度量规则用于规范软件的各种行为属性,它使用了代码行数、注释频度等参数度量软件的各种行为属性,具体参数定义如表6-4所示。
表6-4 度量规则参数表
(2)分类标准(criteria)
分类标准对应ISO9126质量模型的子特性。
①软件的可维护性采用分类标准来评估
a.可分析性(ANALYZABILITY)
b.可修改性(CHANGEABILITY)
c.稳定性(STABILITY)
d.可测性(TESTABILITY)
②每个分类标准由一系列度量规则组成,各规则分配一个权重,由规则的取值与权重值计算出每个分类标准的取值。各分类标准组成如表6-5所示。
表6-5 分类标准组
③各分类标准的结果按以下标准区分等级,如表6-6至表6-12所示。
function_TESTABILITY=DRCT_CALLS+LEVL+PATH+PARA
表6-6 function_TESTABILITY的等级划分
function_STABILITY=NBCALLING+RETU+DRCT_CALLS+PARA
表6-7 function_STABILITY的等级划分
function_CHANGEABILITY=PARA+LVAR+VOCF+GOTO
表6-8 function_CHANGEABILITY的等级划分
function_ANALYZABILITY=VG+STMT+AVGS+COMF
表6-9 function_ANALYZABILITY的等级划分
relativeCall_ANALYZABILITY=STRU_CPX+LEVELS
表6-10 relativeCall_ANALYZABILITY的等级划分
relativeCall_STABILITY=CALL_PATHS+HIER_CPX
表6-11 relativeCall_STABILITY的等级划分
relativeCall_TESTABILITY=TESTBTY+CALL_PATHS
表6-12 relativeCall_TESTABILITY的等级划分
④代码质量的等级
依据这些标准和最终测试结果,可将代码的质量分成四个等级:
a.优秀(EXCELLENT):符合本模型框架中的所有规则;
b.良好(GOOD):未大量偏离模型框架中的规则;
c.一般(FAIR):违背了模型框架中的大量规则;
d.较差(POOR):无法保障正常的软件可维护性。
其中前三者被认为是可以接受的,最后一个等级则是不可接受的。
(3)质量因素(factors)
质量因素对应ISO9126质量模型的质量特性。
①依据各分类标准取值组合权重方法来计算,如表6-13所示。
表6-13 质量因素权重计算表
②依据质量因素取值,将其分成四个等级:优秀(EXCELLENT)、良好(GOOD)、一般(FAIR)和较差(POOR),其中前三者被认为是可以接受的,最后一个等级则是不可接受的。function_MAINTAINABILITY和relativeCall_MINTA-INABILITY的等级划分分别如表6-14和表6-15所示。
表6-14 function_MAITALNABILITY的等级划分
表6-15 relativeCall_MAINTAINABILITY的等级划分
将上述质量模型应用于被测程序后,就可通过量化的数据对软件的质量进行评估。
四、逻辑覆盖法
1.简介
(1)白盒测试的动态测试用例原则
白盒测试的动态测试要根据程序的控制结构设计测试用例,其原则如下:
①保证一个模块中的所有独立路径至少被使用一次。
②对所有逻辑值均需测试true和false。
③在上下边界及可操作范围内运行所有循环。
④检查内部数据结构以确保其有效性。
(2)穷举测试
①举例说明
穷举测试流程图如图6-9所示,图中包括了一个执行达20次的循环,其所包含的不同执行路径数高达520条,假使有这样一个测试程序,对每一条路径进行测试需要1ms,假设一天工作24小时,一年工作365天,若要对其进行穷举测试,需要3024年的时间。
图6-9 穷举测试
②结论
实现穷举测试的工作量过大,需要的时间过长,实施起来是不现实的。任何软件开发项目都要受期限、费用、人力和机时等条件的限制,并且显然对所有有可能的数据进行测试来充分揭露程序中的所有隐藏错误在实际操作中是不可能。
(3)白盒测试用例设计方法
实用的白盒测试用例设计方法包括:逻辑覆盖法和基本路径测试法。其中,逻辑覆盖是通过对程序逻辑结构的遍历实现程序的覆盖,是一系列测试过程的总称。该测试逐步进行越来越完整的通路测试。
从覆盖源程序语句的详尽程度划分,逻辑覆盖标准包括以下不同的覆盖标准:语句覆盖(SC)、判定覆盖(DC)、条件覆盖(CC)、条件判定组合覆盖(CDC)、多条件覆盖(MCC)和修正判定条件覆盖(MCDC)。为便于理解,使用以下所示的程序(用C语言书写),其流程图如图6-10所示。
[程序]:
6-10 程序的流程图
2.语句覆盖(SC)
(1)定义
语句覆盖是指选择足够多的测试数据,使被测程序中每条语句至少执行一次,为了满足该条件,可以构造测试用例:a=T,b=T,c=T。
(2)优缺点
从程序中的每条语句都得到执行的角度看,语句覆盖的方法似乎能够比较全面地检验每一条语句,但是其最严重的缺陷是语句覆盖对程序执行的逻辑覆盖很低。
若一程序段中的逻辑运算有问题,例如,判定的第一个运算符“&&”错写成运算符“‖”,或第二个运算符“‖”错写成运算符“&&”,这时使用上述的测试用例仍然可以达到100%的语句覆盖,但上述的逻辑错误无法发现。故一般认为语句覆盖是很弱的逻辑覆盖。
3.判定覆盖(DC)(分支覆盖)
比语句覆盖稍强的覆盖标准是判定覆盖。
(1)定义
判定覆盖是指设计足够的测试用例,使得程序中的各判定至少都获得一次“真值”或“假值”,或使得程序中的各取“真”分支和取“假”分支至少经历一次。
(2)注意
除了双值的判定语句外,还有多值判定语句,如case语句,因此判定覆盖更一般的含义:使得每一个判定获得每一种可能的结果至少一次。
(3)测试用例
以上述代码为例,构造以下测试用例即可实现判定覆盖标准:
①a=T,b=T,c=T。
②a=F,b=F,c=F。
上述两组测试用例不仅满足判定覆盖,而且满足语句覆盖,故判定覆盖要比语句覆盖更强一些。但假如这一程序段中判定的逻辑运算有问题,如表6-16所示,判定的第一个运算符“&&”错写成运算符“‖”或第二个运算符“‖”错写成运算符“&&”,使用上述的测试用例可达到100%的判定覆盖,仍无法发现上述的逻辑错误。故需要更强的逻辑覆盖标准。
表6-16 判定覆盖
4.条件覆盖(CC)
(1)定义
条件覆盖是指构造一组测试用例,使得各判定语句中各逻辑条件的可能值至少满足一次。
(2)测试用例
①按照这一定义,上述例子要达到100%的条件覆盖,可以使用以下测试用例:
a.a=F,b=T,c=F。
b.a=T,b=F,c=T。
②上述用例在满足条件覆盖的同时,把判定的两个分支也覆盖了,这样只要达到了条件覆盖也就必然实现了判定覆盖。选用以下的两组测试用例为例:
a.a=F,b=T,c=T。
b.a=T,b=F,c=F。
会发现覆盖条件的测试用例并没有覆盖分支,如表6-17所示。为解决该矛盾,需要兼顾条件和分支。
表6-17 条件覆盖
5.条件判定组合覆盖(CDC)
(1)定义
条件判定组合覆盖是指设计足够的测试用例,使得判定中每个条件的所有可能(真/假)至少出现一次,并且每个判定本身的判定结果(真/假)也至少出现一次。
(2)测试用例
对于如图6-10所示的例子,选用以下的两组测试用例可以符合条件判定组合覆盖标准:
a.a=T,b=T,c=T。
b.a=F,b=F,c=F。
条件判定组合覆盖也存在一定缺陷,例如,判定的第一个运算符“&&”错写成运算符“‖”或第二个运算符“‖”错写成运算符“&&”,如表6-18所示,这时使用上述的测试用例仍然可以达到100%的条件判定组合覆盖,但上述的逻辑错误无法发现。
表6-18 条件判定组合覆盖
6.多条件覆盖(MCC)(条件组合覆盖)
(1)定义
多条件覆盖是指设计足够的测试用例,使得每个判定条件中的每一种可能组合都至少出现一次。显然满足多条件覆盖的测试用例是一定满足判定覆盖、条件覆盖和条件判定组合覆盖的。
(2)测试用例
对于如图6-19所示的例子,判定语句中有三个逻辑条件,各逻辑条件有两种可能取值,因此共有23=8种可能组合,如表6-19所示的测试用例保证了多条件覆盖。
表6-19 多条件覆盖
由上可知,当一个程序中判定语句较多时,其条件取值的组合数目是非常大的。
7.修正条件判定覆盖(MCDC)
(1)简介
该覆盖度量是由欧美的航空/航天制造厂商和使用单位联合制定的“航空运输和装备系统软件认证标准”,它需要足够的测试用例来确定各条件是否能够影响到包含的判定的结果。要求满足两个条件如下:
①各程序模块的入口和出口点都要至少被调用一次,每个程序的判定点的所有可能的结果值要至少转换一次。
②程序的判定被分解为通过逻辑操作符(and、or)连接的bool条件,各条件对于判定的结果值是独立的。
(2)测试用例
对于如图6-10所示的例子,可以设计8个用例,如表6-20所示,在此基础上,按照MCDC的要求选择需要的用例。
表6-20 修正条件判定覆盖
由表可知,布尔变量a可以通过用例1和5达到MCDC的要求(用例2和6或用例3和7也可以满足相应要求),变量b可以通过用例2和4达到MCDC的要求,变量c可以通过用例3和4达到MCDC的要求,因此使用用例集{1,2,3,4,5}即可满足MCDC的要求。显而易见,这不是唯一的用例组合。
五、基本路径测试法
在实际问题中,一个不太复杂的程序,其路径的组合都是一个庞大的数字。而基本路径测试是可以把覆盖的路径数压缩到一定限度内的测试方法,在程序控制流图的基础上,通过分析控制流图的环路复杂性,导出基本可执行路径的集合,然后据此设计测试用例。设计出的测试用例要保证测试中程序的每条可执行语句至少被执行一次。
1.程序的控制流图
控制流图是描述程序控制流的一种图示方式。
(1)基本的控制结构对应的图形符号如图6-11所示。在图6-11所示的图形符号中,圆圈称为控制流图的一个结点,表示一个或多个无分支的语句或源程序语句。
图6-11 控制流程图的图形符号
(2)一程序的流程图如图6-12(a)所示,可以映射成的控制流程图如图6-12(b)所示。
图6-12 程序流程图和对应的控制流程图
假定在流程图中用菱形框表示的判定条件内没有复合条件,而一组顺序处理框可以映射为一个单一的结点。控制流程图中的箭头(边)表示了控制流的方向,类似于流程图中的流线,一条边必须终止于一个结点,但在选择或者是多分支结构中分支的汇聚处,即使汇聚处没有执行语句也应该添加一个汇聚结点。边和结点圈定的部分叫区域,当对区域计数时,图形外的部分也应记为一个区域。
注意:如果判断中的条件表达式是复合条件,即条件表达式是由一个或多个逻辑运算符(or、and、nand和nor)连接而成的逻辑表达式,则需要将复合条件的判断改为一系列只有单个条件的嵌套的判断。例如,对应如图6-13所示的复合逻辑下的控制流图(a)的复合条件的判定,应该画成如图6-13(b)所示的控制流图。条件语句if a and b中条件a和条件b各有一个只有单个条件的判断结点。
图6-13 复合逻辑下的控制流图
2.程序环路复杂性
(1)定义1
①概述
程序的环路复杂性即McCabe复杂性度量,在进行程序的基本路径测试时,从程序的环路复杂性可导出程序基本路径集合中的独立路径条数,确保程序中每个可执行语句至少执行一次所必须的测试用例数目的上界。
②独立路径
独立路径是指包括一组以前没有处理的语句或条件的一条路径,一条独立路径是至少包含有一条在其他独立路径中从未有过的边的路径。在如图6-12(b)所示的控制流图中,一组独立的路径如下:
a.Path1:1-11;
b.path2:1-2-3-4-5-10-1-11;
c.path3:1-2-3-6-8-9-10-1-11;
d.path4:1-2-3-6-7-9-10-1-11。
一条新的路径必须包含一条新边。路径path1、path2、path3和path4组成了如图6-12(b)所示控制流图的一个基本路径集。只要设计出的测试用例能够确保这些基本路径的执行,就可使得程序中的各可执行语句都至少执行了一次,各条件的取真和取假分支也能得到测试。对于给定的控制流图,可以得到不同的基本路径集。
(2)定义2
环路复杂性还可简单地定义为控制流图的区域数。对于如图6-12(b)所示的控制流图,有4个区域,环路复杂性V(G)=4,是构成基本路径集的独立路径数的上界,可以据此得到应该设计的测试用例的数目。
3.基本路径测试法步骤
(1)主要步骤
基本路径测试法适用于模块的详细设计及源程序,其主要步骤如下:
①以详细设计或源代码作为基础,导出程序的控制流图;
②计算得到的控制流图G的环路复杂性V(G);
③确定线性无关的路径的基本集;
④生成测试用例,确保基本路径集中每条路径的执行。
(2)举例说明
以下以求平均值的过程average为例,阐述测试用例的设计过程。用PDL语言描述的average过程如下:
①以详细设计或源代码作为基础,导出程序的控制流图
利用在图6-11所示的控制流图的图形符号、图6-12所示的程序流图和对应的控制流图和图6-13所示的复合逻辑下的控制流图给出的符号和构造规则生成控制流图。针对上述过程,给将要映射为对应控制流图中一个结点的PDL语句或语句组,加上用数字表示的标号。加了标号的PDL程序及对应的控制流图如图6-14和图6-15所示。
图6-14 对average过程定义结点
图6-15 average过程的控制流图
②计算得到的控制流图G的环路复杂性V(G)
利用前面给出的计算控制流图环路复杂性的方法,算出控制流图G的环路复杂性。如果一开始就知道判断结点的个数,甚至不必画出整个控制流图,就可计算该图的环路复杂性的值。对于图6-15所示的控制流图,可算出:V(G)=6(区域数)=5(判断结点数)+1=6。
③确定线性无关路径的基本集
针对图6-15所示的average过程的控制流图计算出的环路复杂性的值,就是该图已有的线性无关基本路径集中路径数目。该图所有的6条路径如下:
a.[path1]1-2-10-11-13
b.[path2]1-2-10-12-13
c.[path3]1-2-3-10-11-13
d.[path4]1-2-3-4-5-8-9-2…
e.[path5]1-2-3-4-5-6-8-9-2…
f.[path6]1-2-3-4-5-6-7-8-9-2…
路径4、5、6后面的省略号(…)表示在控制结构中以后剩下的路径是可选择的。在很多情况下,标识判断结点,能够有效地帮助导出测试用例。上例中,结点2、3、5、6和10都是判断结点。
④生成测试用例,确保基本路径集中每条路径的执行
根据判断结点给出的条件,选择适当的数据,以保证每一条路径都可以被测试到。满足上述基本路径集的测试用例如下:
a.[path1] 输入数据:value[k]=有效输入,限于k<i(i定义如下)
value[i]=-999,当2≤i≤100。
预期结果:n个值的正确的平均值、正确的总计数。
注意:不能孤立地进行测试,应当作为路径4、5、6测试的一部分来测试。
b.[path2] 输入数据:value[1]=-999;
预期结果:平均值=-999,总计数取初始值。
c.[path3] 输入数据:试图处理101个或更多的值,而前100个应当是有效的值;
预期结果:与测试用例1相同。
d.[path4] 输入数据:value[i]=有效输入,且i<100;
value[k]<最小值,当k<i时;
预期结果:n个值的正确的平均值、正确的总计数。
e.[path5] 输入数据:value[i]=有效输入,且i<100;
value[k]>最大值,当k≤i时;
预期结果:11个值的正确的平均值、正确的总计数。
f.[path6] 输入数据:value[i]=有效输入,且i<100。
预期结果:n个值的正确的平均值、正确的总计数。
各测试用例执行之后,与预期结果进行比较。如果所有测试用例都执行完毕,则可确信程序中所有的可执行语句至少被执行了一次。但是,一些独立的路径,往往不完全孤立,有时是程序正常控制流的一部分,这时,这些路径的测试可以是另一条路径测试的一部分。
六、其他白盒测试方法
1.域测试
(1)概述
域测试是一种基于程序结构的测试方法。每条执行路径对应于输入域的一类情况,是程序的一个子计算。如果程序的控制流有错误,对于某一特定的输入可能执行的是一条错误路径,这种错误称为路径错误,也称域错误。域测试主要是针对域错误进行的程序测试。错误类型如下:
①计算型错误
由对特定输入执行的是正确路径,但由于赋值语句错误导致输出结果不正确而引起的。
②丢失路径错误
由于程序中的某处少了一个判定谓词而引起的。
(2)输入空间
域测试的“域”是指程序的输入空间。域测试方法基于对输入空间的分析。任何一个被测程序都有一个输入空间。测试的理想结果就是检验输入空间中的每个输入元素是否都产生正确的结果。而输入空间又可分为不同的子空间,每一子空间对应一种不同的计算。在考察被测试程序的结构后,会发现,子空间的划分由程序中分支语句中的谓词决定。输入空间的元素,经过程序某些特定语句的执行而结束,都满足这些特定语句被执行所要求的条件。域测试正是在分析输入域的基础上,选择适当的测试点以后进行的测试。
(3)缺陷
①为进行域测试对程序提出的限制过多;
②当程序存在很多路径时,所需的测试点也就很多。
2.符号测试
(1)概述
符号测试的基本思想是允许程序的输入不仅仅是具体的数值数据,也可以是符号值(可以是基本符号变量值,也可以是这些符号变量值的一个表达式)。故在执行程序过程中以符号的计算代替了普通测试执行中对测试用例的数值计算。所得到的结果自然是符号公式或是符号谓词。更明确地说,普通测试执行的是算术运算,符号测试执行的则是代数运算。因此符号测试可以认为是普通测试的一个自然扩充。
(2)地位
符合测试可当作程序测试和程序验证的一个折衷方法,原因如下:
①符号测试沿用了传统的程序测试方法,通过运行被测程序来验证其可靠性;
②由于一次符号测试的结果代表了一大类普通测试的运行结果,实际上是证明了程序接受此类输入后,所得输出是正确的还是错误的。最为理想的情况是,程序中仅有有限的几条执行路径。如果对这有限的几条路径都完成了符号测试,就能比较有把握地确认程序的正确性了。从符号测试方法的使用出发,关键任务在于开发出比传统的编译器功能更强,能够处理符号运算的编译器和解释器。
(3)存在的问题
①分支问题
当采用符号执行方法进行到某一分支点处时,分支谓词是符号表达式,该情况下通常无法决定谓词的取值,也不能决定分支的走向,需要测试人员做人工干预,或是以执行树的方法进行下去。如果程序中有循环,而循环次数又由输入变量决定,则无法确定循环的次数。
②二义性问题
数据项的符号值可能有二义性。该情况通常出现在带有数组的程序中。以下的程序段:
…
X(I)=2+A
X(J)=3
C=X(I)
…
如果I=J,则C=3;否则,C=2+A。但由于使用符号值运算,无法知道I是否等于J。
③大程序问题
符号测试中经常要处理符号表达式。随着符号执行的继续,一些变量的符号表达式会越来越大。特别是当符号执行树很大,分支点很多时,路径条件本身就变成了一个非常长的表达式。如果能有办法将其化简,会带来很大好处。但如果找不到化简的办法,将大幅度的增长符号测试的时间和运行空间,甚至给整个问题的解决带来难以克服的困难。
3.Z路径覆盖
(1)概述
完成路径测试的理想情况是做到路径覆盖。对于比较简单的小程序实现路径覆盖比较容易做到,但如果程序中出现多个判断和循环,可能的路径数目将会急剧增长,甚至达到天文数字,实现完全路径覆盖是不可能做到的。需要对循环机制进行简化,从而极大地减少路径的数量,使得覆盖这些有限的路径成为可能。称简化循环意义下的路径覆盖为Z路径覆盖。
(2)相关术语
①路径
分析程序中的路径是指:从入口开始检验程序,执行过程中经历的各语句,直到出口。这是白盒测试最为典型的问题。
②路径测试
着眼于路径分析的测试可称为路径测试。
③对循环化简
对循环化简是指限制循环的次数。无论循环的形式和实际执行循环体的次数多少,只考虑循环一次和零次两种情况。即只考虑执行时进入循环体一次和跳过循环体这两种情况。
两种最典型的循环控制结构图6-16(a)和(b)所示。前者先作判断,循环体B可能执行(假定只执行一次),也可能不执行。如同图6-16(c)所示的条件选择结构。后者先执行循环体B(假定也执行一次),再经判断转出,其效果也与(c)中给出的条件选择结构只执行右支的效果一样。
图6-16 循环结构简化成选择结构
注意:对于程序中的所有路径可以用路径树来表示。当得到某一程序的路径树后,从其根结点开始,进行一次遍历,再回到根结点,把所经历的叶结点名排列起来,得到一个路径。如果设法遍历所有的叶结点,就得到所有的路径。当得到所有的路径后,生成每个路径的测试用例,就可做到Z路径覆盖测试。
4.程序变异
程序变异方法是一种错误驱动测试。错误驱动测试是指该方法是针对某类特定程序错误而进行的。逐渐发现要找出程序中所有的错误几乎不可能,比较现实的解决办法是将错误的搜索范围尽可能地缩小,以利于专门测试某类错误是否存在。其好处在于,可以集中目标对软件危害最大的可能错误进行检查,而暂时忽略对软件危害较小的可能错误,可以取得较高的测试效率,并降低了测试成本。错误驱动测试主要有两种,即程序强变异和程序弱变异。
6.3 白盒测试综合策略
一、概述
白盒测试中,可使用各种测试方法的综合策略,如下:
(1)在测试中,应尽量先用工具进行静态结构分析;
(2)测试中可采取先静态后动态的组合方式:先进行静态结构分析、代码检查和静态质量度量,再进行覆盖率测试;
(3)利用静态分析的结果作为引导,通过代码检查和动态测试的方式对静态分析结果进行进一步的确认,使测试工作更为有效;
(4)覆盖率测试是白盒测试的重点,一般可使用基本路径测试法达到语句覆盖标准。对于软件的重点模块,应使用多种覆盖率标准衡量代码的覆盖率;
(5)在不同的测试阶段,测试的侧重点不同:在单元测试阶段,以代码检查、逻辑覆盖为主;在集成测试阶段,需要增加静态结构分析、静态质量度量;在系统测试阶段,应根据黑盒测试的结果,采取相应的白盒测试。
二、最少测试用例数计算
1.结构化程序的组成
结构化程序由以下3种基本控制结构组成:
(1)顺序型——构成串行操作;
(2)选择型——构成分支操作;
(3)重复型——构成循环操作。
为了避免出现测试用例极多的组合爆炸简化问题,把构成循环操作的重复型结构用选择结构代替,即并不对测试循环体所有情况进行重复执行,而只对循环体检验一次。这样,任一循环便改造成进入循环体或不进入循环体的分支操作了。
2.举例
(1)N-S图表示的基本控制结构
类似于流程图的N-S图表示的基本控制结构(图中A、B、C、D、S均表示要执行的操作,P是可取真假值的谓词,Y表真值,N表假值)如图6-17所示。其中图6-17(c)和图6-17(d)两种重复型结构代表两种循环。在作如上简化循环的假设后,对于一般的程序控制流,只考虑选择型结构。事实上其已经能体现顺序型和重复型结构了。
图6-17 N-S图表示的基本控制结构
(2)选择型结构
两个顺序执行的分支结构如图6-18所示。两个分支谓词P1和P2取不同值时,将分别执行a或b及c或d操作。要测试该小程序,需要至少提供4个测试用例才能做到逻辑覆盖,才能使得ac、ad、bc及bd操作均得到检验。此处的4是图中第1个分支谓词引出的两个操作,及第2个分支谓词引出的两个操作组合起来而得到的,即2×2=4;此处的2是由于两个并列的操作,1+1=2而得到的。
图6-18 两个串行的分支结构的N-S图
以如图6-19所示的程序为例,该程序中共有9个分支谓词。该图可分上下两层,分支谓词1的操作域是上层,分支谓词8的操作域是下层。此处需首先考虑较为复杂的上层结构。谓词1不满足时要做的操作又可进一步分解为两层,即图6-20中的图(a)和(b)。所需测试用例个数分别为1+1+1+1+1=5和1+1+1=3。因而两层组合,得到5×3=15。于是整个程序结构上层所需测试用例数为1+15=16,而下层十分显然为3。故最后得到整个程序所需测试用例数至少为16×3=48。
图6-19 计算最少的测试用例数实例
图6-20 最少测试用例数计算
三、测试覆盖准则
1.Foster的ESTCA覆盖准则
(1)背景
逻辑覆盖力图通过全面覆盖来查找错误,但是它并不能真的做到无遗漏,甚至一些小程序段都会遗漏。测试的漏洞,容易出现在容易发生问题的条件判断那里。面对这类情况,应该从中吸取的教训是测试工作要有重点,要多针对容易发生问题的地方设计测试用例。K.A.Foster从测试工作实践的教训出发,吸收了计算机硬件的测试原理,提出了一种经验型的测试覆盖准则,较好地解决了上述问题。
(2)错误敏感测试用例分析ESTCA规则
①规则内容
a.[规则1]
对于A rel B(rel可以是<,=和>)型的分支谓词,应适当地选择A与B的值,使得测试执行到该分支语句时,A<B,A=B和A>B的情况分别出现一次。
b.[规则2]
对于A rel1 C(rel1可以是“>”或是“<”,A是变量,C是常量)型的分支谓词,当rel1为“<”时,应适当地选择A的值,使:A=C-M。(M是距C最小的容器容许的正数,若A和C均为整型时,M=1)。同样,当rel1为“>”时,应适当地选择A,使:A=C+M。
c.[规则3]
对外部输入变量赋值,使其在每一测试用例中均有不同的值和符号,并与同一组测试用例中其他变量的值和符号不一致。
②注意
a.规则1是为检测rel的错误,规则2是为了检测“差1”之类的错误(如本应是“IFA>1”而错成“IFA>0”),规则3是为检测程序语句中的错误。
b.三个规则并不完备,但在普通程序的测试中有效。由于规则本身针对程序编写人员容易发生的错误,或是围绕着发生错误的频繁区域,从而提高了发现错误的命中率。
c.根据这里提供的规则来检验上述小程序段错误。应用规则1,对它测试时,选择I的值为0的情况出现一次,就可立即找出隐藏的错误。
d.ESTCA规则有很多缺陷,如下:
第一,有时不容易找到使得规则所指的变量值满足要求的输入数据。
第二,仍有很多缺陷发现不了。对于查找错误的广度问题在变异测试中得到了较好的解决。
2.Woodward等人的层次LCSAJ覆盖准则
LCSAJ是线性代码序列与跳转。一个LCSAJ是一组顺序执行的代码,以控制流跳转为其结束点。LCSAJ的起点是根据程序本身决定的。其起点是程序第一行或转移语句的入口点,或是控制流可以跳达的点。几个首尾相接,且第一个LCSAJ起点为程序起点,最后一个LCSAJ终点为程序终点的LCSAJ串就组成了程序的一条路径。一条程序路径可能由两个、三个或多个LCSAJ组成。基于LCSAJ与路径的关系,Woodward提出了LCSAJ覆盖准则,作为一个分层的覆盖准则,具体如下:
(1)[第一层]:语句覆盖。
(2)[第二层]:分支覆盖。
(3)[第三层]:LCSAJ覆盖。即程序中的每个LCSAJ都至少在测试中经历过一次。
(4)[第四层]:两两LCSAJ覆盖。即程序中每两个首尾相连的LCSAJ组合起来在测试中都要经历一次。
…
(5)[第n+2层]:每n个首尾相连的LCSAJ组合在测试中都要经历一次。
以上分层表明了越是高层的覆盖准则越难满足。在实施测试时,若要实现上述的Woodward层次的LCSAJ覆盖,需要产生被测程序的所有LCSAJ。
6.4 结论
由上可知,进行白盒测试需要投入巨大的测试资源,包括人力、物力和时间等,但仍然需要进行白盒测试的原因如下:
一、逻辑错误和不正确假设与一条程序路径被运行的可能性成反比
错误往往出现在设计和实现主流之外的功能、条件或控制的工作中,日常处理往往被很好地了解(和很好地细查),而“特殊情况”的处理则难以发现。
二、经常相信某逻辑路径不可能被执行,而事实上,其可能在正常的基础上被执行
程序的逻辑流有时违反直觉,意味着关于控制流和数据流的一些无意识的假设,可能导致设计错误,只有路径测试才能发现这些错误。
三、印刷上的错误是随机的
当一个程序被翻译为程序设计语言源代码时,有可能产生某些打印错误,很多将被语法检查机制发现,但其他的错误只有在测试开始时才会被发现。打印错误出现在主流上和出现在不明显的逻辑路径上的可能性一样。