白盒测试之循环语句覆盖法(蓝桥课学习笔记)
1、简单循环
实验介绍
循环是反复运行同一段代码的语法结构,是代码中常见的一种结构。在白盒测试中,循环结构的测试也是我们需要掌握的内容。循环结构测试主要的侧重点是验证循环结构的有效性,一般可以结合条件覆盖、基本路径覆盖以及黑盒测试方法中的等价类、边界值等方法来设计测试用例。
在白盒测试中循环可以分为四种:简单循环、串接循环、嵌套循环和非结构化循环,其中非结构化循环的可读性、可维护性和可测试性都很差,一般建议重新设计并调整为结构化的程序代码后再进行测试。
简单循环是最简单的循环,即只有一个循环且没有嵌套,例如,一个 while 循环、一个do-while 循环、一个 for 循环。下图是两种简单循环的示意图:
本实验主要通过一个实例介绍简单循环结构的测试方法。注:实验过程中需要使用 eclipse 软件,请读者实验前在电脑上下载并安装好 eclipse 软件。
知识点
- 简单循环测试
实验内容
本次实验的测试对象是求任意一个10以内整数的阶乘,具体需求为:输入1 ~ 10的任意整数,输出该数字的阶乘数;输入不为 1 ~ 10 的整数时提示“请输入 1 ~ 10 的整数!
下面是一段计算数字阶乘的 Java 代码,这段代码只有一个循环且没有嵌套,属于一个简单循环,下面我们就以计算阶乘为例介绍简单循环的测试方法。
public static int getFactorial(Integer num) {
int result = 0;
if (num >= 1 && num <= 10){
result = 1;
int i = 1;
while (i < num){
result = result * i;
i++;
}
System.out.println(num + "的阶乘为:" + result);
}
else{
System.out.println("请输入1~10的整数!");
}
return result;
}
实验步骤
第 1 步:分析源代码,画出流程图。
这个步骤主要是帮助我们理清思路,为后面的测试用例设计打下基础。如果代码比较简单,或是对测试用例设计方法比较熟练以后,可以简化流程图,也可以省略这一步,直接进行测试用例设计。本例的参考流程图如下:
第 2 步:设计测试用例。
循环测试的侧重点是测试循环结构的有效性,主要考虑循环的边界和运行界限执行循环体的情况,所以设计简单循环结构的测试用例主要需要考虑循环变量的初始值、增量、最大值,以及边界取值的情况下代码处理是否正确。我们可以结合黑盒测试用例设计方法中的等价类边界值的方法来设计测试用例,即”如果输入/输出条件规定了值的个数,则用最大个数、最小个数、比最小个数少 1 ,比最大个数多 1 的值作为测试数据“。一般来说,简单循环的测试用例需要考虑下列几种情况(设最大循环次数为 n ):
(1)循环 0 次:测试跳过整个循环的场景;
(2)循环 1 次:目的是检查循环的初始值是否正确;
(3)循环 2 次:目的是检查多次循环是否正确;
(4)循环 m 次(其中 2 < m < n - 1):目的是检查多次循环是否正确,这里我们也可以用等价类的思想来理解,即:可以把大于 2 次、小于 n - 1 次看成是一个等价类,m 可以是这个范围中的任意一个值,根据等价类的思想,如果这个范围中的任意一个值是不会发现程序的问题,那么,我们可以认为这个等价类中所有的值都不会发现程序的问题;
(5)循环 n - 1 次:目的是检查边界值是否正确;
(6)循环 n 次:目的是检查边界值是否正确;
(7)循环 n + 1 次:目的是检查边界值是否正确。这里读者可能会有疑问,一个循环的最大循环次数是 n ,我们要怎么让它循环 n + 1 次呢?这不是一个伪命题吗?通过对边界值方法的理解,我们可以知道,等于、大于、小于边界值的地方是最容易出现 bug 的,如,“差 1 错”,即不正确的多循环或者少循环了一次。在循环结构的测试中设计循环 n + 1次的测试用例,就是为了检查代码是否会出现多循环一次的错误。在实际的测试过程中,我们可以通过分析代码结构决定是否能设计出循环 n + 1次的测试用例。
在本例中,根据以上原则我们可以设计如下测试用例数据:
循环次数 | 0 次 | 1 次 | 2 次 | m 次 | n-1 次 | n 次 | n+1 次 |
---|---|---|---|---|---|---|---|
测试用例( num 的值) | 0 | 1 | 2 | 5 | 9 | 10 | 11 |
转化为测试用例,如下表所示:
测试用例编号 | 输入 | 预期输出 |
---|---|---|
testcase_01 | 0 | result=0,输出:请输入1~10的整数! |
testcase_02 | 1 | result=1,输出:1的阶乘是1 |
testcase_03 | 2 | result=2,输出:2的阶乘是2 |
testcase_04 | 5 | result=120,输出:5的阶乘是120 |
testcase_05 | 9 | result=362880,输出:9的阶乘是362880 |
testcase_06 | 10 | result=3628800,输出:10的阶乘是3628800 |
testcase_07 | 11 | result=0,输出:请输入1~10的整数! |
第 3 步:执行测试用例。
白盒测试用例一般使用专门的测试工具(如:Junit)来执行,使用这些工具可以非常方便的编写测试用例、判断测试用例执行结果是否正确。在没有学习测试工具之前,我们先使用调用被测函数的方法来执行测试用例。具体执行方法为:
1)依次使用测试用例的输入值调用被测对象;
2)比较被测对象的实际返回值与测试用例的“预期输出”是否一致:如果一致,则测试用例执行通过;如果不一致,则测试用例执行失败。
具体的测试代码如下:
package test;
public class simpleTest {
// 执行测试用例
public static void main(String[] args) {
// 执行用例 testcase_01
if(getFactorial(0) == 0){
System.out.println("testcase_01执行通过\n");
}
else{
System.out.println("预期输出为:0 ");
System.out.println("testcase_01执行失败\n");
}
// 执行用例 testcase_02
if(getFactorial(1) == 1){
System.out.println("testcase_02执行通过\n");
}
else {
System.out.println("预期输出为:1 ");
System.out.println("testcase_02执行失败\n");
}
// 执行用例 testcase_03
if(getFactorial(2) == 2){
System.out.println("testcase_03执行通过\n");
}
else{
System.out.println("预期输出为:2 ");
System.out.println("testcase_03执行失败\n");
}
// 执行用例 testcase_04
if(getFactorial(5) == 120){
System.out.println("testcase_04执行通过\n");
}
else{
System.out.println("预期输出为:120 ");
System.out.println("testcase_04执行失败\n");
}
// 执行用例testcase_05
if(getFactorial(9) == 362880){
System.out.println("testcase_05执行通过\n");
}
else{
System.out.println("预期输出为:362880 ");
System.out.println("testcase_05执行失败\n");
}
// 执行用例testcase_06
if(getFactorial(10) == 3628800){
System.out.println("testcase_06执行通过\n");
}
else{
System.out.println("预期输出为:3628800 ");
System.out.println("testcase_06执行失败\n");
}
// 执行用例 testcase_07
if(getFactorial(11) == 0){
System.out.println("testcase_07执行通过\n");
}
else{
System.out.println("testcase_07执行失败\n");
}
}
以上代码在 eclipse 中执行的结果如下图所示:
从上图可以看出,七个测试用例有四个是执行失败的。分析这四个用例的实际输出和预期输出,我们可以发现:实际输出和预期输出相比,少乘了一个该数字本身,例如,测试用例 testcase_03 “ 2 的阶乘”的实际输出是 1 ,预期输出是 2 ,少乘了 2 ;测试用例 testcase_04 “ 5 的阶乘”的实际输出是 24 ,预期输出是 120 ,少乘了 5 。由此我们可以推断出,计算阶乘的 while 循环少循环了一次,即 while (i < num) 应该改为 while (i <= num) 。 。 修改代码后重新执行测试用例进行回归测试的结果如下,所有用例都执行通过:
实验总结
简单循环的测试重点是验证循环结构的有效性,主要考虑循环的边界和运行界限执行循环体的情况。对于最多为 n 次的简单循环,一般需要设计跳过循环、循环 1 次、2 次,m 次(2<m<n-1)、n - 1 次、n 次、n + 1 次的测试用例,重点测试循环变量的初值、最大值、增量以及退出循环的情况。如果循环的最大循环次数不确定,一般设计跳过循环、循环 1 次、2 次,m 次的测试用例即可。
2、嵌套循环
实验介绍
嵌套循环是指一个循环语句的循环体内含有其他的循环语句的语法结构,while、for 等循环语句都可以进行嵌套。最常见的嵌套循环是 for 循环中嵌套 for 循环。嵌套循环执行时,外层循环每执行一次内层循环会执行多次,循环的总次数等于外层循环次数与内层循环次数的积。下面是一个嵌套循环的示意图:
本实验主要通过一个实例介绍嵌套循环结构的测试方法。注:实验过程中需要使用 eclipse 软件,请读者实验前在电脑上下载并安装好 eclipse 软件。
知识点
- 嵌套循环测试
实验内容
冒泡排序是大家都比较熟悉的一种排序算法,在算法中有两个 for 循环,这也是一个典型的嵌套循环的例子。下面是 Java 编写的一段冒泡排序的代码,本实验我们就以这段冒泡排序代码为例,介绍如何测试嵌套循环结构。
//冒泡排序
public static int[] bubble_sort(int[] numbers){
for (int i = 0; i < numbers.length - 1;i++ ){
boolean flag = false;
for (int j = 0;j < numbers.length - 1 - i;j++){
if (numbers[j] > numbers[j+1]){
int temp = 0;
temp = numbers[j];
numbers[j] = numbers[j+1];
numbers[j+1] = temp;
flag = true;
}
}
if (flag == false){
break;
}
}
return numbers;
}
实验步骤
第 1 步:分析源代码,画出流程图。
从上面的代码可以看出,冒泡排序有两个 for 循环,外层循环根据数组的长度控制外层循环次数,内层循环则是将大数下沉,实现冒泡的过程。本例的参考流程图如下:
第 2 步:设计测试用例。
嵌套循环和简单循环的测试侧重点是相同的,都是侧重于验证循环结构的有效性。但是我们也不能直接将简单循环的测试方法应用于嵌套循环,因为如果按照简单循环的思路,测试用例的数量将随着嵌套层次的增加而成几何级增长,让测试变得非常困难。那么,怎样设计测试用例才能让嵌套循环的测试既能尽可能覆盖全面、又能减少测试用例数量呢?我们可以从以下几方面进行考虑:
1)按简单循环的方法对最内层循环进行测试,其他循环次数设置为最小值;
2)由内向外逐步对每一层循环进行测试,直到所有各层循环都测试完成。测试时将当前循环的所有外层循环的循环次数设置为最小值,所有内层循环的循环次数设置为典型值;
3)对各层循环同时取最小循环次数进行测试,如果有最大次数,再同时取最大循环次数进行测试。
下面我们根据这几个测试用例设计的原则来设计本实验中冒泡排序的测试用例:
1)设计内层循环的测试用例:用简单循环的方法对下面的最内层循环进行测试,将外层循环的循环次数设置为最小值 。
for (int j = 0;j < numbers.length-1-i;j++){
if (numbers[j] > numbers[j+1]){
int temp = 0;
temp = numbers[j];
numbers[j] = numbers[j+1];
numbers[j+1] = temp;
flag = true;
}
}
在本例中,根据以上原则我们可以对内层循环设计如下测试用例:
跳过循环:只有一个数时,内层循环不会执行,如:{3}
循环 1 次:当数组中有两个数字时,内层循环会循环一次,如:{21,2}
循环 2 次:当数组中有三个数字,且是按从小到大的顺序排列时,外层循环只会循环 1 次,为该层循环的最小值,而内层循环会循环两次,如:{1,2,21}
循环 m 次:根据简单循环测试用例设计的原则,如果循环没有最大循环次数,我们可以选择任意一个大于 2 的循环次数设计一个测试用例测试多次循环是否正确。这里我们设计一个循环 5 次的测试用例 ,通过分析代码的循环结构,我们可以知道,当传入6个数字,且数字是按从小到大的顺序排列时,外层循环只循环 1 次,内层循环会循环 5 次,如:{1,4,7,11,23,65}
2)设计外层循环的测试用例:
跳过循环:只有一个数时,外层循环不会执行,如:{3}
循环 1 次:当传入的数字都是按从小到大的顺序排列时,外层循环只会循环一次,如:{1,3,5,9}
循环 2 次:当数组中的数字需要交换一次位置时,外层循环会循环两次,如:{3,9,5,48,90}
循环 m 次:可以选择任意一个大于 2 的循环次数设计一个测试用例测试外层多次循环是否正确,如:{76,2,22,59,5,155,1,90,18}
3)对各层循环同时取最小循环次数或最大循环次数进行测试。在本例中内层和外层循环最小次数 1 都已有相关用例覆盖,这里不再重复设计。
综上所述,去重后冒泡排序的测试用例如下:
测试用例编号 | 输入 | 预期输出 |
---|---|---|
testcase_01 | {3} | {3} |
testcase_02 | {21,2} | {2,21} |
testcase_03 | {1,2,21} | {1,2,21} |
testcase_04 | {1,4,7,11,23,65} | {1,4,7,11,23,65} |
testcase_05 | {1,3,5,9} | {1,3,5,9} |
testcase_06 | {3,9,5,48,90} | {3,5,9,48,90} |
testcase_07 | {76,2,22,59,5,155,1,90,18} | {1,2,5,18,22,76,90} |
第 3 步:执行测试用例。
在这里还是使用调用被测函数的方法来执行测试用例,在简单循环中我们使用的是单个用例输入测试数据,判断预期结果的方法,这里换一种更简洁的方式,把所有测试用例输入和预期输出数据都初始化以后一起执行用例、判断实际执行结果与预期输出是否一致。具体的测试代码如下:
package test;
import java.util.Arrays;
public class NestingloopTest {
// 执行测试用例
public static void main(String[] args){
//初始化测试用例输入数据
int[][] input_testcase = {
{3},
{21,2},
{1,2,21},
{1,4,7,11,23,65},
{1,3,5,9},
{3,9,5,48,90},
{76,2,22,59,5,155,1,90,18}
};
//初始化测试用例预期结果
int[][] expect_result = {
{3},
{2,21},
{1,2,21},
{1,4,7,11,23,65},
{1,3,5,9},
{3,5,9,48,90},
{1,2,5,18,22,59,76,90,155}
};
//执行测试用例
for (int i = 0; i < 7; i++){
int[] execute_result = bubble_sort(input_testcase[i]);
//比较的执行结果与测试用例的预期输出是否一致:如果一致则用例执行通过,如果不一致则用例执行失败
Boolean test_result = Arrays.equals(execute_result, expect_result[i]);
if (test_result){
System.out.println("testcase_0" + (i + 1) + "执行通过!\n");
}
else{
System.out.println("预期结果为:" + Arrays.toString(expect_result[i]));
System.out.println("实际结果为:" + Arrays.toString(execute_result));
System.out.println("testcase_0" + (i + 1) + "执行失败!\n");
}
}
}
//冒泡排序
public static int[] bubble_sort(int[] numbers){
for (int i = 0; i < numbers.length - 1; i++ ){
boolean flag = false;
for (int j = 0; j < numbers.length - i - 1; j++){
if (numbers[j] > numbers[j+1]){
int temp = 0;
temp = numbers[j];
numbers[j] = numbers[j+1];
numbers[j+1] = temp;
flag = true;
}
}
if (flag == false){
break;
}
}
return numbers;
}
}
以上代码在 Eclipse 中执行的结果如下图所示,从图中可以看出所有的测试用例都执行通过了:
实验总结
嵌套循环结构的测试侧重点与简单循环相同,都是侧重于验证循环的有效性,不同点在于嵌套循环结构设计测试用例时需考虑用尽可能少的用例覆盖更全面的场景,降低测试成本,提高测试效率。
3、串接循环
实验介绍
串接循环是指两个或多个循环连接在一起的循环结构,也称连锁循环。串接循环分为两种:第一种是各个循环体彼此独立、相互之间没有关联关系,这种循环我们可以使用简单循环的方法,依次对每个独立的循环体进行测试;第二种串接循环是各个循环体之间有关联关系,第二个循环的输入来自于第一个循环的输出,对于这种串接循环,我们可以考虑使用嵌套循环的测试方法来进行测试。
下图是串接循环的示意图:
因为第一种串接循环可以完全使用简单循环的方法来测试,这里不再赘述,本实验主要使用一个实例介绍各个循环体有关联关系的串接循环的测试方法。注:实验过程中需要使用 eclipse 软件,请读者实验前在电脑上下载并安装好 eclipse 软件。
知识点
- 串接循环测试
实验内容
下面是一段将数组中的最大数分解质因数的代码,这是一个串接循环的例子,本实验我们就以这段代码为例,介绍如何测试串接循环结构。
public static String test(int[] numbers){
int max_number = 0;
int factor = 2;
String result = "";
//求数据组中的最大值
for (int i = 0; i < numbers.length - 1; i++){
if (max_number < numbers[i]){
max_number = numbers[i];
}
}
//将最大值分解质因数
int tmp = max_number;
while(factor <= tmp){
if(factor == tmp){
result = result + Integer.toString(factor);
break;
}
else if(tmp % factor == 0){
result = result + factor + "*";
tmp = tmp / factor;
}
else{
factor++;
}
}
System.out.println(max_number + "分解质因数的结果为:" + result);
return result;
}
实验步骤
第 1 步:分析代码结构。
通过观察我们可以发现:这段代码中一共有两个循环,第一个 for 循环的输出是第二个 while 循环的输入,也就是上面所说的第二种串接循环,即各个循环体之间有关联关系的串接循环。本例的流程图与实验介绍中典型的串接循环流程图类似,所以此处不再重复。
第 2 步:设计测试用例。
串接循环与嵌套循环、简单循环的测试侧重点一样,也是侧重于验证循环结构的有效性。对于循环体之间有关联关系的串接循环,我们可以使用嵌套循环的测试方法来进行测试,即:
1)按简单循环的方法对下层循环进行测试,其他循环次数设置为最小值;
2)由下至上逐步对每一层循环进行测试,直到所有循环都测试完成。测试时将当前循环的所有上层循环的循环次数设置为最小值,所有下层循环的循环次数设置为典型值;
3)对各层循环同时取最小循环次数进行测试,如果有最大次数,再同时取最大循环次数进行测试。
下面我们根据这几个原则来设计测试用例:
1)设计第一个 for 循环的测试用例:
//求数据组中的最大值
for (int i = 0; i < numbers.length - 1; i++){
if (max_number < numbers[i]){
max_number = numbers[i];
}
}
用简单循环的方法对下面的最内层循环进行测试,将外层循环的循环次数设置为最小值 ,可以设计如下测试用例:
跳过循环:当传入的数组为空时会跳过 for 循环,即:{}
循环 1 次:当传入的数组中只有一个数字时,for 循环只会循环一次,如:{6}
循环 2 次:当传入的数组中有两个数字时 for 循环会循环两次,如:{75,11}
循环 m 次:因为这个循环没有最大循环次数,所以可以选择任意一个大于 2 的循环次数设计一个测试用例测试多次循环是否正确。这里我们设计一个循环 6 次的测试用例 ,如:{20,6,90,21,45,76}
2)设计第二个 while 循环的测试用例:
while(factor <= tmp){
if(factor == tmp){
result = result + Integer.toString(factor);
break;
}
else if(tmp % factor == 0){
result = result + factor + "*";
tmp = tmp / factor;
}
else{
factor++;
}
}
跳过循环:当传入的数组为空或传入的数组中只有数字 1 时,会跳过 while 循环,因为测试 for 循环的时候我们已经设计了数组为空的测试用例,所以这里我们选择数组中只有数字 1 的用例,即:{1}
循环 1 次:当数组中的最大值为 2 时,while 循环只会执行 1 次,如:{2,1}
循环 2 次:当数组中的最大值为 3 或 4 时,while 循环会执行 2 次,如:{4,1,3,2}
循环 m 次:可以选择任意一个大于 2 的循环次数设计一个测试用例测试 while 循环是否正确,如:{27,5,50,2,100,11,21}
3)对各个循环同时取最小循环次数或最大循环次数进行测试。在本例中两个循环的最小次数 1 都已有相关用例覆盖,这里不再重复设计。
综上所述,测试用例如下:
测试用例编号 | 输入 | 预期输出 |
---|---|---|
testcase_01 | {} | 空 |
testcase_02 | {6} | 2 * 3 |
testcase_03 | {75,11} | 3 * 5 * 5 |
testcase_04 | {20,6,90,21,45,76} | 2 * 3 * 3 * 5 |
testcase_05 | {1} | 空 |
testcase_06 | {2,1} | 2 |
testcase_07 | {4,1,3,2} | 2 * 2 |
testcase_08 | {27,5,50,2,100,11,21} | 2 * 2 * 5 * 5 |
第 3 步:执行测试用例。
调用被测函数执行测试用例,判断实际执行结果与预期输出是否一致。具体的测试代码如下:
package test;
public class CascadeCycleTest {
// 执行测试用例
public static void main(String[] args){//初始化测试用例输入数据
int[][] input_testcase = {
{},
{6},
{75,11},
{20,6,90,21,45,76},
{1},
{2,1},
{4,1,3,2},
{27,5,50,2,100,11,21},
};
//初始化测试用例预期结果
String[] expect_result = {
"",
"2*3",
"3*5*5",
"2*3*3*5",
"",
"2",
"2*2",
"2*2*5*5",
};
//执行测试用例
for (int i = 0; i < 8; i++){
String execute_result = test(input_testcase[i]);
//比较的执行结果与测试用例的预期输出是否一致:如果一致则用例执行通过,如果不一致则用例执行失败
Boolean test_result = execute_result.equals(expect_result[i]);
if (test_result){
System.out.println("testcase_0" + (i + 1) + "执行通过!\n");
}
else{
System.out.println("预期结果为:" + expect_result[i+1]);
System.out.println("实际结果为:" + execute_result);
System.out.println("testcase_0" + (i + 1) + "执行失败!\n");
}
}
}
public static String test(int[] numbers){
int max_number = 0;
int factor = 2;
String result = "";
//求数据组中的最大值
for (int i = 0; i < numbers.length - 1; i++){
if (max_number < numbers[i]){
max_number = numbers[i];
}
}
//将最大值分解质因数
int tmp = max_number;
while(factor <= tmp){
if(factor == tmp){
result = result + Integer.toString(factor);
break;
}
else if(tmp % factor == 0){
result = result + factor + "*";
tmp = tmp / factor;
}
else{
factor++;
}
}
System.out.println(max_number + "分解质因数的结果为:" + result);
return result;
}
}
以上代码在 Eclipse 中执行的结果如下图所示:
从上图中可以看出,测试用例 testcase_02 的执行结果为失败。分析该用例输出的实际结果,可以发现待分解质因数的数为空,也就是第一个循环(数据组中的最大值)没有正确地计算出数组 {6} 中的最大值。通过分析第一个循环的代码,我们可以发现问题出在 for 循环的最大循环次数,代码中 for 的最大循环次数是数组的长度减 1 ,导致只有一个数字的数组无法计算出最大值。
根据以上分析,我们可以将这段代码修改如下:
//求数据组中的最大值
for (int i = 0; i < numbers.length; i++){
if (max_number < numbers[i]){
max_number = numbers[i];
}
}
修改代码后再次执行测试用例进行回归测试的结果如下:
实验总结
各个循环体彼此独立的串接循环可以使用简单循环的方法单独测试每一个循环,循环体之间有关联关系的串接循环则可以考虑使用嵌套循环的测试方法来进行测试。