UML软件工程组织

 

 

开发利器之单元测试
 
作者: hyysguyang  文章来源:网络
 
 0.导言
 1.单元测试的分类
  1.1 逻辑单元测试(plain junit test)
  1.2. 集成单元测试
  1.3. 功能单元测试
 2.单元测试的动机
 3.单元测试的目标
 4.确保可测试性
 5.测试策略
  5.1用stub进行粗粒度测试
  5.2用mock objects进行孤立测试
  5.3用Cactus进行容器内测试
 6 测试覆盖率
 7.最佳实践
 8.Spring的经验
 9.参考资料

 0.导言

 Never in the field of software development was so much owed by so many to so few lines of code.
软件开发领域中此前从未有过这样的事情:很少几行代码对大量的代码起了重要的作用。——Martin Fowler
Any program feature without an automated test simply doesn’t exist.

 任何没有经过自动测试的程序特性就等于不存在的特性。—— Kent Beck

 1.单元测试的分类

 1.1 逻辑单元测试(plain junit test)

 逻辑单元测试主要检查代码逻辑性,这些测试通常只是针对单个方法。你可以通过mock
objects 或者stub来控制特定的方法的边界。这种类型的单元测试是最重要也是最基本的。其在单元测试中的重要性不亚于POJO.这种类型的测试最大的特点就是执行速度快,而这也是单元测试最重要的一个特性之一。
可以参看附近中的文件过滤器ExtensionFileFilter.java及其测试用例。

 1.2. 集成单元测试
 这种单元测试主要是在真是环境(或者真实的环境的一部分)下的两个组件相互交互的测试。例如:一段访问数据库的程序已经被测试证实能够有效地访问到数据库,那么就可以提供和数据库交互的接口,最典型的就是Cactus容器内测试,以及公司的测试command的Lavender已经测试非command组件的vanilla。commad的测试实例可以参看附近中的规则命令SolarRuleEntryCommand.java及其测试用例。

 1.3. 功能单元测试
 这种单元测试越出了集成单元测试的边界,目的是为了确认激励-响应。如访问页面测试。
更相信的分类可以参考相关的测试文献,junit in action 上面也提到,事实上上面提到的概念,都来自于它。
没有什么是最好的,最适合的就是最好的!写单元测试你不得不写额外的代码,如果写测试没有好处,我们又何必去浪费时间呢。因此你应该确信测试用例能给你带来好处,这样你才会愿意去写,也只有这样你才能写的出对于你有帮助的测试用例。因此,在你未确信单元测试给你带来好处之前,请不要去写它。既然这样,我们就简单看看单元测试到底给我们带来什么好处。

 2.单元测试的动机

 2.1. 确保产品质量,单元测试可以有效的挑出很多常见的代码错误。

 2.2. 有助于明确需求, 一边编写应用代码一边编写测试,有助于定义类的需求。

 如对于一个方法,对于传入的null参数应该如何处理?是否应该返回null?你必须为这些情况编写测试,所以你一定得把这些行为都想得清清楚楚,之后就可以在JavaDoc中写明它得用法。

 2.3. 测试套件(Test Suite)是一种强大而重要得文档。可执行得文档,保持与代码同步。

 2.4. 详尽的Test Suite确保有效的回归测试。
 当我们增加了新功能,或者修正bug时,我们可以确保现存的功能仍然正确。同时增强自信。确保不断疯狂的重构。重构保证我们不管在什么时候对所需的功能有最优的实现。优秀的开发人员都习惯于重构。每次我commit到SCM(如公司采用的svn)中的代码都是正确的,因为在commit之前我看到了绿色动态条,我的所有的代码都通过了测试,这很好。这样你难道不会更加自信么?

 对于重构的精妙叙述请参阅Martin Fowler 的经典重构:改善既有代码的设计.

 就上述几点,我相信测试用例就值得我去写。其实还有很多其他的好处,但目前来说我体会最深的就是这几点。既然单元测试有这么多好处,那就让我们看看,我们该测什么?什么时候我们需要写测试用例呢?

 3.单元测试的目标

 其实编写单元测试很简单,不论是stub也好,还是mock object也好,抑或是容器内测试也好,入门都很简单,更重要的是测试策略,针对什么类你采用对应的测试方法,是stub呢还是mock object,你应该知道你该测什么,而不该测什么,这才是最重要的,当然这些需要不断的实践,经验越丰富你就越清楚。

 单元测试一般是白盒测试,通常需要对被测试的类的内部细节有足够的了解。在写测试用例时请永远记住:

 1. 每一个测试用例只应该测试一个类,而不是间接测试其合作者。
 因为没个需要测试的类都会有自己的测试用例,它不再需要其他的来间接测试它。

 2. 基于第一条,你应对你的类进行隔离测试。采用stub,或者mock object,替代你要测的类的协作对象。把你要测的类隔离开来进行测试。

 单元测试也意味着你要知道哪些是不应该被测试的。当你明白哪些是不该测试的,剩下哪些该测试就显而易见了。明确该测什么不该测什么,这点很重要。

 对于什么该测,什么不该测试,以前我及其迷惘,因为我不知道某个类我到底该不该测,思来想去之后,我后面决定:
任何时候对你的任何代码不放心,请你立刻写一个对应的测试。确定测试通过之后,直到你放心为止。

 以我的经历,写测试用例不难,但写有用的测试用例就不是那么容易了。提高你的测试能力一个比较好的做法就是动手去写,分享别人的经验,实践一些最佳实践。

 既然我们已经确认我们应该要写单元测试了,那么就存在一个问题:这个类容易测试么?可测试么?这就涉及代码可测试性的问题,在下一节,我们就来看看代码的可测试性吧。

 4.确保可测试性

 不同的代码(类或方法)其可测试性是有很大区别的,比如:给你一个计算a+b的方法,相信你很快就写好它的测试代码,可是如果让你测试一个servlet或访问数据库的代码呢?也许你就要花上一点时间了。比如,对于EJB组件必须在应用服务器中运行,要测他也许你要启动一个应用服务器,且要部署整个应用,要编写并部署这样一个组件的一个测试用例就会花费很长的时间,运行这样的测试用例同样要花费很长很长的时间,这样你就不能很频繁的运行测试套件,你也就失去很多单元测试所带来的好处。

 既然我们要编写单元测试,那可测试性就理所当然的成为程序代码质量的一个重要方面。事实上代码的可测试性与程序代码的质量有很大的关联。

 容易测试的代码通常会更好、更可读、并且更加易于维护。对于难测试的代码通常也难于维护和升级。改善代码的可测试性很大程度上会改善你的设计。大多数情况下,编写可测试的程序代码会促成高质量的程序代码。比如:

 1.如果一个方法长的无法确保其中所有的代码都被执行了,那么这个方法必定也是不可读的、难于维护的。
 2.如果有些代码依赖于在Singleton中持有的全局静态状态,这些代码无法独立进行测试。
 3.假若代码不是可插入的——也就是说无法把一个特定的合作者替换成一个模仿测试对象—会使的程序难以扩张。
如果你不知道如何测试某段代码,请考虑重构,看看是否能对实现略加修改以便进行测试。

 有些编程惯例容易导致不可测试的代码,如:
 1. Singleton反模式
 难以用stub来替换Singleton对象。无法被IoC容器创建和管理。
 2. 静态Facade
 由静态方法组成的facade也不容易测试。由于所有的调用者都绑定到了静态Facade,你不可能插入一个测试实现。此外,静态方法无法被覆盖,这样实际上失去了一种重要的OOP能力。

 一般只有在下列情况下才应该使用静态方法。
  • 不属于任何类的、过程性的工具方法。
  • 操作ThreadLocal状态。
  • 返回Singleton的实例。

遵循一定的原则和利用一些技巧可以提高代码的可测试性,如:针对接口编程,而非针对类编程

  • 使用Strategy设计模式
  • 牢记迪米特法则
  • 尽量减少对环境相关API的依赖
  • 合理划分类的职责
  • 隐藏实现细节
  • 重构以便在测试期间覆盖某些方法

从上面的几点可以看出,编写容易测试的代码与编写优秀的高质量的代码所遵循的编程惯例是一致的。事实上先编写测试用例再编写产品代码(或者采用TDD)的方式进行开发逼着你遵循这些原则,很自然的,你在不经意之中,就遵循了这些优秀的编程管理,当然实际上不仅仅这些,还有很多其他的OOD原则,都与上述的这些类似。

 5.测试策略

 并不是每个类都可以用同样的方式来测试的,比如测试访问数据库的代码,测试依赖与容器的组件(如Servlet,EJB),因此需要针对具体要测试的类采用相应的测试方式。下面是一般测试策略。

  • 用stub进行粗粒度测试
  • 用mock objects进行孤立测试
  • 用Cactus进行容器内测试

前两种其实都是为了解决同一个问题:如何保持测试的独立性,而最后一种测试实际上属于集成单元测试。

 在单元测试的目标中,我们提到,每一个测试用例只应该测试一个类,而不是间接测试其合作者。既然这样,那这个类的合作者我们怎么去创建呢?很多时候他们类代码我们也许还没编写,而我们现在用用,该怎么办?采用stub或者mock object进行隔离测试。

 5.1用stub进行粗粒度测试
 一般来说,你都可以编写你所测试的类所依赖的其他类的stub来替代协作的对象。不过这样你可能就需要编写大量的stub代码。而且有些时候编写这样的stub并不是那么容易,如当你测试一个EJB组件是,你可能需要实现一个简单的stub容器。

 5.2用mock objects进行孤立测试
 除了采用stub之外,你还可以写一个模拟协作类的类,用他创建的对象来代替要测试的类的协作对象,我们称这样的对象为mock object(模拟对象)。模拟对象有两种方式:

 5.2.1静态mock object
 编写模拟类,并用模拟类创建的对象来代替协作对象与待测试类的对象进行交互,需要编写模拟类。
 5.2.2动态mock object
 采用运行期动态的生产模拟对象来与待测试类的对象进行交互。由于mock object是动态生成的,因此不需要编写模拟类。目前已经有很多这样的动态mock object的框架,如JMock,EasyMock。不论你采用哪一种动态mock object框架,一般都遵循以下的步骤:

 (1).创建mock实例。
 (2).记录对mock调用的期望
 (3).重放(replay)状态。
 (4).把mock实例作为交互对象来参加测试
 (5).调用使用mock的测试用例
 (6).验证mock行为
 
 5.3用Cactus进行容器内测试
 采用stub或者mock object编写的TestCase(Plain JUnit TestCase),只能验证单个类的行为,而不能验证真个系统的各个组件之间的交互,要验证这样的交互是否正确,比如一个请求是否正确的转发给相应的组件?一个控制器是否正确的访问某个业务对象?一个DAO是否正常的访问了数据库?你的数据连接是否能正常连接到目标数据库呢?这些是Plain Junit TestCase没法测试的,对于这样的代码,我们需要编写集成单元测试,借助Apache的Cactus可以很轻松的编写这样的TestCase。

 6 测试覆盖率

 测试覆盖率对单元测试具有重要的意义,但是不要误用。测试覆盖报告最好用来检查哪些代码没有充分的测试。当您检查覆盖报告时,找出较低的值,并了解为什么特定的代码没有经过充分的测试。然后采取相应的解决方法。测试覆盖报告一般还可以用来:

 1.估计修改已有代码所需的时间
 2.评估代码质量
 3.评定功能测试
 切记:不要为了覆盖率而覆盖,应该为了确定代码的功能编写测试,而不是为了提高覆盖率而编写测试。
 以下是一个真实项目的2006-8-24日的包com.numen.ralos.web.action测试覆盖报告:
 图1.测试覆盖报告
 
 7.最佳实践

 1. 测试用例应当经济,同时又保持自描述性
 2. 必要时对测试套件进行重构
 3. 避免编写有副作用的测试
 4. 连续的回归测试:测试系统自动化
 5. 一次只测试一个对象
 6. 测试要尽可能地小,执行速度快
 7. 反向测试
 8. 等价划分
 9. 测试自动化

 8.Spring的经验

 这部分内容取自Expert One-on-One™J2EE™Development without EJB™[5]。

 1. 小步前进。一次只实现一部分功能。
 2. 如果你认为一段待命的测试不够充分,请立即为它加上更多的测试。
 3. 测试的名字要有意义,显示除它们的用途。
 4. 不要依赖于测试用例的前后顺序,也不要编程创建测试套件。
 5. 确保整个测试套件能够快速运行。
 6. 把任何代码提交到源代码管理系统之前,完整的运行测试套件。
 7. 如果用到了配置文件,从classpath来加载它。
 8. 编写ant脚本来运行所有的测试,以及进行测试覆盖率分分析。
 
 9.参考资料

 [1] Vincent Massol. JUnit in action.Manning.2004
 [2] David Astels. Test-Driven Development:A Practical Guide(2004年度美国《软件开发》杂志Jolt大奖 )
 [3] Robert C.Martin. Agile Software Development:Principles,Patterns,and Practices(2003年度美国《软件开发》杂志Jolt大奖 )
 [4] Kent Beck. Test-Driven Development By Example.2002
 [5] Rod Johnson.etc. Expert One-on-One™J2EE™ Development without EJB™.2004



 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号