一、单元测试的定义和目的
单元测试是针对单元内部的测试,一般在编码完成后由测试工程师或测试工程师与开发工程师共同完成。单元测试只关心模块内部是否存在问题,不关心模块之间的接口,这就好比工厂组装电脑,在组装之前会对所有零部件进行测试,确定零件都没有问题后再进行组装。
1、单元测试的定义
单元测试是对软件的基本组成单元进行的测试,如函数、类或类的方法。单元测试的依据是模块的详细设计文档,主要关注一个单元是否正确实现了规定的功能、代码逻辑是否正确、输入输出是否正确、代码是否符合相关的标准规范等。
单元测试是对软件的最小可测试单元(即可独立编译或汇编的程序模块)进行的测试活动,也称为模块测试。单元可以是程序的模块或功能模块,具有一些基本属性,如:名称、明确的功能、规格定义,使用的数据与其他单元的联系等。在不同的编程语言中,单元的表现形式有所不同:在 C 等结构化编程语言中,单元一般是一个函数或过程;在 C++ 、Java 等面向对象的语言中,单元一般是指类或类的方法。
2、单元测试的目的
软件单元测试的目的是:
- 检查每个软件单元能否正确地实现设计说明中的功能、性能、接口和其他设计约束等要求;
- 发现单元内部可能存在的各种差错;
- 提高软件质量。
二、单元测试过程
单元测试是软件测试过程中的一个关键环节,它与集成测试、系统测试一样,分为测试策划、测试设计、测试执行和测试总结几个阶段。
单元测试过程中每个阶段需要完成的主要工作如下:
三、测试方法
1、语句覆盖
语句覆盖法是指设计适当数量的测试用例,使被测程序中的每条语句至少被执行一次。语句覆盖率的计算方法为:至少被执行一次的语句数量 / 程序中可执行的语句总数。
如下:
public static int test(int a,int b,int c) {
1 int result = 0;
2 if(a == 0 or b > 2) {
3 result = b - a;
}
4 if(a > 0 and c > 0 ) {
5 result = c * a;
}
6 return result;
}
第 1 步:分析待测试程序,画出程序的流程图。上述代码的参考流程图如下图所示(为了方便后面的讲解,笔者在流程图中标上了序号)。如果对语句覆盖法比较熟悉或代码逻辑比较简单,也可以省略画流程图这一个步骤。
第 2 步:分析流程图,编写测试用例。
根据语句覆盖法的定义,我们需要设计一些测试用例,使程序中的每条语句至少被执行一次。通过对第一步中的流程图进行分析,我们设计如下表中的一个测试用例即可以将所有语句全部覆盖。
测试用例编号输入数据预期结果语句覆盖情况testcase_01a = 1 , b = 3 , c = 9result = 9覆盖语句 1,2,4,3,5, 6
总结:语句覆盖可以使程序中的语句都被测试到,但是它也是覆盖最弱的一种逻辑覆盖方法,无法发现程序中的很多逻辑错误,需要和别的覆盖方法结合起来使用才能保证覆盖更为全面。
2、分支覆盖法
分支覆盖,也叫判定覆盖,是指运行代码进行测试时,程序中的所有判定语句的真、假分支至少都被执行过一次。分支覆盖率的计算方法为:测试时覆盖到的判定语句真、假分支的总数 / 程序中判定语句真、假分支的总数。例如,判定语句 if a > 0 有真、假两个分支,如果设计一个测试用例 a = 3 ,则该判定语句的真分支可以被覆盖,分支覆盖率为 1 / 2 = 50% ; 如果设计两个测试用例 a = 3 ,a = -1 ,分别使用这两个测试用例数据执行被测试程序,则该判定语句的真、假分支都可以被覆盖到,分支覆盖率为 2 / 2 = 100% 。
例题如下:
public static int test(int a,int b,int c) {
1 int result = 0;
2 if(a == 0 or b > 2) {
3 result = b - a;
}
4 if(a > 0 and c > 0 ) {
5 result = c * a;
}
6 return result;
}
第 1 步:分析待测试代码,画出程序的流程图。上述代码的参考流程图如下图所示:
第 2 步:分析流程图,编写测试用例。
根据分支(判定)覆盖的定义,我们需要设计一些测试用例,使程序中的每个判定条件至少被执行一次,即上图中的判断语句 ② 和 ③ 的真、假分支都需要至少被执行一次。因此,我们可以设计如下表中的两个测试用例覆盖所有的真、假分支。
测试用例编号输入数据预期结果分支覆盖情况语句覆盖情况testcase_01a = 0 , b = 5 , c = 9result = 5覆盖判断语句 ② 的真分支和判断语句 ③ 的假分支1,2,4,3,6testcase_02a = 5 , b = -2 , c = 3result = 15覆盖判断语句 ② 的假分支和判断语句 ③ 的真分支1,2,3,5,6
总结: 分支(判定)覆盖比语句覆盖的的覆盖效果要强一些,但是分支(判定)覆盖可能还是无法发现程序中的一些逻辑错误,仍需结合其他白盒测试用例设计方法才能覆盖全面。
3、条件覆盖法
条件覆盖是指运行代码进行测试时,程序中所有判断语句中的条件取值为真值为假的情况都被覆盖到,即每个判断语句的所有条件取真值和假值的情况都至少被经历过一次。条件覆盖率的计算方法为:测试时覆盖到的条件语句真、假情况的总数 / 程序中条件语句真、假情况的总数。
例题如下:
public static int test(int a,int b,int c) {
1 int result = 0;
2 if(a == 0 or b > 2) {
3 result = b - a;
}
4 if(a > 0 and c > 0 ) {
5 result = c * a;
}
6 return result;
}
第 1 步:分析待测试代码,画出程序的流程图。上述代码的参考流程图如下图所示:
第 2 步:分析流程图。
通过分析流程图,我们可以知道:这段代码中有两个判断语句,分别是上图中标识为 ② 和 ③ 的语句,语句 ② 中有两个条件,分别为 a == 0 和 b > 2 ;语句 ③ 中也有两个条件,分别为 a > 0 和 c > 0 。为了使后续的分析过程更加清晰明了,我们先来梳理一下流程图中的条件语句,并进行相应的标识,具体如下:
条件取值标识a == 0真Y1a == 0假N1b > 2真Y2b > 2假N2a > 0真Y3a > 0假N3c > 0真Y4c > 0假N4
第 3 步:使用条件覆盖法编写测试用例。
根据条件覆盖法的定义,我们需要设计一些测试用例,使程序中所有判定语句中的每个条件为真和为假的情况都至少被执行一次,即上表中列出的 8 种情况都需要至少被执行一次。因此,我们可以设计如下表中的测试用例来对程序中的四个条件进行覆盖:
测试用例编号输入数据预期结果条件覆盖情况分支覆盖情况testcase_01a = 0 , b = 5 , c = 9result = 5Y1、Y2、N3、Y4判断语句 ② 的真分支和③ 的假分支testcase_02a = 5 , b = 1 , c = -3result = 0N1、N2、Y3、N4判断语句 ② 的假分支和 ③ 的假分支
总结:条件覆盖法可以使程序中判断语句的每个条件都至少被覆盖一次,但满足了条件覆盖却不一定会满足分支(判定)覆盖,对于有些程序判定的错误仍无法发现。
4、分支-条件覆盖
分支(判定)覆盖是设计一定量的测试用例使程序中的每个判断语句的真假分支都得到覆盖,但是分支覆盖不能保证判断语句中每个条件的真、假分支都得到覆盖。那么,使用条件覆盖是否可以解决覆盖不全面的问题呢?通过上一小节的学习,我们发现:条件覆盖虽然可以覆盖判断语句中每个条件的真、假分支,但可能没有将所有判断语句的真、假分支覆盖全,仍然做不到对程序的 100% 的覆盖。所以,我们需要把分支(判定)和条件覆盖一起进行综合考虑。
代码如下:
public static int test(int a,int b,int c) {
1 int result = 0;
2 if(a == 0 or b > 2) {
3 result = b - a;
}
4 if(a > 0 and c > 0 ) {
5 result = c * a;
}
6 return result;
}
第 1 步:分析待测试代码,画出程序的流程图。上述代码的参考流程图如下图所示:
第 2 步:分析流程图。
通过分析流程图,我们可以知道:这段代码中有两个判断语句,分别是上图中标识为 ② 和 ③ 的语句,语句 ② 中有两个条件语句,分别为 a == 0 和 b > 2 ;语句 ③ 中也有两个条件,分别为 a > 0 和 c > 0 。为了使后续的分析过程更加清晰明了,我们先来梳理一下流程图中的判断语句及语句中的条件项,并进行相应的标识。
判断语句:
判断语句取值标识a == 0 or b > 2真T1a == 0 or b > 2假F1a > 0 and c > 0真T2a > 0 and c > 0假F2
条件:
条件取值标识a == 0真Y1a == 0假N1b > 2真Y2b > 2假N2a > 0真Y3a > 0假N3c > 0真Y4c > 0假N4
第 3 步:使用分支-条件覆盖法编写测试用例。
根据分支-条件覆盖法的定义,这种方法是将分支覆盖和条件覆盖结合起来,设计一些测试用例,使程序中每个判定语句中的每个条件为真和为假的情况都至少被执行一次,并且每个判断语句本身为真、为假的情况也至少被执行一次。因此,我们可以设计如下表中的测试用例来对程序中的两个判断语句及其四个条件进行覆盖:
测试用例编号输入数据预期结果条件覆盖情况分支覆盖情况testcase_01a = 0 , b = 5 , c = 0result = 5Y1、Y2、N3、N4T1、F2testcase_02a = 5 , b = 1 , c = 3result = 15N1、N2、Y3、Y4F1、T2
通过执行以上两个用例可以实现程序的分支-条件 100% 覆盖。那么,是不是实现了分支-条件覆盖就代表程序已经被覆盖全面了呢?下图是该程序标识了执行路径的流程图:
** 总结**:分支-条件覆盖可以使程序中的判断语句以及判断语句中的条件的真、假分支都得到覆盖,但是分支-条件覆盖达到 100% 仍然强度不够,程序中的某些逻辑运算等错误仍然可能不会被发现。
5、条件组合覆盖
条件组合覆盖又称为多条件覆盖,是指设计足够数据的测试用例,使每个判定语句中的所有判定条件的各种可能的组合都至少被执行一次。
代码如下:
public static int test(int a,int b,int c) {
1 int result = 0;
2 if(a == 0 or b > 2) {
3 result = b - a;
}
4 if(a > 0 and c > 0) {
5 result = c * a;
}
6 return result;
}
第 1 步:分析待测试代码,画出程序的流程图。上述代码的参考流程图如下图所示:
标识了路径的流程图如下:
第 2 步:分析流程图。
由流程图可知:这段代码中有两个判断语句,分别是上图中标识为 ② 和 ③ 的语句,语句 ② 中有两个条件语句,分别为 a == 0 和 b > 2 ;语句 ③ 中也有两个条件,分别为 a > 0 和 c > 0 。为了使后续的分析过程更加清晰明了,我们先将条件语句进行相应的标识,具体如下:
条件取值标识a == 0真Y1a == 0假N1b > 2真Y2b > 2假N2a > 0真Y3a > 0假N3c > 0真Y4c > 0假N4
根据条件组合覆盖法的定义,我们需要设计一些测试用例分别使判断语句 ② 中两个条件的四种组合及判断语句 ③ 中两个条件的四种组合都至少执行一次,如下表所示:
组合编号判断语句条件组合1a == 0 or b > 2Y1 + Y22a == 0 or b > 2Y1 + N23a == 0 or b > 2N1 + Y24a == 0 or b > 2N1 + N25a > 0 and c > 0Y3 + Y46a > 0 and c > 0Y3 + N47a > 0 and c > 0N3 + Y48a > 0 and c > 0N3 + N4
第 3 步:使用条件组合覆盖法编写测试用例。
分别设计测试用例覆盖上表中的各种组合,具体如下:
测试用例编号输入数据预期结果条件组合覆盖条件覆盖路径覆盖testcase_01a = 0 , b = 3 , c = 1result = 3组合 1 、组合 7Y1、Y2、N3、Y4B - Ctestcase_02a = 0 , b = 1 , c = 0result = 1组合 2 、组合 8Y1、N2、N3、N4B - Ctestcase_03a = 1 , b = 5 , c = 3result = 3组合 3 、组合 5N1、Y2、Y3、Y4B - Dtestcase_04a = 2 , b = 0 , c = -1result = 1组合 4 、组合 6N1、N2、Y3、N4A - C
通过对上表的分析我们可以发现 :条件组合覆盖可以使程序判断断语句中的条件组合都至少被执行一次,但是,满足了条件组合覆盖也不能保证所有的路径都已经得到覆盖,在本例中,四个测试用例实现了条件组合的 100% 覆盖,但是仍有路径 A - D 未被覆盖到。如果要将路径全部覆盖,需要再增加一个覆盖路径 A - D 的测试用例,即使用条件组合 + 分支覆盖的全部测试用例如下:
测试用例编号输入数据预期结果条件组合覆盖条件覆盖路径覆盖testcase_01a = 0 , b = 3 , c = 1result = 3组合 1 、组合 7Y1、Y2、N3、Y4B - Ctestcase_02a = 0 , b = 1 , c = 0result = 1组合 2 、组合 8Y1、N2、N3、N4B - Ctestcase_03a = 1 , b = 5 , c = 3result = 3组合 3 、组合 5N1、Y2、Y3、Y4B - Dtestcase_04a = 2 , b = 0 , c = -1result = 0组合 4 、组合 6N1、N2、Y3、N4A - Ctestcase_05a = 2 , b = 0 , c = 5result = 10组合 4 、组合 5N1、N2、Y3、Y4A - D
总结:条件组合覆盖可以使程序所有判断语句中的条件组合都被覆盖,但是仍然不能保证覆盖所有路径,需要再补充用例进行路径覆盖。另外,如果程序中的条件和分支比较多,需要设计的测试用例数量会很庞大。
6、基本路径覆盖
路径覆盖可以使程序中的路径都被测试到,但是,要对程序中的路径做到完全覆盖经常是无法实现的。为了解决这一难题,我们需要在保证测试质量的前提下把测试的路径数量压缩到一定的范围内,基础路径覆盖法正好可以解决该问题。
代码如下:
public static int test(int a,int b,int c) {
1 int result = 0;
2 if(a == 0 or b > 2) {
3 result = b - a;
}
4 if(a > 0 and c > 0 ) {
5 result = c * a;
}
6 return result;
}
使用基本路径覆盖法设计白盒测试用例的具体步骤如下:
第 1 步:分析待测试代码,画出程序的流程图。参考流程图如下图所示,如果对该方法比较熟练或对程序流程比较清晰,可省略这个步骤。
在第二章的“ 2.2.2 控制流分析”小节中曾介绍过“在控制流图中如果含有复合条件,需要改为单条件嵌套的形式”,为了后续的讲述更加清晰,这里先把上述流程图中的复合条件按控制流图的要求进行拆分,具体如下:
第 2 步:根据流程图画出控制流图。
控制流图的画法在第二章的“ 2.2.2 控制流分析”小节中已详细介绍,此处不再详细介绍,只进行简单的回顾。在控制流图中,圆形符号○称为“节点”,表示一个基本的代码块;包含条件的节点称为“判断节点”;箭头称为“边”,表示控制流路径,反向边则表示可能存在循环。按照控制流图的规则,上述流程图可以画成下面的控制流图:
第 3 步:计算圈复杂度。
圈复杂度 V(G) 有三种计算方法,在第二章的“复杂度分析”小节已详细介绍过,读者可以任选其中一种方法来进行计算。下面简单介绍一下用这三种方法计算本例的圈复杂度:
方法一:V(G) = A + 1,其中 A 代表控制流图中的封闭区域数量。从下图可以看出,程序的控制流图中共有 4 个封闭区域,所以,圈复杂度 V(G) = 4 + 1 = 5 。
方法二:V(G) = P + 1,其中 P 代表控制流图中的判定节点数。从下图可以看出,程序的控制流图中共有 4 个判定节点,所以,圈复杂度 V(G) = 4 + 1 = 5 。
方法三:V(G) = e - n + 2,其中 e 代表控制流图中的边的数量,即控制流中的箭头数量;n 代表控制流图的节点数量,即控制流图中的圆圈数量。从下图中可以看出,程序的控制流图中有 11 条边(11个箭头),8个节点(8个圆圈),所以,圈复杂度 V(G) = 11 - 8 + 2 = 5 。
第 4 步:确定基本路径的集合。
基本路径又称为独立路径,是指至少包含一条其他独立路径中未包含的路径。例如,在上图中,路径 1 - 2 - 3 - 5 - 8 是一条基本路径,1 - 2 - 4 - 3 - 5 - 8 则可以看成了另外一条基本路径,因为这条路径中经过 4 节点的路径在第一条基本路径中未包含。
圈复杂度是指程序中的独立路径数量,是确保程序中每个可执行语句至少执行一次需要的测试用例数量的最小值。根据第 3 步的计算结果,本例中我们需要确定 5 条基本路径,具体如下:
路径 1 :1 - 2 - 3 - 5 - 8
路径 2 :1 - 2 - 4 - 3 - 5 - 8
路径 3 :1 - 2 - 4 - 5 - 8
路径 4 :1 - 2 - 4 - 5 - 6 - 8
路径 5 :1 - 2 - 4 - 5 - 6 - 7 - 8
第 5 步:根据基本路径编写测试用例。
根据基本路径覆盖法的定义,我们需要设计测试用例分别覆盖第 4 步中的 5 条基本路径,即设计合理的输入数据使程序运行时经过指定的路径。因此,我们可以设计如下表中的 5 个测试用例来覆盖这 5 条基本路径。
测试用例编号输入数据预期结果路径基本覆盖情况testcase_01a = 0 , b = 1 , c = 9result = 1路径 1testcase_02a = 0 , b = 3 , c = 5result = 3路径 2testcase_03a = -2 , b = 1 , c = 3result = 0路径 3testcase_04a = 1 , b = 0 , c = -1result = 0路径 4testcase_05a = 5 , b = -3 , c = 2result = 10路径 5
** 总结:**使用基本路径覆盖法设计用例进行测试时,可以使程序中的每条独立路径都至少执行一次。如果程序中的基本路径达到了 100% 覆盖,则分支(判定)覆盖、条件覆盖也能达到 100% 覆盖。如果使用基本路径覆盖法后程序中仍有未覆盖到的路径,可使用逻辑覆盖法补充测试用例保证覆盖全面。
7、循环语句覆盖--简单循环
循环是反复运行同一段代码的语法结构,是代码中常见的一种结构。在白盒测试中,循环结构的测试也是我们需要掌握的内容。循环结构测试主要的侧重点是验证循环结构的有效性,一般可以结合条件覆盖、基本路径覆盖以及黑盒测试方法中的等价类、边界值等方法来设计测试用例。
简单循环是最简单的循环,即只有一个循环且没有嵌套,例如,一个 while 循环、一个do-while 循环、一个 for 循环。下图是两种简单循环的示意图:
代码如下:
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 的值)012591011
转化为测试用例,如下表所示:
测试用例编号输入预期输出testcase_010result=0,输出:请输入110的整数!testcase_021result=1,输出:1的阶乘是1testcase_032result=2,输出:2的阶乘是2testcase_045result=120,输出:5的阶乘是120testcase_059result=362880,输出:9的阶乘是362880testcase_0610result=3628800,输出:10的阶乘是3628800testcase_0711result=0,输出:请输入110的整数!
第 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");
}
}
总结:简单循环的测试重点是验证循环结构的有效性,主要考虑循环的边界和运行界限执行循环体的情况。对于最多为 n 次的简单循环,一般需要设计跳过循环、循环 1 次、2 次,m 次(2<m<n-1)、n - 1 次、n 次、n + 1 次的测试用例,重点测试循环变量的初值、最大值、增量以及退出循环的情况。如果循环的最大循环次数不确定,一般设计跳过循环、循环 1 次、2 次,m 次的测试用例即可。
8、循环语句覆盖---嵌套循环
嵌套循环是指一个循环语句的循环体内含有其他的循环语句的语法结构,while、for 等循环语句都可以进行嵌套。最常见的嵌套循环是 for 循环中嵌套 for 循环。嵌套循环执行时,外层循环每执行一次内层循环会执行多次,循环的总次数等于外层循环次数与内层循环次数的积。下面是一个嵌套循环的示意图:
代码如下:
//冒泡排序
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;
}
}
** 总结**:嵌套循环结构的测试侧重点与简单循环相同,都是侧重于验证循环的有效性,不同点在于嵌套循环结构设计测试用例时需考虑用尽可能少的用例覆盖更全面的场景,降低测试成本,提高测试效率。
9、循环语句覆盖---串接循环
串接循环是指两个或多个循环连接在一起的循环结构,也称连锁循环。串接循环分为两种:第一种是各个循环体彼此独立、相互之间没有关联关系,这种循环我们可以使用简单循环的方法,依次对每个独立的循环体进行测试;第二种串接循环是各个循环体之间有关联关系,第二个循环的输入来自于第一个循环的输出,对于这种串接循环,我们可以考虑使用嵌套循环的测试方法来进行测试。
下图是串接循环的示意图:
代码如下:
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 * 3testcase_03{75,11}3 * 5 * 5testcase_04{20,6,90,21,45,76}2 * 3 * 3 * 5testcase_05{1}空testcase_06{2,1}2testcase_07{4,1,3,2}2 * 2testcase_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];
}
}
修改代码后再次执行测试用例进行回归测试的结果如下:
总结:各个循环体彼此独立的串接循环可以使用简单循环的方法单独测试每一个循环,循环体之间有关联关系的串接循环则可以考虑使用嵌套循环的测试方法来进行测试。
版权归原作者 没工作的小马 所有, 如有侵权,请联系我们删除。