博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

单元测试实践小结

Posted on 2010-01-18 16:08  雷雷  阅读(159)  评论(0编辑  收藏  举报

应用单元测试,首先要解决的是单元测试的关注点。

测试的关注点在于测试逻辑,只要有逻辑就要写测试代码。测试的手段就是验证所有被测试方法的所有产出物,包括:

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