单元测试实践小结

在系统开发过程种使用单元测试,会带来很多的的好处,最明显为:
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来改进这个测试方法。then, we needn't to do the same work,each time. We repeat it only once.

讨论完测试的关注点后,需要看看实际面临的具体困难
职责不明确 
   类或类方法的职责不明确,违反SRP原则.一个类或方法处理了本不该有它处理的逻辑,使得单元测试需要关心过多的外部关联类
静态方法
    静态方法使得调用者直接面对实际的服务类,难以通过其他方式替换其实现,也难以扩展
直接访问对象实例
调用者直接实例化服务对象,从而使用服务对象提供的服务.同静态方法一样,直接面对其服务类
J2se和J2ee标准库或者其他类库
    标准类库中有非常多的接口调用使得调用者难以测试 e.g JNDI, JavaMail, JAXP
准备数据及其困难
    编写测试用例需要外部准备大量的数据

针对这些困难,可用解决方法如下: 
重构系统
    对于职责不明确的代码,只有通过重构才可以达到单元测试的目的。
Self-Delegate test pattern
   针对于class的测试,使用自代理测试模式, 使得测试时,可以重写被测试类的一些方法.达到测试的目的.通过extend class override methods来实现。Inner class mock方法也一样。不过这种方法比较别扭
编写Stubs和Mock object 
   1.   接口的mock比较容易,测试时,编写stubs和mock object来辅助测试,是非常重要的技术. Mock object分动态mock和静态mock.采用EasyMock可以很好的实现动态mock。 
   2.  具体类的mock,也很简单,通常利用子类继承的方式实现,利用cglib框架可以很好大达到测试目的。 
   3.  静态方法的mock。静态方法由于是直接面对服务对象,比较麻烦。不过,并非不可以测试,实际我们可以利用classpath的特点来实现。
方法很简单,mock类与建立一个将被mock的类的package,class name以及方法签名完全一样,但方法实现却是mock过的。在运行测试用例时,把mock类打成jar(不一定要这么做), 在配置classpath时确保,该jar的位置在当前class之前,就可以实现替换。代码如下:StaticMock.rar
使用成熟单元测试框架
   除了最基本的Junit外,Opensource提供了很多非常有价值的单元测试框架,熟练使用这些工具,可以提高测试的效率。包括对准备大量的数据,以及j2ee的框架代码。
   现有代码的可选自动化测试工具:
   1. POJO:JUnit, JMock或者EasyMock
   2. Data Object:DDTUnit。准备大量数据。
   3. Dao:DBUnit。初始化数据库。批量产生数据库数据。
   4. EJB: MockEJB或者MockRunner
   5. Servlet:Cactus
   6. Struts:StrutsUnitTest
   7. XML:XMLUnit
   8. J2EE: MockRunner
   9. GUI: JFCUnit, Marathor
   10. Other: JTestCase(采用XML定义测试过程)

分层架构下的单元测试
1 Web层的单元测试
主要测试Controller的数据结构化逻辑
如果View是利用模板引擎的,需要测试页面的控制脚本是否正确。

2 Domain Service的单元测试
包括业务规则和业务流程。
Service有四种参与对象,如下:
   1. Domain Object
   2. Dao对象
   3. 其它Service服务。
   4. 工具类
产出物:
   1. 返回值包括POJO,和结构化的数据(如XML)
   2. 传递给流程节点的参数值。
特点:
   概念上,业务逻辑和业务流程是相对独立的。实际代码,虽然一些业务逻辑是相对独立的。但是有一些业务逻辑与流程合在一起。由于业务逻辑有明确的返回值,业务规则可以独立成一个方法,其是有显示的返回值,这样UnitTest就可以focus在业务规则的测试上。而业务流程通常没有显示的返回值,在很多实践中表现为写表动作,测试比较麻烦。
   同时,不过的实际情况是业务规则和业务流程是合并在一起的。

测试的应覆盖:
1. 返回值包括POJO,或者结构化的数据如XML可以利用XMLUnit来解决。
2. 流程节点的访问,以及传递给流程节点的参数值。即对业务流程的测试,可以使用上面的访问点的方法。

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所有的数据.另外,我们还有一种数据库操作测试的策略,就是使用真实数据库,在每次操作完毕后都回滚事务.