一、单元测试的好处
在系统开发过程种使用单元测试,会带来很多的的好处,最明显为:
When you become convinced of the value of comprehensive unit testing, you’ll find that it begins to influence how you write code, and the frameworks you choose to use。
此外,单元测试可以有效的支持重构;同时其本身就是一个很好的类使用说明;
二、单元测试的关注点
应用单元测试,首先要解决的是单元测试的关注点。
测试的关注点在于测试逻辑,只要有逻辑就要写测试代码。测试的手段就是验证所有被测试方法的所有产出物,包括:
1. 测试方法的返回值
2. 测试方法的执行流程
例如:
public class DomainService {
private static TheDAO dao = new TheDAO ();
public ReturnObject findByCond(String) {
return (ReturnObject)dao.getBeanByCondition("select * from ReturnObject where cond="+ paramter, ReturnObject.class);
}
}
在对于测试findByCond方法,有两个测试用例:
A.测传递给TheDAO.getBeanByCondition的参数的正确性,如果参数不是”select * from ReturnObject where cond=?”和ReturnObject.class则返回为null。
B.测返回对象的正确性。
特别是第二点,在商业应用上比较常见的。通常有些方法无明显output,通常是执行写表操作的。对于这样的方法就是测试它的执行流程。当然这些方法本身包含逻辑的。
一个简单的解决方法是利用Access Log来实现(虽然这样的测试不多,而写的case代码也看着怪怪的)。
public class ServiceExample{
private DatabaseDao1 dao1;
private DatabaseDao2 dao2;
public void noOutputMethod(){
if(...)
dao1.update(...);
if(...)
dao2.delete();
}
}
相关的测试代码可以这样:
public class MockDatabaseDao1 implements DatabaseDao1 {
private Map map;
public void setMap(Map map){
this.map = map;
}
public void update(args){
map.put("MockDatabaseDao1.update", args);
}
}
public class MockDatabaseDao2 implements DatabaseDao2 {
private Map map;
public void setMap(Map map){
this.map = map;
}
public void delete(args){
map.put("MockDatabaseDao2.delete", args);
}
}
public class ServiceExampleTestCase{
private Map map = new HashMap();
public void testNoOutputMethod(){
DaoTest test = new DaoTest();
DatabaseDao1 dao1 = new MockDatabaseDao1();
dao1.setMap(map);
dao2.setMap(map);
DatabaseDao2 dao2 = new MockDatabaseDao2();
test.setDao1(dao1);
test.setDao2(dao2);
test.noOutputMethod();
assertEquals(new Boolean(true),
new Boolean(map.containsKey("MockDatabaseDao1.update")));
assertEquals(new Boolean(true),
new Boolean(map.containsKey("MockDatabaseDao2.delete")));
}
}
例子只测试执行流程,实际实践中还可以验证所有的参数。
我们还可以考虑利用AOP(dynamic proxy和cglib)结合StackFrame来改进这个测试方法。这样不用每次做同样的工作。开源的EasyMock已经很好的实现流程点以及参数验证测试.
三、单元测试的手段
讨论完测试的关注点后,看看单元测试的手段:编写Mock object(Stubs)。
编写mock object来辅助测试,是非常重要的技术。Mock object分动态mock和静态mock即stubs。推荐采用动态mock,因为动态mock方式中,mock数据和测试用例在一起,非常方便开发,阅读和管理,而采用stub则不得不编写较多的类用于处理不同的测试用例。
而从目标来看,Mock包括接口和类的mock。接口的Mock很容易,而具体类的mock也很简单,通常利用子类继承的方式实现;利用cglib框架可以很好大达到测试目的。
针对于POJO的设计模型,采用EasyMock可以很好的实现动态的接口和类mock。
现在看看实际面临的具体困难:
职责不明确
类或类方法的职责不明确,违反SRP原则.一个类或方法处理了本不该有它处理的逻辑,使得单元测试需要关心过多的外部关联类。
静态方法
静态方法使得调用者直接面对实际的服务类,难以通过其他方式替换其实现,也难以扩展。
无返回值的服务方法
无返回值方法使得调用者难以模拟其实现
直接访问对象实例
调用者直接实例化服务对象,从而使用服务对象提供的服务.同静态方法一样,直接面对其服务类。
J2se和J2ee标准库或者其他类库
标准类库中有非常多的接口调用使得调用者难以测试 e.g JNDI, JavaMail, JAXP。
准备数据及其困难
编写测试用例需要外部准备大量的数据
针对这些困难,可用解决方法如下:
针对于职责不明确
1. 重构系统。对于职责不明确的代码,只有通过重构才可以达到单元测试的目的。
2. 自我测试。针对于class的测试,使用自代理测试模式, 使得测试时,可以重写被测试类的一些方法.达到测试的目的.通过extend class override methods来实现。Inner class mock方法也一样。不过这种方法比较别扭
针对于静态方法
静态方法的mock。静态方法由于是直接面对服务对象,比较麻烦。不过,并非不可以测试,有3种方法。
1 如果有源代码,应更新设计,通过委派给内部单例的做法,提供静态服务,这样就允许设计上对于内部单例实现的代理和Mock。
2 利用classpath的特点来实现。方法很简单,mock类与建立一个将被mock的类的package,class name以及方法签名完全一样,但方法实现却是mock过的。在运行测试用例时,把mock类打成jar(不一定要这么做), 在配置classpath时确保,该jar的位置在当前class之前,就可以实现替换。
3 利用AOP,Amock类库采用了AspectJ提供的类加载时字节码增强能力,可以提供对无源代码的静态方法mock。
针对于无返回值的服务方法
EasyMock在2.2版本上支持StubAnswer,使得我们可以模拟该方法;
针对于直接访问对象实例
重构系统。采用接口隔离的设计方法。
针对于J2SE和J2EE标准库
1.重构系统。采用POJO的设计方法
2.使用成熟单元测试框架
除了最基本的Junit外,Opensource提供了很多非常有价值的单元测试框架,熟练使用这些工具,可以提高测试的效率。包括对准备大量的数据,以及j2ee的框架代码。
现有代码的可选自动化测试工具:
1. EJB: MockEJB或者MockRunner
2. Servlet:Cactus
3. Struts:StrutsUnitTest
4. XML:XMLUnit
5. J2EE: MockRunner
6. GUI: JFCUnit, Marathor
针对于准备数据及其困难
1. Data Object:DDTUnit。准备大量数据。
2. Dao:DBUnit。初始化数据库。批量产生数据库数据。
此外,Spring框架本身可以用来组织大量而复杂的测试对象模型,只要配合好一定的设计手法和约定,大量数据和对象的组织问题就可迎刃而解。
四、分层架构下的单元测试
1 Web层的单元测试
主要测试Controller的数据结构化逻辑。如果View是利用模板引擎的,需要测试页面的控制脚本是否正确。
2 Domain Service的单元测试
包括了业务规则和业务流程
|
参与对象
|
有四种参与对象:1. Domain Object2. Dao对象3. 其它Service服务。4. 工具类 |
| 产出物 |
1. 返回值包括POJO,和结构化的数据(如XML)2. 传递给流程节点的参数值 |
| 特点 |
概念上,业务逻辑和业务流程是相对独立的。实际代码中,虽然一些业务逻辑是相对独立的。但是有一些业务逻辑与流程合在一起。由于业务逻辑有明确的返回值,业务规则可以独立成一个方法,其是有显示的返回值,这样单元测试就可以关注在业务规则的测试上。而业务流程通常没有显示的返回值,在很多实践中表现为持久化动作,测试比较麻烦,需要重构和通过EasyMock来完成测试 |
| 测试覆盖面 |
返回值包括POJO,或者结构化的数据如XML可以利用XMLUnit来解决。流程节点的访问,以及传递给流程节点的参数值。即对业务流程的测试,可以使用上面的访问点的方法。 |
3.Dao的单元测试
第一个面临的问题是:做Dao数据访问层的单元测试时机。another word也就是要不要做单元测试。
几种情况是不用测试的
1. 如果Dao就是简单的CRUD,那么不用测;在未来当我们使用1.5的范型后,这些CRUD只要在父类做一边里就可以了。
2. 如果hbm文件是自动生成的,那也不用测。
以下是要测的情况:
1. 如果hbm文件是手工写的,那么需要你保证hbm的正确性。如何测试,后面再说。
2. 如果Dao中包括了一些组合查询,那么这是一种逻辑,就应该去测;如果Dao的查询还包含了某个排序机制,这个排序逻辑依据的是业务字段,那么也是要测的。(理由是:这些逻辑可以在java代码实现,不过是性能太差了,但是既然java代码的逻辑要测,那么我们没有理由不去测在sql中的逻辑)。
第二个问题如何测试:
0. 测试数据准备
可以将BA准备的数据导出。在利用Excel编辑产生一批数据。
但是每个UnitTest测试本身应该focus一个关注点上,所以每个UnitTest的数据保持在较少的水平上。
另外由于DBUnit导入数据的顺序是依据sheet的顺序的,请注意把所有外键表在前,否则插入数据时,会报外键不存在错误。
1. 数据库的选择
a.可以直接用小组用的开发数据库。优点:现成的, 所有schema都建好了。缺点:目前数据库的数据干净性无法保证,连接速度太慢。
b. 使用hsqldb。优点:利用其内存模式,可以随测试程序启动,简单小巧,schema可以自行定义,每人各自一套互不影响。缺点:无法提供PLSQL支持。出于UnitTest本身的要求,以及性能上考量,大部分情况下,建议使用hsqldb,对于涉及到PLSQL的,需要 mock处理。
2.测试hbm
利用hsqldb内存数据库,在setup的时候,利用hibernate的SchemaExport工具类,将hbm导出成数据库的schema,如果有确实有潜在问题,那么测试程序将不通过。
3.测试Dao
很简单了,调用dao程序操作。对于save,update和delete操作的。需要利用原始的connection执行查询验证。对于组合查询的和逻辑排序的,就是一般的做法了。
4. 在使用DBUnit时,测试非只读操作时,我们经常会采用 DatabaseOperation.CLEAN_INSERT 策略.在关联表比较多时,效率会很差.因为每次setUp,tearDown时都会重新先Delete,再Insert所有的数据.另外,我们还有一种数据库操作测试的策略,就是使用真实数据库,在每次操作完毕后都回滚事务。
五、单元测试的考量
最后要说的是单元测试的度量和考核。
测试覆盖率,简单的说就是衡量测试活动覆盖产品代码的指标。测试的目的,是为了验证产品代码按照预期的目的执行,也可以被看作代码功能的文档说明。进一步的分析,测试覆盖率间接的衡量产品的质量,因为它只是衡量了测试代码的质量,而不是产品代码
常用测试覆盖率衡量指标:
Line coverage
Basic block coverage
Method coverage
Class coverage
Branch coverage
并非覆盖率越高越好,实际针对于不同类型的公司,所需要的单元测试覆盖率也不同,必须考量单元测试的投入产出。
作者介绍
林仪明,网名“Anders小明”(yimlin)。有数年企业应用软件的设计与开发经验,面向对象和面向方面技术的爱好者。参与过国内开源项目SpringSide的设计开发,参与了InfoQ的迷你书《Scrum Checklists》和《Grails入门指南》的翻译。个人也是敏捷的爱好者,致力于将敏捷推向实践。
当前评分 4.0 , 共有 2 人参与
- Currently 4/5 Stars.
- 1
- 2
- 3
- 4
- 5