/**
* @author tao.youzt
*/
public class BizUrlIbatisImpl extends GodzillaDaoSupport implements BizUrlDAO {
private static final String GET_BY_URL = "SELECT-BIZ-URL";
private static final String DELETE = "DELETE-BIZ-URL";
private static final String INSERT = "INSERT-BIZ-URL";
public int delete(String url) {
return this.delete(DELETE, url);
}
public BizUrlDO getByUrl(String url) {
return this.queryForObject(GET_BY_URL, url, BizUrlDO.class);
}
public Object insert(BizUrlDO bizUrlDO) {
return this.insert(INSERT, bizUrlDO);
}
}
DO领域对象
/**
* @author tao.youzt
*/
public class BizUrlDO {
private int id;
private String url;
private String email;
private String name;
// getter and setter
}
因为本文案例使用Spring作为底层框架,因此这里需要编写Spring配置文件对DAO进行组装。
Godzilla-dao.xml
Godzilla-db.xml
DAO及其配置文件都已经准备完毕,我们接下来编写测试用例。Spring为单元测试提供了很多有用的支持类,我们在这里使用的是:
该类提供了POJO属性自动注入的能力,只要为为你的属性字段提供一个Set方法即可。下面我们来看完整的测试用例:
/**
* @author tao.youzt
*/
public class TestBizUrlDAO extends AbstractDependencyInjectionSpringContextTests {
private BizUrlDAO bizUrlDAO;
@Override
protected String[] getConfigLocations() {
return new String[]{"godzilla-dao.xml","godzilla-db.xml"};
}
public void testInsert(){
bizUrlDAO.insert(generateDO());
assertNotNull(bizUrlDAO.getByUrl("www.easyjf.com"));
}
public void testDuplicateInsert(){
bizUrlDAO.insert(generateDO());
try{
bizUrlDAO.insert(generateDO());
assertFalse("Must throw an exception!",true);
}catch(Exception e){
assertTrue(true);
}
}
public void testDelete(){
bizUrlDAO.insert(generateDO());
assertNotNull(bizUrlDAO.getByUrl("www.easyjf.com"));
bizUrlDAO.delete("www.easyjf.com");
assertNull(bizUrlDAO.getByUrl("www.easyjf.com"));
}
private BizUrlSynchronizeDO generateDO() {
BizUrlDO bizUrlDO = new BizUrlDO();
bizUrlDO.setUrl("www.easyjf.com");
bizUrlDO.setName("EasyJWeb");
bizUrlDO.setEmail("webmaster@easyjf.com");
return bizUrlDO;
}
public void setBizUrlDAO(BizUrlSynchronzieDAO bizUrlDAO) {
this.bizUrlDAO = bizUrlDAO;
}
}
getConfigLocations()方法为AbstractDependencyInjectionSpringContextTests 提供配置,Spring会根据该配置文件自动注入bizUrlDAO属性。testInsert()方法用于测试插入新数据,注意这里有个问题,如果数据库中已经存在该URL的记录,则应用会报错,所以这里还要进行数据清除准备处理,我们称之为“测试环境准备”,以后会用到该名词;testDuplicateInsert()方法用于测试插入重复数据的情况,该方法同样存在上面的问题;testDelete()方法用于测试删除数据的情况,这里尽管准备了数据,但仍没有考虑数据库中已经有记录的情况。
综上所述,尽管该测试类已经比较清晰,但仍然存在许多不足之处。我们将在后面的章节进行详细分析,并给出解决方案。
Callback Function & Template Method Pattern
回调函数(Callback Function)和模板方法(Template Method)是软件架构设计中最常用的两种设计模式,这两种设计模式在Spring框架中随处可见。
关于本节是否要详细介绍回调函数(Callback Function)和模板方法(Template Method)模式的问题,笔者考虑了很长时间。因为网络上对这两种普遍使用的设计模式的定义层出不穷,各有各的道理,很难说谁是谁非。况且,针对不同的应用场景,这两种模式也有许多变体,或者联合使用。
因此,笔者最终决定不在此处对这两种模式做任何定义或引用,请读者自行参阅相关文档资料。
回调函数和模板方法模式在单元测试中的应用
上一节我们简单的回顾了回调函数和模板方法模式,Spring框架中大量采用了这两种设计模式,有兴趣的读者可以阅读Spring框架代码进一步巩固对这两种模式的理解和运用。本节将结合回调函数模式和模板方法模式对前面的测试用例进行重构,读者可以在重构过程中逐步了解这两种设计模式的运用。
首先,让我们简单总结一下前面测试用例的问题:
一、抽象层次太低,不够通用?
例如,对于getConfigLocations()方法,我们完全可以放到一个父类中实现,因为对于一个项目而言,其配置文件大多都是统一的,没有必要在没有测试类中都定义该方法。
/**
* DAL层测试支持类.
*
*
* 除非特殊情况,所有DAO都要继承此类.
*
* @author tao.youzt
*/
public abstract class GodzillaDalTestSupport extends AbstractDependencyInjectionSpringContextTests {
/*
* @see org.springframework.test.AbstractDependencyInjectionSpringContextTests#getConfigLocations()
*/
@Override
protected final String[] getConfigLocations() {
String[] configLocations = null;
String[] customConfigLocations = getCustomConfigLocations();
if (customConfigLocations != null && customConfigLocations.length > 0) {
configLocations = new String[customConfigLocations.length + 2];
configLocations[0] = "classpath:godzilla/dal/godzilla-db-test.xml";
configLocations[1] = "classpath:godzilla/dal/godzilla-dao.xml";
for (int i = 2; i < configLocations.length; i++) {
configLocations[i] = customConfigLocations[i - 2];
}
return configLocations;
} else {
return new String[] { "classpath:godzilla/dal/godzilla-db-test.xml",
"classpath:godzilla/dal/godzilla-dao.xml" };
}
}
/**
* 子类可以覆盖该方法加载个性化配置.
*
* @return
*/
protected String[] getCustomConfigLocations() {
return null;
}
}
如图所示,我们提炼了一个抽象支持类,实现了getConfigLocations()方法,同时还提供了getCustomConfigLocations()方法供子类使用,子类可以通过重载该方法提供定制的配置。
有了该支持类,具体测试类只需要继承该类并编写测试逻辑即可。
二、缺少准备测试环境和清除测试数据的环节?
对于大多数测试用例,可能都会涉及到初始化数据和清除测试数据的问题,最典型的就是数据库操作,这也是本文采用数据库操作作为案例的原因。那么如何实现呢?很显然在每个测试方法中都编写准备环境和清除测试数据的代码是不合适的,因为大多数时候对于一个测试类而言,准备环境和清除数据的逻辑都是一样的。聪明的你一定会想到定义两个方法,一个初始化环境,一个清除测试数据。是的,就是这样!
/**
* @author tao.youzt
*/
public class TestBizUrlDAO extends AbstractDependencyInjectionSpringContextTests {
private BizUrlDAO bizUrlDAO;
@Override
protected String[] getConfigLocations() {
return new String[]{"godzilla-dao.xml","godzilla-db.xml"};
}
protected void setupEnv(){
bizUrlDAO.delete("www.easyjf.com");
}
protected void cleanEnv(){
bizUrlDAO.delete("www.easyjf.com");
}
public void testTemp(){
setupEnv();
bizUrlDAO.insert(generateDO());
assertNotNull(bizUrlDAO.getByUrl("www.easyjf.com"));
setupEnv();
}
}
如你所见,我们在这里定义了setupEnv()和cleanEnv()两个方法,分别用于初始化环境和清除测试数据,然后在测试方法开始和结束时分别调用这两个方法。这的确达到了我们的目的,不用在每个测试方法中都编写初始化和清除逻辑!但此时你一定发现在每个测试方法前后都调用setupEnv()和cleanEnv()也很不爽,那说明我们的抽象程度还不够!那么该如何做的更好呢?
这里该到模板方法(Template Method)模式发挥威力的时候了。我们将使用模板方法来继续重构前面的案例。让我们先来定义一个方法:
/**
* @author tao.youzt
*/
public class TestBizUrlDAO extends AbstractDependencyInjectionSpringContextTests {
private BizUrlDAO bizUrlDAO;
@Override
protected String[] getConfigLocations() {
return new String[]{"godzilla-dao.xml","godzilla-db.xml"};
}
protected void setupEnv(){
bizUrlDAO.delete("www.easyjf.com");
}
protected void cleanEnv(){
bizUrlDAO.delete("www.easyjf.com");
}
public void testTemp(){
//do test logic in this method
execute();
}
protected void execute(){
setupEnv();
doTestLogic();
setupEnv();
}
}
相比之前的方法,我们这里已经有了一些进步,定义了一个execute方法,在该方法开始和结束分别执行初始化和清除逻辑,然后由doTestLogic()方法实现测试逻辑。实际测试方法中只要执行execute方法,并传入测试逻辑就可以了。瞧,不经意间我们已经实现了模板方法模式——把通用的逻辑封转起来,变化的部分由具体方法提供。怎么,不相信么?呵呵,设计模式其实并不复杂,就是前人解决通用问题的一些最佳实践总结而已。
此时你可能会说,TeseCase类已经提供了setUp()和tearDown()方法来做这件事情,我也想到了,哈哈!但这并不和本文产生冲突!
问题似乎越来越清晰,但我们遭遇了一条无法跨越的鸿沟——如何才能把测试逻辑传递到execute方法中呢?单靠传统的编程方法已经无法解决这个问题,因此我们必须寻找其他途径。
可能此时此刻你已经想到,本文另一个重要概念——回调方法模式还没有用到,是不是该使用该模式了?没错,就是它了!我先把代码给出,然后再详细解释。
我们提供了一个抽象类TestExecutor,并定义一个抽象的execute方法,然后为测试类的execute方法传入一个TestExecutor的实例,并调用该实例的execute方法。最后,我们的测试方法中只需要new一个TestExecutor,并在execute方法中实现测试逻辑,便可以按照预期的方式执行:准备测试环境-执行测试逻辑-清除测试数据。这便是一个典型的回调方法模式的应用!
模板方法和回调函数模式说起来挺悬,其实也就这么简单,明白了吧:)