单元测试系列之4:使用Unitils测试DAO层
Spring 的测试框架为我们提供一个强大的测试环境,解决日常单元测试中遇到的大部分测试难题:如运行多个测试用例和测试方法时,Spring上下文只需创建一次;数据库现场不受破坏;方便手工指定Spring配置文件、手工设定Spring容器是否需要重新加载等。但也存在不足的地方,基本上所有的Java应用都涉及数据库,带数据库应用系统的测试难点在于数据库测试数据的准备、维护、验证及清理。Spring 测试框架并不能很好地解决所有问题。要解决这些问题,必须整合多方资源,如DbUnit、Unitils、Mokito等。其中Unitils正是这样的一个测试框架。
数据库测试的难点
按照Kent Back的观点,单元测试最重要的特性之一应该是可重复性。不可重复的单元测试是没有价值的。因此好的单元测试应该具备独立性和可重复性,对于业务逻辑层,可以通过Mockito底层对象和上层对象来获得这种独立性和可重复性。而DAO层因为是和数据库打交道的层,其单元测试依赖于数据库中的数据。要实现DAO层单元测试的可重复性就需要对每次因单元测试引起数据库中的数据变化进行还原,也就是保护单元测试数据库的数据现场。
扩展Dbunit用Excel准备数据
在测试数据访问层(DAO)时,通常需要经过测试数据的准备、维护、验证及清理的过程。这个过程不仅烦锁,而且容易出错,如数据库现场容易遭受破坏、如何对数据操作正确性进行检查等。虽然Spring测试框架在这一方面为我们减轻了很多工作,如通过事务回滚机制来保存数据库现场等,但对测试数据及验证数据准备方面还没有一种很好的处理方式。Unitils框架出现,改变了难测试DAO的局面,它将SpringModule、DatabaseModule、DbUnitModule等整合在一起,使得DAO的单元测试变得非常容易。基于Unitils框架的DAO测试过程如图16-6所示。
以JUnit作为整个测试的基础框架,并采用DbUnit作为自动管理数据库的工具,以XML、Excel作为测试数据及验证数据准备,最后通过Unitils的数据集注解从Excel、XML文件中加载测试数据。使用一个注解标签就可以完成加载、删除数据操作。由于XML作为数据集易用性不如Excel,在这里就不对XML数据集进行讲解。下面我们主要讲解如何应用Excel作为准备及验证数据的载体,减化DAO单元测试。由于Unitils没有提供访问Excel的数据集工厂,因此需要编写插件支持Excel格式数据源。Unitils提供一个访问XML的数据集工厂MultiSchemaXmlDataSetFactory,其继承自DbUnit提供的数据集工厂接口DataSetFactory。我们可以参考这个XML数据集工厂类,编写一个访问Excel的数据集工厂MultiSchemaXlsDataSetFactory及Excel数据集读取器MultiSchemaXlsDataSetReader,然后在数据集读取器中调用Apache POI类库来读写Excel文件。如代码清单16-20所示。
import org.unitils.core.UnitilsException;import org.unitils.DbUnit.datasetfactory.DataSetFactory;import org.unitils.DbUnit.util.MultiSchemaDataSet;…public class MultiSchemaXlsDataSetFactory implements DataSetFactory {protected String defaultSchemaName;//① 初始化数据集工厂public void init(Properties configuration, String defaultSchemaName) {this.defaultSchemaName = defaultSchemaName;}//② 从Excel文件创建数据集public MultiSchemaDataSet createDataSet(File... dataSetFiles) {try {MultiSchemaXlsDataSetReader xlsDataSetReader = new MultiSchemaXlsDataSetReader(defaultSchemaName);return xlsDataSetReader.readDataSetXls(dataSetFiles);} catch (Exception e) {throw new UnitilsException("创建数据集失败: "+ Arrays.toString(dataSetFiles), e);}}//③ 获取数据集文件的扩展名public String getDataSetFileExtension() {return "xls";}}…import org.unitils.core.UnitilsException;import org.unitils.DbUnit.datasetfactory.DataSetFactory;import org.unitils.DbUnit.util.MultiSchemaDataSet;…// Excel数据集读取器public class MultiSchemaXlsDataSetReader {private String defaultSchemaName;public MultiSchemaXlsDataSetReader(String defaultSchemaName) {this.defaultSchemaName = defaultSchemaName;} // Excel数据集读取器public MultiSchemaDataSet readDataSetXls(File... dataSetFiles) {try {Map<String, List<ITable>> tableMap = getTables(dataSetFiles);MultiSchemaDataSet dataSets = new MultiSchemaDataSet();for (Entry<String, List<ITable>> entry : tableMap.entrySet()) {List<ITable> tables = entry.getValue();try {DefaultDataSet ds = new DefaultDataSet(tables.toArray(new ITable[] {}));dataSets.setDataSetForSchema(entry.getKey(), ds);} catch (AmbiguousTableNameException e) {throw new UnitilsException("构造DataSet失败!", e);}}return dataSets;} catch (Exception e) {throw new UnitilsException("解析EXCEL文件出错:", e);}}…}…import java.util.List;import org.hibernate.Session;import org.hibernate.SessionFactory;import org.springframework.orm.hibernate3.HibernateTemplate;import com.baobaotao.dao.UserDao;import com.baobaotao.domain.User;public class UserDaoImpl implements UserDao { //通过用户名获取用户信息public User findUserByUserName(String userName) {String hql = " from User u where u.userName=?";List<User> users = getHibernateTemplate().find(hql, userName);if (users != null && users.size() > 0)return users.get(0);elsereturn null;}//保存用户信息public void save(User user) {getHibernateTemplate().saveOrUpdate(user);}…}import javax.persistence.Column;import javax.persistence.Entity;…@Entity@GeneratedValue(strategy = GenerationType.IDENTITY)@Table(name = "t_user")public class User implements Serializable{@Id@Column(name = "user_id")protected int userId;@Column(name = "user_name")protected String userName;protected String password;@Column(name = "last_visit")protected Date lastVisit;@Column(name = "last_ip")protected String lastIp;@Column(name = "credits")private int credits;…}<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE hibernate-configuration PUBLIC"-//Hibernate/Hibernate Configuration DTD 3.0//EN""http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"><hibernate-configuration><session-factory><!--① SQL方言,这边设定的是HSQL --><property name="dialect">org.hibernate.dialect.HSQLDialect</property><!--② 数据库连接配置 --><property name="hibernate.connection.driver_class">org.hsqldb.jdbcDriver</property><property name="hibernate.connection.url">jdbc:hsqldb:data/sampledb</property><!--设置连接数据库的用户名--><property name="hibernate.connection.username">sa</property><!--设置连接数据库的密码--><property name="hibernate.connection.password"></property><!--③ 设置显示sql语句方便调试--><property name="hibernate.show_sql">true</property><!--④ 配置映射 --><property name="configurationClass">org.hibernate.cfg.AnnotationConfiguration</property><mapping /><mapping /></session-factory></hibernate-configuration>
#① 启用unitils所需模块 unitils.modules=database,dbunit,hibernate,spring#自定义扩展模块,详见实例源码unitils.module.dbunit.className=sample.unitils.module.CustomExtModule#② 配置数据库连接database.driverClassName=org.hsqldb.jdbcDriverdatabase.url=jdbc:hsqldb:data/sampledb;shutdown=truedatabase.userName=sadatabase.password=database.schemaNames=publicdatabase.dialect = hsqldb#③ 配置数据库维护策略.updateDataBaseSchema.enabled=true#④ 配置数据库表创建策略dbMaintainer.autoCreateExecutedScriptsTable=truedbMaintainer.script.locations=D:/masterSpring/chapter16/resources/dbscripts#⑤ 数据集加载策略#DbUnitModule.DataSet.loadStrategy.default=org.unitils.dbunit.datasetloadstrategy.InsertLoadStrategy #⑥ 配置数据集工厂DbUnitModule.DataSet.factory.default=sample.unitils.dataset.excel.MultiSchemaXlsDataSetFactoryDbUnitModule.ExpectedDataSet.factory.default=sample.unitils.dataset.excel.MultiSchemaXlsDataSetFactory#⑦ 配置事务策略DatabaseModule.Transactional.value.default=commit#⑧ 配置数据集结构模式XSD生成路径dataSetStructureGenerator.xsd.dirName=resources/xsd
…database.userName=sadatabase.password=database.schemaNames=public…

CREATE TABLE t_user (user_id INT generated by default as identity (start with 100),user_name VARCHAR(30),credits INT,password VARCHAR(32),last_visit timestamp,last_ip VARCHAR(23), primary key (user_id));CREATE TABLE t_login_log (login_log_id INT generated by default as identity (start with 1), user_id INT,ip VARCHAR(23),login_datetime timestamp,primary key (login_log_id));


import org.unitils.core.UnitilsException;import org.unitils.DbUnit.datasetfactory.DataSetFactory;import org.unitils.DbUnit.util.MultiSchemaDataSet;…@SpringApplicationContext( {"baobaotao-dao.xml" }) //① 初始化Spring容器public class UserDaoTest extends UnitilsJUnit4 {@SpringBean("jdbcUserDao") //② 从Spring容器中加载DAOprivate UserDao userDao; @Beforepublic void init() {} …}import org.unitils.core.UnitilsException;import org.unitils.DbUnit.datasetfactory.DataSetFactory;import org.unitils.DbUnit.util.MultiSchemaDataSet;…public class UserDaoTest extends UnitilsJUnit4 {…@Test //① 标志为测试方法@DataSet("BaobaoTao.Users.xls") //② 加载准备用户测试数据public void findUserByUserName() {User user = userDao.findUserByUserName("tony"); //③ 从数据库中加载tony用户assertNull("不存在用户名为tony的用户!", user);user = userDao.findUserByUserName("jan"); //④ 从数据库中加载jan用户assertNotNull("jan用户存在!", user);assertEquals("jan", user.getUserName()); assertEquals("123456",user.getPassword());assertEquals(10,user.getCredits()); }…}
import org.unitils.core.UnitilsException;import org.unitils.DbUnit.datasetfactory.DataSetFactory;import org.unitils.DbUnit.util.MultiSchemaDataSet;…public class UserDaoTest extends UnitilsJUnit4 {…@Test //① 标志为测试方法@ExpectedDataSet("BaobaoTao.ExpectedSaveUser.xls") //准备验证数据public void saveUser()throws Exception {User u = new User();u.setUserId(1);u.setUserName("tom");u.setPassword("123456");u.setLastVisit(getDate("2011-06-06 08:00:00","yyyy-MM-dd HH:mm:ss"));u.setCredits(30);u.setLastIp("127.0.0.1");userDao.save(u); //执行用户信息更新操作}…}
import org.unitils.core.UnitilsException;import org.unitils.DbUnit.datasetfactory.DataSetFactory;import org.unitils.DbUnit.util.MultiSchemaDataSet;import sample.unitils.dataset.util.XlsDataSetBeanFactory;…public class UserDaoTest extends UnitilsJUnit4 {…@Test //① 标志为测试方法@ExpectedDataSet("BaobaoTao.ExpectedSaveUser.xls") //准备验证数据public void saveUser()throws Exception {//② 从保存数据集中创建BeanUser u = XlsDataSetBeanFactory.createBean("BaobaoTao.SaveUser.xls” ,"t_user", User.class);userDao.save(u); //③ 执行用户信息更新操作}…}

import org.dbunit.dataset.Column;import org.dbunit.dataset.DataSetException;import org.dbunit.dataset.IDataSet;import org.dbunit.dataset.ITable;import org.dbunit.dataset.excel.XlsDataSet;…public class XlsDataSetBeanFactory {//从Excel数据集文件创建多个Beanpublic static <T> List<T> createBeans(String file, String tableName,Class<T> clazz) throws Exception {BeanUtilsBean beanUtils = createBeanUtils();List<Map<String, Object>> propsList = createProps(file, tableName);List<T> beans = new ArrayList<T>();for (Map<String, Object> props : propsList) {T bean = clazz.newInstance();beanUtils.populate(bean, props);beans.add(bean);}return beans;}//从Excel数据集文件创建多个Beanpublic static <T> T createBean(String file, String tableName, Class<T> clazz)throws Exception {BeanUtilsBean beanUtils = createBeanUtils();List<Map<String, Object>> propsList = createProps(file, tableName);T bean = clazz.newInstance();beanUtils.populate(bean, propsList.get(0));return bean;}…}