本文描述了三种单元测试的组织方法:自上而下法,自下而上法和分离法。组织方法是制定
单元测试策略和拟制测试计划的关键因素;选择不适当的方法会对单元测试成本和软件维护
开支造成不利影响。这里将推荐一种基于分离法的单元测试策略。
一、介绍
单元测试是对软件单个组件(单元)进行的测试。尽管它的编码是不同类型的,而且有两个
不同的阶段,但单元测试通常被认为是一种组合代码以及软件生命周期单元测试阶段的一部
分。在Ada,C 和C++程序中最基本的设计和代码单元是单个的子程序(如过程,函数,成员
函数)。Ada 和C++提供将基本的单元群组成包(这里指Ada 语言)和复合类(C++语言)的
功能。而针对Ada 和C++程序的单元测试就是要测试语境中所包含的包和类。当设计一个单
元测试的策略时,可以采用三种基本的组织方法。它们分别是自上而下法、自下而上法和分
离法。在接下来的第二、第三和第四部分将对上述三种方法的详细内容、各自的优点和缺点
分别进行介绍。在文章中要一直用到测试驱动和桩模块这两个概念。所谓的测试驱动是指能
使软件执行的软件,它的目的就是为了测试软件,提供一个能设置输入参数的框架,并执行
这个框架单元以得到相应的输出参数。而桩模块是指一个模拟单元,用这个模拟单元来替代
真实的单元完成测试。
二、自上而下的测试
1. 详述
在自上而下的测试过程中,每个单元是通过使用它们来进行测试的,这个过程是由调用这些
被测单元的其他独立的单元完成的。首先测试最高层的单元,将所有的调用单元用桩模块替
换。接着用实际的调用单元替换桩模块,而继续将较低层次的单元用桩模块替换。重复这个
过程直到测试了最底层的单元。自上而下测试法需要测试桩,而不需要测试驱动。图2.1 描
述了使用测试桩和一些已测试单元来测试单元D 的过程,假设单元A,B,C 已经用自上而下
法进行了测试。由图2.1 得到的是一个使用基于自上而下组织方法的单元测试计划,其过程
可以描述如下:
步骤1:测试A 单元,使用B,C,D 单元的桩模块。
步骤2:测试B 单元,通过已测试过的A 单元来调用它,并且使用C,D 单元的桩模块。
步骤3:测试C 单元,通过已测试过的A 单元来调用它,并且使用已通过测试的B 单元和D 单
元的桩模块。
步骤4:测试D 单元,从已测试过的A 单元调用它,使用已测试过的B 和C单元,并且将E,F
和G 单元用桩模块代替。(如图2.1 所示)
步骤5:测试E 单元,通过已测试过的D 单元调用它,而D 单元是由已通过测试的A 单元来调
用的,使用已通过测试的B 和C 单元,并且将F,G,H,I和J 单元用桩模块代替。
步骤6:测试F 单元,通过已测试过的D 单元调用它,而D 单元是由已通过测试的A 单元来调
用的,使用已通过测试的B,C 和E 单元,并且将G,H,I和J 单元用桩模块代替。
步骤7:测试G 单元,通过已测试过的D 单元调用它,而D 单元是由已通过测试的A 单元来
调用的,使用已通过测试的B,C 和F 单元,并且将H,I 和J 单元用桩模块代替。
步骤8:测试H 单元,通过已测试过的E 单元调用它,而E 单元是由已通过测试的D 单元来调
用的,而D 单元是由已通过测试的A 单元来调用的,使用已通过测试的B,C,E,F,G 和H
单元,并且将J 单元用桩模块代替。
步骤9:测试J 单元,通过已测试过的E 单元调用它,而E 单元是由已通过测试的D 单元来调
用的,而D 单元是由已通过测试的A 单元来调用的,使用已通过测试的B,C,E,F,G,H
和I 单元。
图2.1 自上而下法
2. 优点
自上而下单元测试法提供了一种软件集成阶段之前的较早的单元集成方法。实际上,自上而
下单元测试法确实将单元测试和软件集成策略进行了组合。单元的详细设计是自上而下的,
自上而下的测试实现过程使得被测单元按照原设计的顺序进行,因为单元测试的详细设计与
软件生命周期代码设计阶段的重叠,所以开发时间将被缩短。在通常的结构化设计中,高等
级的单元提供高层的功能,而低等级的单元实现细节,自上而下的单元测试将提供一种早期
的“可见”的功能化集成。它给予单元测试一种必要的合理的实现途径。较低层次的多余功能
可以通过自上而下法来鉴别,这是因为没有路径来测试它。(但是,这可能在区分多余的功
能和没有被测试的功能时带来困难)。
3. 缺点
自上而下法是通过桩模块来进行控制的,而且测试用例常常涉及很多的桩模块。对于每个已
测单元来说,测试变得越来越复杂,结果是开发和维护的费用也越来越昂贵。依层次进行的
自上而下的测试,要达到一个好的覆盖结构也很困难,而这对于一个较为完善、安全的关键
性应用来说至为重要,同时这也是很多的标准所要求的。难于达到一个好的覆盖结构也可能
导致最终的多余功能和未测试功能之间的混乱。由此,测试一些低层次的功能,特别是错误
处理代码,将彻底不切实际。
一个单元的变化往往会影响对其兄弟单元和下层单元的测试。例如,考虑一下D 单元一个变
化。很明显,对D 单元的单元测试不得不发生变化和重新进行。另外,要使用已测试单元D
的E、F、G、H、I 和J 单元也不得不重新测试。作为单元D 改变的结果,上述测试自身可能
也不得不发生改变,即使单元E、F、G、H、I 和J 实际上并没有改变。这将导致当变化发生
时,重复测试带来的高成本,以及高额的维护成本和高额的整个软件生产周期的成本。
在为自上而下测试法设计测试用例当中,当被测单元调用其他单元时需要测试人员具备结构
化知识。被测试单元的顺序受限于单元的层次结构,低层次的单元必须要等到高层次的单元
被测试后才能被测试,这样就形成了一个“又长又瘦”的单元测试阶段。(然而,这可能会导
致测试详细设计与软件生命周期编码阶段的整体重叠。)如图2.1 所示的例子程序中各个单
元之间的层次关系十分简单,在实际的编程过程中可能会遇到类似的情形,而且各个单元之
间的层次关系会更复杂。所以自上而下测试法的缺点对单元测试造成的不利影响会随着被测
单元之间复杂的联系而加深。
4. 总结
一个自上而下的测试策略成本将高于基于分离的测试策略,这取决于顶层单元下层单元的复
杂程度,以及由于下层单元自身发生变化所带来的显著影响。对于单元测试来说自上而下的
组织方法不是一个好的选择。然而,当各个组成单元已经被单独测试的情况下,用自上而下
法进行单元的集成测试是个不错的手段。
三、自下而上法
1. 详述
在自下而上的单元测试中,被测单元与调用被测单元的单元是分开测试的,但是测试时所使
用的是真实的被调用单元。测试时最底层的单元首先被测试,这样就方便了对高层次单元的
测试。然后使用前面已经被测试过的被调用单元来测试其他的单元。重复这个过程直到最高
层的单元被测试为止。自下而上法需要测试驱动,但是不需要测试桩。图3.1 说明了测试D
单元时需要的测试驱动和已测单元的情况,假设单元E、F、G、H、I 和J 已经通过自下而上
法进行了测试。
图3.1 自下而上测试法
图3.1 显示了一个程序的单元测试的测试计划,该计划使用了基于自下而上的组织方法,其
过程如下:
步骤(1)
(注意在测试步骤中测试的顺序不是最主要的,步骤1 中的所有测试可以同
步进行)测试单元H,在调用H 单元的E 单元处使用一个测试驱动;测试单元I,在调用I 单
元的E 单元处使用一个测试驱动;
测试单元J,在调用J 单元的E 单元处使用一个测试驱动;
测试单元F,在调用F 单元的D 单元处使用一个测试驱动;
测试单元G,在调用G 单元的D 单元处使用一个测试驱动;
测试单元B,在调用B 单元的A 单元处使用一个测试驱动;
测试单元C,在调用C 单元的A 单元处使用一个测试驱动;
步骤(2)
测试单元E,在调用E 单元的D 单元处使用一个测试驱动,再加上已测试过的单元H、I 和J。
步骤(3)
测试单元D,在调用D 单元的A 单元处使用一个测试驱动,再加上已测试过的单元E、F、G
、H、I 和J。(如图3.1 所示)
步骤(4)
测试单元A,使用已测试过的单元B、C、D、E、F、G、H、I 和J。
2. 优点
和自上而下法一样,自下而上单元测试法提供了一种比软件集成阶段更早的单元集成。自下
而上单元测试同样也是真正意义上的单元测试和软件集成策略的结合。因为不需要测试桩,
所以所有的测试用例都由测试驱动控制。这样就使得低层次单元附近的单元测试相对简单些
。(但是,高层次单元的测试可能会变得很复杂。)在使用自下而上法测试时,测试用例的
编写可能只需要功能性的设计信息,不需要结构化的设计信息(尽管结构化设计信息可能有
利于实现测试的全覆盖)。所以当详细的设计文档缺乏结构化的细节时,自下而上的单元测
试就变得十分有用处。自下而上单元测试法提供了一种低层次功能性的集成,而较高层次的
功能随着单元测试过程的进行按照单元层次关系逐层增加。这就使得自下而上单元测试很容
易地与测试对象相兼容。
3. 缺点
随着测试逐层推进,自下而上单元测试变得越来越复杂,随之而来的是开发和维护的成本越
来越高昂,同样要实现好的结构覆盖也变得越来越困难。低层单元的变化经常影响其上层单
元的测试。例如:想象一下H 单元发生变化的情况。很明显,对H 单元的测试不得不发生变
化和重新进行。另外,对于A、D 和E 单元的测试来说,因为它们共同使用了已测试过的H
单元,所以它们的测试也不得不重做。作为H 单元发生变化的后果,这些测试本身可能也要
进行改变,即使单元A、D 和E 实际上并没有发生变化。这就导致了当变化发生时,产生了
与重新测试有关的高额代价,以及高额的维护成本和整个软件生命周期成本的提高。单元测
试的顺序取决于单元的层次关系,较高层次的单元必须要等到较低层次单元通过测试后才能
进行测试,所以就形成了“长瘦”型的单元测试阶段。最先被测试的单元是最后被设计的单元
,所以单元测试不能与软件生命周期的详细设计阶段重叠。如图2.2 所示的例子程序中各个
单元之间的层次关系十分简单,在实际的编程过程中可能会遇到类似的情形,而且各个单元
之间的层次关系会更复杂。与自上而下测试法一样,自下而上测试法的缺点会随着被测单元
之间复杂的联系而放大。
4. 总结
自下而上组织法对于单元测试来说是个比较好的手段,特别是当测试对象和重用情况时。然
而,自下而上方法偏向于功能性测试,而不是结构化测试。对于很多标准所需要的高集成度
和安全的关键性应用,需要达到高层次的结构覆盖,但自下而上法很难满足这个要求。自下
而上单元测试法与很多软件开发所要求的紧凑的时间计划是相冲突的。总的来说,一个自下
而上策略成本将高于基于分离的测试策略,这是因为单元层次结构中低层次单元以上单元的
复杂程度和它们发生变化所带来的显著影响。
四、分离法
1. 详述
分离测试法是分开测试每一个单元,无论是被调用单元还是调用单元。被测单元可以按照任
意顺序进行测试,因为被测单元不需要其他任何已测单元的支持。每一个单元的测试都需要
一个测试驱动,并且所有的被调用单元都要用测试桩代替。图4.1 说明了测试单元D 时需要
的测试驱动和测试桩的情况。
图4.1 分离测试法
图4.1 显示了某个程序中一个单元的测试计划,该计划基于分离组织方法的策略,只需要如
下所示的一步:
步骤(1)
(注意该测试计划只有一步。测试的顺序不是最主要的,所有的测试可以同步进行。)
测试A 单元,使用一个测试驱动启动测试,并且将B、C 和D 单元换成测试桩;
测试B 单元,在A 单元处使用一个测试驱动来调用B 单元;
测试C 单元,在A 单元处使用一个测试驱动来调用C 单元;
测试D 单元,在A 单元处使用一个测试驱动来调用D 单元,并且将E、F和G 单元换成测试桩
(如图3.1 所示);
测试E 单元,在D 单元处使用一个测试驱动来调用E 单元,并且将H、I和J 单元换成测试桩
;
测试F 单元,在D 单元处使用一个测试驱动来调用F 单元;
测试G 单元,在D 单元处使用一个测试驱动来调用G 单元;
测试H 单元,在E 单元处使用一个测试驱动来调用H 单元;
测试I 单元,在E 单元处使用一个测试驱动来调用I 单元;
测试J 单元,在E 单元处使用一个测试驱动来调用J 单元。
2. 优点
彻底地测试一个分离的单元是很容易做到的,单元测试将其从与其它单元之间复杂的关系中
分离了出来。分离测试是最容易实现良好的结构性覆盖的方法,并且实现良好结构性覆盖的
困难程度与确定某一个单元在单元层次中所处位置的难易度没有什么不同。
因为每一次只测试一个单元,所以该方法中所使用的测试驱动比自下而上法中所使用的测试
驱动简单,该方法中所使用的测试桩比自上而下法中使用的测试桩简单。由于采用了分离的
方法进行单元测试,被测单元之间没有依赖关系,所以单元测试阶段可以和详细设计阶段,
以及软件生命周期的代码编写阶段重叠。所有单元都能同步测试,形成了单元测试阶段“短
而宽”的特点。这有利于通过扩大团队规模的手段缩短整个软件开发的时间。分离测试法另
外一个优点是去除了测试单元之间的内部依赖关系,所以当一个单元发生变化时只需要改变
那个发生变化的测试单元,而对其它测试单元没有任何影响。由此可以看出分离组织法的成
本要低于自下而上组织法和自上而下组织法,特别是当发生变化时其效果更加明显。分离法
提供了一种与集成测试不同的单元测试分离手段,它允许开发人员在软件生命周期的单元测
试阶段专心致力于单元测试工作,而在软件生命周期的集成测试阶段专心致力于集成测试工
作。只有分离法是纯粹意义上适用于单元测试的方法,自上而下测试法和自下而上测试法适
用于单元测试和集成阶段的混合过程。与自上而下法和自下而上法不同的是,用分离法进行
的单元测试,被测单元不会受到与其关联的其它任何单元的影响。
3. 缺点
用分离法进行单元测试最主要的缺点是它不能提供一个早期的单元集成。这必须要等到软件
生命周期的集成阶段才能做到。(这很难说是一个真正的缺点)用分离法进行单元测试时需
要结构设计信息和使用测试桩、测试驱动。这会导致在测试靠近底层的单元时,所花费成本
要高于自下而上法。然而,这个缺陷可以通过简化层次较高的单元的测试,以及每个单元每
次发生变化时的较低花费得到补偿。
4. 总结
用分离法进行单元测试是最合适的选择。在加上适当的集成策略作为补充,将会缩短软件开
发时间所占比例和降低开发费用,这个优势将会贯穿整个软件开发过程和软件生命周期。按
照分离法进行单元测试时,被测单元可以按照自上而下或者自下而上的顺序进行集成,或者
集成为任何便利的群组和群组的结合。然而,一个自下而上的集成方式是与目前流行的面向
对象和面向对象的设计最相兼容的策略。分离法单元测试是实现高层次结构覆盖的最佳手段
,而高层次结构覆盖对于很多标准所要求的高完善性和安全的关键性应用来说是至关重要。
在通过单元测试完成了所有实现好的结构覆盖的困难工作的基础上,集成测试就可以集中于
全面的功能测试和单元交互的测试。
五、使用AdaTEST 和Cantata
一个单元的测试在整个软件生命周期中要重复进行很多次,无论是在开发阶段还是维护过程
中。一些测试工具如:AdaTEST 和Cantata,可以用于一些易于重复进行和花费较少的自动化
单元测试中,这样可以有效降低人为因素带来的风险。AdaTEST 和Cantata 测试脚本由一个
测试驱动和一个桩的集合(可选的)组成。AdaTEST 和Cantata 可以用于本文所介绍的任何
单元测试的组织方法,或者这些方法的任意组合,使得开发人员可以采用最适合于项目应用
的测试策略。IPL 提供了两篇相关论文,如下所示:
“Achieving Testability when using Ada Packaging and Data Hiding Methods”“Testing C++ Objects”
论文“Testing C++ Objects”同样详细讨论了在用自下而上法进行单元测试时,分离的类和层次
等级的约束是如何引发问题的。文章介绍了分离单元测试法是如何成为唯一实用的处理分离
的类和层次等级约束的途径。
1、结论
在实践中,将任何一种方法专门用于进行单元测试是不可能的。通常,分离单元测试法要通
过一些自下而上的测试加以修改,将被调用单元用测试桩和已测的实际单元的混合体来表示
。例如,直接使用一个数学函数更有实际意义,因为它已被测试并且不大可能发生改变。
一些建议的策略如下:
1、基于你的分离法的单元测试策略,继而自下而上的集成被测单元。
2、折中法,即自下而上的通过合并一些便于合并的单元,(例如:使用实际的操作符,数
学函数,字符串操作等。)但是要记住潜在的变化带来的影响。无论是进行单元测试,还是
随着所测单元发生变化时重新测试和维护,同时也为了满足软件的可靠性而促进彻底的测试
覆盖,这些都将导致成本的最低化。请记住,单元测试是指测试每一个单元,而集成测试是
指测试被测单元之间的交互关系。
延伸阅读
文章来源于领测软件测试网 https://www.ltesting.net/