单元测试经验总结
1. 概述
工厂在组装一台电视机之前,会对每个元件都进行测试,这,就是单元测试。其实我们每天都在做单元测试。你写了一个函数,除了极简单的外,总是要执行一下,看看功能是否正常,有时还要想办法输出些数据,如弹出信息窗口什么的,这,也是单元测试,我们把这种单元测试称为临时单元测试。只进行了临时单元测试的软件,针对代码的测试很不完整,代码覆盖率要超过70%都很困难,未覆盖的代码可能遗留大量的细小的错误,这些错误还会互相影响,当BUG暴露出来的时候难于调试,大幅度提高后期测试和维护成本,也降低了开发商的竞争力。可以说,进行充分的单元测试,是提高软件质量,降低开发成本的必由之路。对于程序员来说,如果养成了对自己写的代码进行单元测试的习惯,不但可以写出高质量的代码,而且还能提高编程水平。
要进行充分的单元测试,应专门编写测试代码,并与产品代码隔离。我们认为,比较简单的办法是为产品工程建立对应的测试工程,为每个类建立对应的测试类,为每个函数(很简单的除外)建立测试函数。首先就几个概念谈谈我们的看法。
一般认为,在结构化程序时代,单元测试所说的单元是指函数,在当今的面向对象时代,单元测试所说的单元是指类。以我们的实践来看,以类作为测试单位,复杂度高,可操作性较差,因此仍然主张以函数作为单元测试的测试单位,但可以用一个测试类来组织某个类的所有测试函数。单元测试不应过分强调面向对象,因为局部代码依然是结构化的。单元测试的工作量较大,简单实用高效才是硬道理。
有一种看法是,只测试类的接口(公有函数),不测试其他函数,从面向对象角度来看,确实有其道理,但是,测试的目的是找错并最终排错,因此,只要是包含错误的可能性较大的函数都要测试,跟函数是否私有没有关系。对于C++来说,可以用一种简单的方法区隔需测试的函数:简单的函数如数据读写函数的实现在头文件中编写(inline函数),所有在源文件编写实现的函数都要进行测试(构造函数和析构函数除外)。
2.什么时间开始测试
什么时候测试?单元测试越早越好,早到什么程度?XP开发理论讲究TDD,即测试驱动开发,先编写测试代码,再进行开发。在实际的工作中,可以不必过分强调先什么后什么,重要的是高效和感觉舒适。从我们的经验来看,先编写产品函数的框架,然后编写测试函数,针对产品函数的功能编写测试用例,然后编写产品函数的代码,每写一个功能点都运行测试,随时补充测试用例。所谓先编写产品函数的框架,是指先编写函数空的实现,有返回值的随便返回一个值,编译通过后再编写测试代码,这时,函数名、参数表、返回类型都应该确定下来了,所编写的测试代码以后需修改的可能性比较小。
3.谁来测试
由谁测试?单元测试与其他测试不同,单元测试可看作是编码工作的一部分,应该由程序员完成,也就是说,经过了单元测试的代码才是已完成的代码,提交产品代码时也要同时提交测试代码。测试部门可以作一定程度的审核。
4.关于桩代码
我们认为,单元测试应避免编写桩代码。桩代码就是用来代替某些代码的代码,例如,产品函数或测试函数调用了一个未编写的函数,可以编写桩函数来代替该被调用的函数,桩代码也用于实现测试隔离。采用由底向上的方式进行开发,底层的代码先开发并先测试,可以避免编写桩代码,这样做的好处有:减少了工作量;测试上层函数时,也是对下层函数的间接测试;当下层函数修改时,通过回归测试可以确认修改是否导致上层函数产生错误。
单元测试的基本策略
1.概述
当设计一个单元测试的策略时,可以采用三种基本的组织方法。它们分别是自上而下法、自下而上法和分离法。在接下来的第二、第三和第四部分将对上述三种方法的详细内容、各自的优点和缺点分别进行介绍。在文章中要一直用到测试驱动和桩模块这两个概念。所谓的测试驱动是指能使软件执行的软件,它的目的就是为了测试软件,提供一个能设置输入参数的框架,并执行这个框架单元以得到相应的输出参数。而桩模块是指一个vwin 单元,用这个模拟单元来替代真实的单元完成测试。
2. 自上而下法 2.1 详述
在自上而下的测试过程中,每个单元是通过使用它们来进行测试的,这个过程是由调用这些被测单元的其他独立的单元完成的。
首先测试最高层的单元,将所有的调用单元用桩模块替换。接着用实际的调用单元替换桩模块,而继续将较低层次的单元用桩模块替换。重复这个过程直到测试了最底层的单元。自上而下测试法需要测试桩,而不需要测试驱动。
图2.1描述了使用测试桩和一些已测试单元来测试单元D的过程,假设单元A,B,C已经用自上而下法进行了测试。
由图2.1得到的是一个使用基于自上而下组织方法的单元测试计划,其过程可以描述如下:
1) 步骤1:测试A单元,使用B,C,D单元的桩模块。
2) 步骤2:测试B单元,通过已测试过的A单元来调用它,并且使用C,D单元的桩
模块。步骤3:测试C单元,通过已测试过的A单元来调用它,并且使用已通过测试的B单元和D单元的桩模块。
3) 步骤4:测试D单元,从已测试过的A单元调用它,使用已测试过的B和C单元,
并且将E,F和G单元用桩模块代替。(如图2.1所示)
4) 步骤5:测试E单元,通过已测试过的D单元调用它,而D单元是由已通过测试
的A单元来调用的,使用已通过测试的B和C单元,并且将F,G,H,I和J单元用桩模块代替。
5) 步骤6:测试F单元,通过已测试过的D单元调用它,而D单元是由已通过测试
的A单元来调用的,使用已通过测试的B,C和E单元,并且将G,H,I和J单元用桩模块代替。
6) 步骤7:测试G单元,通过已测试过的D单元调用它,而D单元是由已通过测试
的A单元来调用的,使用已通过测试的B,C和F单元,并且将H,I和J单元用桩模块代替。
7) 步骤8:测试H单元,通过已测试过的E单元调用它,而E单元是由已通过测试的D单元来调用的,而D单元是由已通过测试的A单元来调用的,使用已通过测试的B,C,E,F,G和H单元,并且将J单元用桩模块代替。
8) 步骤9:测试J单元,通过已测试过的E单元调用它,而E单元是由已通过测试的
D单元来调用的,而D单元是由已通过测试的A单元来调用的,使用已通过测试的B,C,E,F,G,H和I单元
2.2 优点
自上而下单元测试法提供了一种软件集成阶段之前的较早的单元集成方法。实际上,自上而下单元测试法确实将单元测试和软件集成策略进行了组合。
单元的详细设计是自上而下的,自上而下的测试实现过程使得被测单元按照原设计的顺序进行,因为单元测试的详细设计与软件生命周期代码设计阶段的重叠,所以开发时间将被缩短。
在通常的结构化设计中,高等级的单元提供高层的功能,而低等级的单元实现细节,自上而下的单元测试将提供一种早期的“可见”的功能化集成。它给予单元测试一种必要的合理的实现途径。
较低层次的多余功能可以通过自上而下法来鉴别,这是因为没有路径来测试它。(但是,这可能在区分多余的功能和没有被测试的功能时带来困难)。
2.3 缺点
自上而下法是通过桩模块来进行控制的,而且测试用例常常涉及很多的桩模块。对于每个已测单元来说,测试变得越来越复杂,结果是开发和维护的费用也越来越昂贵。
依层次进行的自上而下的测试,要达到一个好的覆盖结构也很困难,而这对于一个较为完善、安全的关键性应用来说至为重要,同时这也是很多的标准所要求的。难于达到一个好的覆盖结构也可能导致最终的多余功能和未测试功能之间的混乱。由此,测试一些低层次的功能,特别是错误处理代码,将彻底不切实。
一个单元的变化往往会影响对其兄弟单元和下层单元的测试。例如,考虑一下D单元一个变化。很明显,对D单元的单元测试不得不发生变化和重新进行。另外,要使用已测试单元D的E、F、G、H、I和J单元也不得不重新测试。作为单元D改变的结果,上述测试自身可能也不得不发生改变,即使单元E、F、G、H、I和J实际上并没有改变。这将导致当变化发生时,重复测试带来的高成本,以及高额的维护成本和高额的整个软件生产周期的成本。
在为自上而下测试法设计测试用例当中,当被测单元调用其他单元时需要测试人员具备结构化知识。被测试单元的顺序受限于单元的层次结构,低层次的单元必须要等到高层次的单元被测试后才能被测试,这样就形成了一个“又长又瘦”的单元测试阶段。(然而,这可能会导致测试详细设计与软件生命周期编码阶段的整体重叠。)
如图2.1所示的例子程序中各个单元之间的层次关系十分简单,在实际的编程过程中可能会遇到类似的情形,而且各个单元之间的层次关系会更复杂。所以自上而下测试法的缺点对单元测试造成的不利影响会随着被测单元之间复杂的联系而加深。
2.4 总结
一个自上而下的测试策略成本将高于基于分离的测试策略,这取决于顶层单元下层单元的复杂程度,以及由于下层单元自身发生变化所带来的显著影响。对于单元测试来说自上而下的组织方法不是一个好的选择。然而,当各个组成单元已经被单独测试的情况下,用自上而下法进行单元的集成测试是个不错的手段。
3. 自下而上法 3.1 详述
在自下而上的单元测试中,被测单元与调用被测单元的单元是分开测试的,但是测试时所使用的是真实的被调用单元。
测试时最底层的单元首先被测试,这样就方便了对高层次单元的测试。然后使用前面已经被测试过的被调用单元来测试其他的单元。重复这个过程直到最高层的单元被测试为止。自下而上法需要测试驱动,但是不需要测试桩。
图3.1说明了测试D单元时需要的测试驱动和已测单元的情况,假设单元E、F、G、H、I和J已经通过自下而上法进行了测试。
图3.1显示了一个程序的单元测试的测试计划,该计划使用了基于自下而上的组织方法,其过程如下: 步骤(1)(注意在测试步骤中测试的顺序不是最主要的,步骤1中的所有测试可以同步进行)。
1) 测试单元H,在调用H单元的E单元处使用一个测试驱动; 2) 测试单元I,在调用I单元的E单元处使用一个测试驱动; 3) 测试单元J,在调用J单元的E单元处使用一个测试驱动; 4) 测试单元F,在调用F单元的D单元处使用一个测试驱动; 5) 测试单元G,在调用G单元的D单元处使用一个测试驱动; 6) 测试单元B,在调用B单元的A单元处使用一个测试驱动; 7) 测试单元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。
3.2 优点
和自上而下法一样,自下而上单元测试法提供了一种比软件集成阶段更早的单元集成。自下而上单元测试同样也是真正意义上的单元测试和软件集成策略的结合。因为不需要测试桩,所以所有的测试用例都由测试驱动控制。这样就使得低层次单元附近的单元测试相对简单些。(但是,高层次单元的测试可能会变得很复杂。)
在使用自下而上法测试时,测试用例的编写可能只需要功能性的设计信息,不需要结构化的设计信息(尽管结构化设计信息可能有利于实现测试的全覆盖)。所以当详细的设计文档缺乏结构化的细节时,自下而上的单元测试就变得十分有用处。
自下而上单元测试法提供了一种低层次功能性的集成,而较高层次的功能随着单元测试过程的进行按照单元层次关系逐层增加。这就使得自下而上单元测试很容易地与测试对象相兼容。
3.3 缺点
随着测试逐层推进,自下而上单元测试变得越来越复杂,随之而来的是开发和维护的成本越来越高昂,同样要实现好的结构覆盖也变得越来越困难。
低层单元的变化经常影响其上层单元的测试。例如:想象一下H单元发生变化的情况。很明显,对H单元的测试不得不发生变化和重新进行。另外,对于A、D和E单元的测试来说,因为它们共同使用了已测试过的H单元,所以它们的测试也不得不重做。作为H单元发生变化的后果,这些测试本身可能也要进行改变,即使单元A、D和E实际上并没有发生变化。这就导致了当变化发生时,产生了与重新测试有关的高额代价,以及高额的维护成本和整个软件生命周期成本的提高。
单元测试的顺序取决于单元的层次关系,较高层次的单元必须要等到较低层次单元通过测试后才能进行测试,所以就形成了“长瘦”型的单元测试阶段。最先被测试的单元是最后被设计的单元,所以单元测试不能与软件生命周期的详细设计阶段重叠。
如图2.2所示的例子程序中各个单元之间的层次关系十分简单,在实际的编程过程中可能会遇到类似的情形,而且各个单元之间的层次关系会更复杂。与自上而下测试法一样,自下而上测试法的缺点会随着被测单元之间复杂的联系而放大。
3.4 总结
自下而上组织法对于单元测试来说是个比较好的手段,特别是当测试对象和重用情况时。然而,自下而上方法偏向于功能性测试,而不是结构化测试。对于很多标准所需要的高集成度和安全的关键性应用,需要达到高层次的结构覆盖,但自下而上法很难满足这个要求。
自下而上单元测试法与很多软件开发所要求的紧凑的时间计划是相冲突的。总的来说,一个自下而上策略成本将高于基于分离的测试策略,这是因为单元层次结构中低层次单元以上单元的复杂程度和它们发生变化所带来的显著影响。
4. 分离法 4.1 详述
分离测试法是分开测试每一个单元,无论是被调用单元还是调用单元。被测单元可以按照任意顺序进行测试,因为被测单元不需要其他任何已测单元的支持。每一个单元的测试都需要一个测试驱动,并且所有的被调用单元都要用测试桩代替。图4.1 说明了测试单元D 时需要的测试驱动和测试桩的情况。
图4.1 显示了某个程序中一个单元的测试计划,该计划基于分离组织方法的策略,只需要如下所示的一步:
步骤(1)(注意该测试计划只有一步。测试的顺序不是最主要的,所有的测试可以同步进行。)
1) 测试A 单元,使用一个测试驱动启动测试,并且将B、C 和D 单元换成测试桩; 2) 测试B 单元,在A 单元处使用一个测试驱动来调用B 单元; 3) 测试C 单元,在A 单元处使用一个测试驱动来调用C 单元;
4) 测试D 单元,在A 单元处使用一个测试驱动来调用D 单元,并且将E、F和G
单元换成测试桩(如图3.1 所示);
5) 测试E 单元,在D 单元处使用一个测试驱动来调用E 单元,并且将H、I和J
单元换成测试桩;
6) 测试F 单元,在D 单元处使用一个测试驱动来调用F 单元; 7) 测试G 单元,在D 单元处使用一个测试驱动来调用G 单元; 8) 测试H 单元,在E 单元处使用一个测试驱动来调用H 单元; 9) 测试I 单元,在E 单元处使用一个测试驱动来调用I 单元; 10) 测试J 单元,在E 单元处使用一个测试驱动来调用J 单元。
4.2 优点
彻底地测试一个分离的单元是很容易做到的,单元测试将其从与其它单元之间复杂的关系中分离了出来。分离测试是最容易实现良好的结构性覆盖的方法,并且实现良好结构性覆盖的困难程度与确定某一个单元在单元层次中所处位置的难易度没有什么不同。
因为每一次只测试一个单元,所以该方法中所使用的测试驱动比自下而上法中所使用的测试驱动简单,该方法中所使用的测试桩比自上而下法中使用的测试桩简单。由于采用了分离的方法进行单元测试,被测单元之间没有依赖关系,所以单元测试阶段可以和详细设计阶段,以及软件生命周期的代码编写阶段重叠。所有单元都能同步测试,形成了单元测试阶段“短而宽”的特点。这有利于通过扩大团队规模的手段缩短整个软件开发的时间。分离测试法另外一个优点是去除了测试单元之间的内部依赖关系,所以当一个单元发生变化时只需要改变那个发生变化的测试单元,而对其它测试单元没有任何影响。由此可以看出分离组织法的成本要低于自下而上组织法和自上而下组织法,特别是当发生变化时其效果更加明显。
分离法提供了一种与集成测试不同的单元测试分离手段,它允许开发人员在软件生命周期的单元测试阶段专心致力于单元测试工作,而在软件生命周期的集成测试阶段专心致力于集成测试工作。只有分离法是纯粹意义上适用于单元测试的方法,自上而下测试法和自下而上测试法适用于单元测试和集成阶段的混合过程。与自上而下法和自下而上法不同的是,用分离法进行的单元测试,被测单元不会受到与其关联的其它任何单元的影响。
4.3 缺点
用分离法进行单元测试最主要的缺点是它不能提供一个早期的单元集成。这必须要等到软件生命周期的集成阶段才能做到。(这很难说是一个真正的缺点)
用分离法进行单元测试时需要结构设计信息和使用测试桩、测试驱动。这会导致在测试靠近底层的单元时,所花费成本要高于自下而上法。然而,这个缺陷可以通过简化层次较高的单元的测试,以及每个单元每次发生变化时的较低花费得到补偿。
4.4 总结
用分离法进行单元测试是最合适的选择。在加上适当的集成策略作为补充,将会缩短软件开发时间所占比例和降低开发费用,这个优势将会贯穿整个软件开发过程和软件生命周期。按照分离法进行单元测试时,被测单元可以按照自上而下或者自下而上的顺序进行集成,或者集成为任何便利的群组和群组的结合。然而,一个自下而上的集成方式是与目前流行的面向对象和面向对象的设计最相兼容的策略。分离法单元测试是实现高层次结构覆盖的最佳手段,而高层次结构覆盖对于很多标准所要求的高完善性和安全的关键性应用来说是至关重要。在通过单元测试完成了所有实现好的结构覆盖的困难工作的基础上,集成测试就可以集中于全面的功能测试和单元交互的测试。
5. 使用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”同样详细讨论了在用自下而上法进行单元测试时,分离的类和层次等级的约束是如何引发问题的。文章介绍了分离单元测试法是如何成为唯一实用的处理分离的类和层次等级约束的途径
变那个发生变化的测试单元,而对其它测试单元没有任何影响。由此可以看出分离组织法的成本要低于自下而上组织法和自上而下组织法,特别是当发生变化时其效果更加明显。
分离法提供了一种与集成测试不同的单元测试分离手段,它允许开发人员在软件生命周期的单元测试阶段专心致力于单元测试工作,而在软件生命周期的集成测试阶段专心致力于集成测试工作。只有分离法是纯粹意义上适用于单元测试的方法,自上而下测试法和自下而上测试法适用于单元测试和集成阶段的混合过程。与自上而下法和自下而上法不同的是,用分离法进行的单元测试,被测单元不会受到与其关联的其它任何单元的影响。
4.3 缺点
用分离法进行单元测试最主要的缺点是它不能提供一个早期的单元集成。这必须要等到软件生命周期的集成阶段才能做到。(这很难说是一个真正的缺点)
用分离法进行单元测试时需要结构设计信息和使用测试桩、测试驱动。这会导致在测试靠近底层的单元时,所花费成本要高于自下而上法。然而,这个缺陷可以通过简化层次较高的单元的测试,以及每个单元每次发生变化时的较低花费得到补偿。
4.4 总结
用分离法进行单元测试是最合适的选择。在加上适当的集成策略作为补充,将会缩短软件开发时间所占比例和降低开发费用,这个优势将会贯穿整个软件开发过程和软件生命周期。按照分离法进行单元测试时,被测单元可以按照自上而下或者自下而上的顺序进行集成,或者集成为任何便利的群组和群组的结合。然而,一个自下而上的集成方式是与目前流行的面向对象和面向对象的设计最相兼容的策略。分离法单元测试是实现高层次结构覆盖的最佳手段,而高层次结构覆盖对于很多标准所要求的高完善性和安全的关键性应用来说是至关重要。在通过单元测试完成了所有实现好的结构覆盖的困难工作的基础上,集成测试就可以集中于全面的功能测试和单元交互的测试。
5. 使用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”同样详细讨论了在用自下而上法进行单元测试时,分离的类和层次等级的约束是如何引发问题的。文章介绍了分离单元测试法是如何成为唯一实用的处理分离的类和层次等级约束的途径
评论
查看更多