4.1 白盒测试
白盒测试关注的是测试用例执行的程度或覆盖程序逻辑结构(源代码)的程度。如同我们在本书第2章所看到的,完全的白盒测试是将程序中每条路径都执行到,然而对一个带有循环的程序来说,完全的路径测试并不切合实际。
逻辑覆盖测试
如果完全从路径测试中跳出来看,那么有价值的目标似乎就是将程序中的每条语句至少执行一次。遗憾的是,这恰是合理的白盒测试中较弱的准则。图4-1描述了这种思想。假设图4-1代表了一个将要进行测试的小程序,其等价的Java代码段如下:
通过编写单个的测试用例遍历程序路径ace,可以执行到每一条语句。也就是说,通过在点a处设置A=2,B=0,X=3,每条语句将被执行一次(实际上,X可被赋任何值)。
遗憾的是,这个准则相当不足。举例来说,也许第一个判断应是“或”,而不是“与”。如果这样,这个错误就会发现不到。另外,可能第二个判断应该写成“X>0”,这个错误也不会被发现。还有,程序中存在一条X未发生改变的路径(路径abd),如果这是个错误,它也不会被发现。换句话说,语句覆盖这条准则有很大的不足,以至于它通常没有什么用处。
判定覆盖或分支覆盖是较强一些的逻辑覆盖准则。该准则要求必须编写足够的测试用例,使得每一个判断都至少有一个为真和为假的输出结果。换句话说,也就是每条分支路径都必须至少遍历一次。分支或判定语句的例子包括switch、do-while和if-else语句。在一些程序语言如FORTRAN中,多重选择GOTO语句也是合法的。
判定覆盖通常可以满足语句覆盖。由于每条语句都是在要么从分支语句开始,要么从程序入口点开始的某条子路径上,如果每条分支路径都被执行到了,那么每条语句也应该被执行到了。但是,仍然还有至少三种例外情况:
·程序中不存在判断。
·程序或子程序/方法有着多重入口点。只有从程序的特定入口点进入时,某条特定的语句才能执行到。
·在ON单元(ON-unit)里的语句。遍历每条分支路径并不一定能确保所有的ON单元都能执行到。
由于我们将语句覆盖视为一个必要条件,那么,作为似乎更佳准则的判定覆盖的定义理应涵盖语句覆盖。因此,判定覆盖要求每个判断都必须有“是”和“否”的结果,并且每条语句都至少被执行一次。换一种更简单的表达方式,即每个判断都必须有“是”和“否”的结果,而且每个入口点(包括ON单元)都必须至少被调用一次。
我们的探讨仅针对有两个选择的判断或分支,当程序中包含有多重选择的判断时,判定/分支覆盖准则的定义就必须有所改变。典型的例子有包含select(case)语句的Java程序,包含算术(三重选择)IF语句、计算或算术GOTO语句的FORTRAN程序,以及包含可选GOTO语句或GO-TO-DEPENDING-ON语句的COBOL程序。对于这些程序,判定/分支覆盖准则将所有判断的每个可能结果都至少执行一次,以及将程序或子程序的每个入口点都至少执行一次。
在图4-1中,两个涵盖了路径ace和abd,或涵盖了路径acd和abe的测试用例就可以满足判定覆盖的要求。如果我们选择了后一种情况,两个测试用例的输入是A=3,B=0,X=3和A=2,B=1,X=1。
判定覆盖是一种比语句覆盖更强的准则,但仍然相当不足。举例来说,我们仅有50%的可能性遍历到那条X未发生变化的路径(也即,仅当我们选择前一种情况)。如果第二个判断存在错误(例如把X>1写成了X<1),那么前面例子中的两个测试用例都无法找出这个错误。
比判定覆盖更强一些的准则是条件覆盖。在条件覆盖情况下,要编写足够的测试用例以确保将一个判断中的每个条件的所有可能的结果至少执行一次。因为,就如同判定覆盖的情况一样,这并不总是能让每条语句都执行到,因此作为对这条准则的补充就是对程序或子程序,包括ON单元的每一个入口点都至少调用一次。举例来说,分支语句包含两种情况:K是否小于或等于50?以及J+K是否小于QUEST?因此,需要针对K<=50、K>50(达到循环的最后一次迭代)以及J+K<QUEST、J+K>=QUEST的情况设计测试用例。
图4-1有四个条件:A>1、B=0、A=2以及X>1。因此需要足够的测试用例,使得在点a处出现A>1、A<=1、B=0及B<>0的情况,在点b处出现A=2、A<>2、X>1及X<=1的情况。有足够数量的测试用例满足此准则,用例及其遍历的路径如下所示:
请注意,尽管在本例中生成的测试用例数量是一样的,但条件覆盖通常还是要比判定覆盖更强一些。因为,条件覆盖可能(但并不总是这样)会使判断中的各个条件都取到两个结果(“真”和“假”),而判定覆盖却做不到这一点。举例来说,在相同的分支语句中,存在一个两重分支(执行循环体,或者跳过循环体)。如果使用的是判定覆盖测试,将循环从K=0执行到K=51即可满足该准则,但从未考虑到WHILE子句为假的情况。如果使用的是条件覆盖准则,就需要设计一个测试用例为J+K<QUEST产生一个为假的结果。
虽然条件覆盖准则乍看上去似乎满足了判定覆盖准则,但并不总是如此。如果正在测试判断条件IF(A&B),条件覆盖准则将要求编写两个测试用例:A为真,B为假;A为假,B为真。但是这并不能使IF语句中的THEN被执行到。对图4-1所示例子所进行的条件覆盖测试涵盖了全部判断结果,但这仅仅是偶然情况。举例来说,两个可选的测试用例:
涵盖了全部的条件结果,却仅涵盖了四个判断结果中的两个(这两个测试用例都涵盖到了路径abe,因而不会执行第一个判断结果为真的路径,以及第二个判断结果为假的路径)。
显然,解决上面左右为难局面的办法就是所谓的判定/条件覆盖准则。这种准则要求设计出充足的测试用例,将一个判断中的每个条件的所有可能的结果至少执行一次,将每个判断的所有可能的结果至少执行一次,将每个入口点都至少调用一次。
判定/条件覆盖准则的一个缺点是尽管看上去所有条件的所有结果似乎都执行到了,但由于有些特定的条件会屏蔽掉其他的条件,常常并不能全部都执行到。请参见图4-2来观察此种情况。图4-2中的流程图描述的是编译器将图4-1中的程序编译生成机器代码的过程。源程序中的多重条件判断被分解成单个的判断和分支,因为大多数的机器都没有能执行多重条件判断的单独指令。那么,更为完全的测试覆盖似乎是将每个基本判断的全部可能的结果都执行到,而前两个判定覆盖的测试用例都做不到这点;它们未能执行到判断H中的结果为“假”的分支,以及判断K中结果为“真”的分支。
如图4-2所示,其中的原因是“与”和“或”表达式中某些条件的结果可能会屏蔽掉或阻碍其他条件的判断。举例来说,如果“与”表达式中有个条件为“假”,那么就无须计算该表达式中的后续条件。同样,如果“或”表达式中有个条件为“真”,那么后续条件也无须计算。因此,条件覆盖或判定/条件覆盖准则不一定会发现逻辑表达式中的错误。
所谓的多重条件覆盖准则能够部分解决这个问题。该准则要求编写足够多的测试用例,将每个判定中的所有可能的条件结果的组合,以及所有的入口点都至少执行一次。举例来说,考虑下面的伪代码程序:
要测试四种情况:
1.I<=TABSIZE,并且NOTFOUND为真;
2.I<=TABSIZE,并且NOTFOUND为假(在到达表格尾部前查询指定条目);
3.I>TABSIZE,并且NOTFOUND为真(查询了整个表格,未找到指定条目);
4.I>TABSIZE,并且NOTFOUND为假(指定条目位于表格的最后位置)。
很容易发现,满足多重条件覆盖准则的测试用例集,同样满足判定覆盖准则、条件覆盖准则以及判定/条件覆盖准则。
再次回到图4-1中,测试用例必须覆盖以下8种组合:
1.A>1,B=0 5.A=2,X>1
2.A>1,B<>0 6.A=2,X<=1
3.A<=1,B=0 7.A<>2,X>1
4.A<=1,B<>0 8.A<>2,X<=1
注意,与左边的情况一样,第5至第8组合表示了第二个if语句的值。由于x可能在该if语句之前发生了改变,因此这个if语句所需的值必需对程序逻辑进行回溯,以找到相对应的输入值。
要测试的这8种组合并不一定意味着需要设计出8个测试用例。实际上,用4个测试用例就可以覆盖它们。下面是这些测试用例的输入,以及它们覆盖的组合:
A=2,B=0,X=4 覆盖组合1,5
A=2,B=1,X=1 覆盖组合2,6
A=1,B=0,X=2 覆盖组合3,7
A=1,B=1,X=1 覆盖组合4,8
图4-1的程序存在4条不同的路径,需要4个测试用例,这样的情况纯属巧合。事实上,这4个用例也没有覆盖到每条路径,路径acd就被遗漏掉了。举例来说,对于如下所示的判断语句,尽管它只包含两条路径,仍可能需要8个测试用例:
在存在循环的情况下,多重条件覆盖准则所需要的测试用例的数量通常会远远小于其路径的数量。
总的来说,对于包含每个判断只存在一种条件的程序,最简单的测试准则就是设计出足够数量的测试用例,实现:(1)将每个判断的所有结果都至少执行一次;(2)将所有的程序入口(例如入口点或ON单元)都至少调用一次,以确保全部的语句都至少执行一次。而对于包含多重条件判断的程序,最简单的测试准则是设计出足够数量的测试用例,将每个判断的所有可能的条件结果的组合,以及所有的入口点都至少执行一次(加入“可能”二字,是因为有些组合情况难以生成)。