- 互联网单元测试及实践
- 陈卫俊等编著
- 3106字
- 2020-08-27 09:41:25
2.2 单元测试用例设计技术
在很多人的理解中,单元测试等同于白盒测试。其实这两者并不是同一种概念。单元测试属于测试过程的一个阶段,这个阶段的特点就是对软件进行细粒度、单元级的测试。而白盒测试和黑盒测试属于测试技术。白盒测试需要分析测试单元内部结构;黑盒测试不需要了解被测单元的内部实现,只需要按照接口和功能描述进行测试。此外还有一些文章会提到介于白盒测试、黑盒测试之间的灰盒测试。单元测试、集成测试、系统测试等测试活动中,白盒测试和黑盒测试有着不同的应用场景,对应着不同的测试用例设计策略。
单元测试用例应该包含4个关键元素:被测单元模块初始状态声明,即测试用例的开始状态(仅适用于被测单元维持了调用间状态的情况);被测单元的输入,包含由被测单元读入的任何外部数据值;该测试用例实际测试的代码,用被测单元的功能和测试用例设计中使用的分析来说明,比如,单元中哪一个决策条件被测试;测试用例的期望输出结果,测试用例的期望输出结果总是应该在测试进行之前在测试说明中定义。
单元测试主要采用面向白盒测试设计方法。白盒测试用例的设计手段包括静态分析和动态分析两大类。静态分析是一种不通过执行程序而进行测试的技术,主要是检查软件的表示和描述是否一致,没有冲突或者没有歧义。动态分析是根据程序的控制结构设计测试用例,需要保证每个模块的所有独立路径至少被使用一次,对所有的逻辑值均测试true和false、上下边界,在可操作范围内运行所有循环,以及检查内部数据结构以确保其有效性。
单元测试又不仅仅是采用白盒测试设计方法,其主导思想是:使用一种或多种白盒测试方法分析模块的逻辑结构,然后使用黑盒测试方法对照模块的规格说明以补充测试用例。《软件测试的艺术》一书中认为:在使用白盒测试方法前,需要列举出程序中所有的条件判断;而在使用白盒测试方法时,应在开始就使用逻辑覆盖的方法;在使用黑盒测试方法时,最好要使用边界值分析的方法,且不要依据边界值分析的结果来重写白盒测试的测试用例,最好黑盒测试的用例再单独写出来进行补充,不改动之前已经确认过的白盒测试的测试用例。
2.2.1 逻辑覆盖
测试覆盖率是白盒测试衡量测试用例设计完备性的关键指标,也是指导测试用例设计的原则。测试覆盖率的重点是覆盖程序的逻辑结构。基本的逻辑覆盖率度量方法,有语句覆盖、判定覆盖、条件覆盖、路径覆盖、分支条件组合覆盖等等。
语句覆盖:根据可执行语句是否被测试执行来度量覆盖率,又称行覆盖、段覆盖或基本块覆盖。语句覆盖容易遗漏分支,是最弱的一种覆盖,请见下例。
代码2.1 语句覆盖示例
1 String str = null; 2 if (condition) { 3 str = "true"; 4 } 5 str = "123";
这段代码有两个分支,当condition为true时执行第3行、第5行的语句;当condition为false时只执行第5行的语句。显然在condition为true时,语句覆盖已经达到100%。这样condition为false的分支就被遗漏。
判定覆盖:根据布尔表达式的取值true和false度量覆盖率,又称分支覆盖、基本路径覆盖等。这种覆盖简单但是强于语句覆盖,缺点在于这种方式忽略了布尔表达式内部的分支,请见下例。
代码2.2 判定覆盖示例
1 if (condition1 && (condition2 || function1())) 2 statement1; 3 else 4 statement2;
当condition1为true、condition2为true时,布尔表达式为true,走第一个分支(第2行的语句);当condition为false时,布尔表达式为false,走第二个分支(第4行语句)。这样分支覆盖率达到100%,但是显然没有覆盖到condition2为false以及function1的取值。
条件覆盖:根据每个表达式的值来度量覆盖率。这种方法要覆盖判定中每个子表达式的取值,但是完全的条件覆盖并不能保证完全的判定覆盖,请见下例。
代码2.3 条件覆盖示例
1 bool f(bool e) { return false; } 2 bool a[2] = {false,false}; 3 if (f(a && b)) { statement1 } 4 if (a[int(a && b)]) { statement2 } 5 if ((a && b) ? false : false) { statement3 }
不管a、b取值如何,三个布尔表达式均是false,也就是说条件覆盖达到100%,但是判定覆盖只有50%。
条件组合覆盖:这种覆盖结合了条件覆盖(condition coverage)和分支覆盖(decision coverage)的技术。它有两者的简单性但是没有两者的缺点,是比较强的覆盖标准。它是指设计足够的测试用例,使得每个判定表达式中条件的各种可能的值的组合都至少出现一次。缺点是未考虑组合条件的组合情况。
路径覆盖:根据函数的每条可能路径来度量覆盖率,又称断言覆盖。每条路径对应函数内的一个分支组合。这种覆盖是一种彻底的测试覆盖方法。但是路径覆盖的组合是以分支的指数级别增加的,如果一个函数有3个if语句,就有8条路径需要测试。另外许多路径其实是不可能走到的,请见下例。
代码2.4 路径覆盖示例
1 if (success) 2 statement1; 3 if (success) 4 statement2;
这段代码共有4条路径,但是由于两个if语句的判定条件相同,其实只有两条路径。
除了以上介绍的覆盖率度量方法,还有多条件覆盖、修正条件/判定覆盖等其他方法。需要指出的是任何逻辑覆盖准则尚不足以胜任作为生成单元测试用例的唯一手段。在实际的逻辑覆盖测试中,一般以条件组合覆盖为主设计测试用例,然后再补充部分用例,以达到路径覆盖测试标准。
2.2.2 等价类划分
等价类划分是测试用例设计非常形式化的方法,它把所有可能的输入数据,即程序的输入域划分成若干部分(子集),然后从每一个子集中选取少数具有代表性的数据作为测试用例。该方法是一种重要的、常用的黑盒测试用例设计方法。
等价类 指某个输入域的子集合。
在每一个等价类中取一个数据作为测试的输入条件,这样就可以用少量代表性的测试数据代表整个域。等价类划分可有两种不同的情况:有效等价类和无效等价类。有效等价类指合理的,有意义的输入数据构成的集合,利用有效等价类可检验程序是否实现了规定的功能和性能;反之则是无效等价类。
代码2.5 等价类示例代码
1 public void partition(int i) { 2 if (i > 0 && i < 99) { 3 i = 1; 4 } 5 }
在代码2.5中我们可以设计出这样三个等价类:
1.有效等价类:i∈(0, 99)
2.无效等价类:i∈(-∞, 0) 和i∈[99, +∞]
设计测试用例时,要同时考虑这两种等价类。因为,软件不仅要能接收合理的数据,也要能经受意外的考验。这样的测试才能确保软件具有更高的可靠性。
划分等价类有6条原则。
1.在输入条件规定了取值范围或值的个数的情况下,则可以确立一个有效等价类和两个无效等价类。
2.在输入条件规定了输入值的集合或者规定了“必须如何”的条件的情况下,可确立一个有效等价类和一个无效等价类。
3.在输入条件是一个布尔量的情况下,可确定一个有效等价类和一个无效等价类。
4.在规定了输入数据的一组值(假定为n个),并且程序要对每一个输入值分别处理的情况下,可确立n个有效等价类和一个无效等价类。
5.在规定了输入数据必须遵守的规则的情况下,可确立一个有效等价类(符合规则)和若干个无效等价类(从不同角度违反规则)。
6.如果确知已划分的等价类中各元素在程序处理中的方式不同,则应再将该等价类进一步地划分为更小的等价类。
2.2.3 边界条件
有测试经验的人都知道,软件经常在边界上失效,采用边界值分析技术,针对边界值及其左、右设计测试用例,很有可能发现软件缺陷。边界值分析使用与等价区间划分相似的分析方法,不同点在于边界值分析更加侧重于区间的边界。因此,这种方法也经常被看做是等价区间的补充。以代码2.5为例,0和99就是两个边界。
边界值分析将一定程度的负面测试加入到测试设计中,期望错误会在区间边界发生,对边界值的两边都需设计测试用例。一般的选择边界用例的原则是:
1.如果输入条件规定了值的范围,则应取刚达到这个范围的边界的值,以及刚刚超越这个范围边界的值作为测试输入数据。
2.如果输入条件规定了值的个数,则选择最大个数、最小个数、比最小个数少1,比最大个数多1的数。
3.对每个输出条件,应用边界用例设计原则1。
4.对每个输出条件,应用边界用例设计原则2。
5.如果输入域或输出域是有序集合,则应选取集合的第一个元素和最后一个元素作为测试用例。
6.如果程序中使用了一个内部数据结构,则应当选择这个内部数据结构的边界上的值作为测试用例。
7.找出其他可能的边界条件。