在设计测试数据的时候,应当牢记,测试的目标是披露错误。如果用来寻找错误的测试数据找不到错误,我们就可以有信心相信程序的正确性。目标是采用那些能够以尽量少的测试数据来发现尽量多的错误的测试数据。为了弄清楚对于一个给定的测试数据,程序是否存在错误,首先必须知道对于该测试数据,程序的正确结果应是什么。
上节的二次方程求解的例子中,可以用如下两种方法来判断给定任意测试数据时程序的正确输出是什么。第1种方法是,计算出所测试二次方程的根。例如系数(a,b,c)=(1,-5,6)的二次方程的根为2和3,用测试数据(1,-5,6)对程序进行测试,即用程序所输出的根与2和3进行比较,以验证程序的正确性。第2种可行的方法用测试数据(1,-5,6)对程序进行测试,即是把程序所产生的根代入二次函数以验证函数的值是否真为0,如果程序输出的是2和3,可以计算出f(2)=2 2-5*2+6=0,f(3)=3 2-5*3+6=0。可以把以上验证方法用计算机程序来实现。对于第1种方法,编写测试程序输入测试组(a,b,c)和期望的根,然后把程序计算出的根与期望的根进行比较;对于第2种方法,可以编写代码来计算被测试程序输出的根相应的二次函数的函数值,然后验证这个值是否为0。
一般主要从两个方面来选择测试数据:一是这个数据能够发现错误的程度,二是能验证采用这个数据时程序的正确性。
设计测试数据的技术有3种,白盒测试法、黑盒测试法和灰盒测试法。第3种方法是前两种的混合。不同的测试在选择测试用例方面有着很大差别。在黑盒法中,选择测试用例考虑的是被测程序的功能,而不是实际的代码。在白盒法中,选择测试用例时是通过检查被测试程序代码来设计测试数据,以便使测试数据的执行结果能很好地覆盖被测试程序的语句以及执行路径。
1.黑盒法
最流行的黑盒法是I/O分类及因果图,这里仅探讨I/O分类。在这种方法中,输入数据和输出数据空间被分成若干类,不同类中的数据会使程序所表现出的行为有质的不同,而相同类中的数据则使程序表现出本质上类似的行为。二次方程求解的例子中有3种本质上不同的行为:产生复数根,产生实数根且不同,产生实数根且相同。可以根据这3种行为把输入空间分为3类。第1类中的数据将产生第1种行为;第2类中的数据将产生第2种行为;而第3类中的数据将产生第3种行为。一个测试集应至少从每一类中抽取一个输入数据。
2.白盒法
白盒法基于对代码的考察来设计测试数据。对一个测试集最起码的要求就是使被测程序中的每一条语句都至少执行一次。这种要求被称为语句覆盖(statement coverage)。对于二次方程求解的例子,测试集{(1,-5,6),(1,-8,16),(1,2,5)}将使程序中的每一条语句都得以执行,而测试集{(1,-5,6),(1,3,2),(2,5,2)}则不能提供语句覆盖。
在分支覆盖中要求测试集要能够使程序中的每一个条件都分别能出现true和false两种情况。二次方程求解的FindRoots程序中的代码有两个条件:(d>0)和(d==0)。在进行分支覆盖测试时要求测试集至少能使条件(d>0)和(d==0)分别出现一次为true、一次为false的情况。下面再以寻找数组中的最大元素所在位置的程序为例,说明白盒法中对测试数据的要求。
int Max(int a[ ],int n ){//寻找a[n]中的最大元素 int pos=0; for (int i=1;i<n;i++) if (a[pos]<a[i]) pos=i; return pos;}
程序返回数组a[n]中最大元素所在的位置。它依次扫描a[0]到a[n-1],并用变量pos来保存到目前为止所能找到的最大元素的位置。数据集{2,4,6,8,9}能够提供语句覆盖,但不能提供分支覆盖,因为条件a[pos]<a[i]不会变成false。数据集{4,2,6,8,9}既能提供语句覆盖也能提供分支覆盖。
可以进一步加强分支覆盖的条件,要求每个条件中的每个从句既能出现true也能出现false的情况,这种加强的条件被称为从句覆盖。一个从句在形式上被定义成一个不包含布尔操作符(如&&、||、!)的布尔表达式。表达式x>y,x+y<y*z以及c(c是一个布尔类型)都是从句的例子。考察如下语句:
if(C1 && C2)||(C3&& C4)) S1;else S2;
其中C1,C2,C3和C4是从句,S1和S2是语句。在分支覆盖方式下,需要使用一个能使
((C1 && C2)||(C3 && C4))
为true的测试数据以及一个能使该条件为false的测试数据。而从句覆盖则要求测试数据能使4个从句C1,C2,C3和C4都分别至少取一次true值和至少取一次false值。
还可以继续加强从句覆盖要求使用16个测试数据集:每一个测试集对应4个从句值组合的情形。不过,其中有些组合是不可能的。
如果按照某个测试数据集来排列程序语句的执行次序,可以得到一条执行路径。不同的测试数据可能会得到不同的执行路径。如例13.14中解二次方程的程序仅存在3条执行路径,第1行至第6行,第1、2、7~8行,第1、2、7、9~13行。而寻找数组中的最大元素所在位置程序的执行路径则随着n的增加而增加。执行路径覆盖要求测试数据集能使每条执行路径都得以执行。对于二次方程求解程序,语句覆盖、分支覆盖、从句覆盖以及执行路径覆盖都是等价的,但对于寻找数组中的最大元素所在位置程序,语句覆盖、分支覆盖、和执行路径覆盖三者是不同的,而分支覆盖和从句覆盖是等价的。
在上述这些白盒测试方法中,一般要求实现执行路径覆盖。一个能实现全部执行路径覆盖的测试数据同样能实现语句覆盖和分支覆盖,然而,它可能无法实现从句覆盖。全部执行路径覆盖通常会需要无数的测试数据或至少是非常可观的测试数据,所以在实践中一般不可能进行全部执行路径覆盖。
但是,所使用的测试数据应至少提供语句覆盖。此外,必须测试那些可能会使程序出错的特定情形。例如,对于一个用来对n≥0个元素进行排序的程序,除了测试n的正常取值外,还必须测试n=0、1这两种特殊情形;如果该程序使用数组a[100],而n=0和99分别表示边界条件,那么还需要测试n=0和99这两种情况。
3.灰盒法
灰盒测试方法是前两种的混合。可以根据情况让两种方法相互配合,以便充分发挥黑盒法和白盒法各自的优点。
4.调试程序
测试能够发现程序中的错误。一旦测试过程中产生的结果与所期望的结果不同,就可以了解到程序中存在错误。确定并纠正程序错误的过程被称为调试(debug)。尽管透彻地形容程序调试的方法超出了本书的范围,但这里还是提供一些好的建议给大家:
(1)可以用逻辑推理的方法来确定错误语句。如果这种方法失败,还可以进行程序跟踪,以确定程序什么时候开始出现错误。如果对于给定的测试数据程序需要运行很多指令,因而需要跟踪太多语句,很难人工确定错误,此时,这种方法就不太可行了,在这种情况下,必须试着把可疑的代码分离出来,专门跟踪这段代码。
(2)不要试图通过产生异常来纠正错误。异常的数量可能会迅速增长。必须首先找到需要纠正的错误,然后根据需要重新设计。
(3)在纠正一个错误时,必须保证不会产生一个新的、以前没有的错误。要用原本能使程序正确运行的测试数据来运行纠正过错误的程序,确信对于该数据,程序仍然正确。
(4)在测试和调试一个有错的程序时,应从一个与其他函数独立的函数开始。这个函数应该是一个典型的输入或输出函数,然后每次引入一个尚未测试的函数,测试并调试更大一些的程序。这种策略被称为增量测试与调试(incremental test and debug)。在使用这种策略时,可以有理由认为产生错误的语句位于刚刚引入的函数之中。