浏览 5975 次
锁定老帖子 主题:数据访问层(Dao)的单元测试实践
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2012-08-02
最后修改:2012-08-02
Dao层主要工作是数据库访问,是非常重要的模块。为了保证SQL的正确执行,单元测试是必须的。但是一直以来Dao层的单元测试很难进行,主要因为几个问题 1、 单元测试必须是执行隔离的环境代码,而隔离数据库非常困难,不得不放弃这个念头。所以Dao层需要和数据库直接打交道,但是单元测试要求每次重复的动作结果都是一致,但是由于外部数据库环境的问题,测试环境无法稳定。 2、 现阶段的Dao层一般都会利用Spring的容器组装Dao对象,在辅以一些Support对象。这样的结果就是没有Spring容器,无法测试Dao。 3、 每个测试之前,数据库必须处于一个稳定的已知的状态,这就需要数据准备,而单元测试的数据如果要手工插入到数据库中,工作量过大。 4、 测试用例必须有断言,我们需要通过断言来判断数据是否插入数据,每个字段是否相同,这个如果没有辅助工具,而手工去一个个断言,工作量不能接受的 2. 解决方案 为了解决以上问题,我们选择了Unitils来集成Spring、Dbunit等,完成Dao层的单元测试工作,并和Maven工程结合完成配置。 Unitils的使用很简单,以下面的一个例子来说明。 Dao的代码很简单,是查询、更新一个用户的账户信息 public class AccountDao extends JdbcDaoSupport { public Account getAccount(String accountId) { List<Account> list = null; list = getJdbcTemplate().query("select account_id,balance from tb_account where account_id=?", new Object[] { accountId }, new RowMapper<Account>() { @Override public Account mapRow(ResultSet rs, int rowNum) throws SQLException { Account acc = new Account(); acc.setAccountId(rs.getString("account_id")); acc.setBalance(rs.getInt("balance")); return acc; } }); if (list.size() > 0) { return list.get(0); } else { return null; } } public int updateAccount(String accountId, int balance) { int ret = getJdbcTemplate().update("update tb_account set balance = ? where account_id =?", new Object[] { balance, accountId }); return ret; } } 一、Maven的POM文件修改 在Dao工程中的POM文件加入如下 <dependency> <groupId>org.unitils</groupId> <artifactId>unitils-dbunit</artifactId> <version>${unitils.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.unitils</groupId> <artifactId>unitils-spring</artifactId> <version>${unitils.version}</version> <scope>test</scope> </dependency> unitils.version目前最新的为3.3版本 二、Unitils的环境配置 Unitils的启动,需要一个配置文件unitils.properties,这个文件默认需要放到classpath下,我们一般为test/resources/unitils.properties文件。文件内容如下 database.driverClassName=com.mysql.jdbc.Driver database.url=jdbc:mysql://192.168.100.242:3306/test database.userName=mantis database.password=mantispw database.schemaNames=test database.dialect=mysql DatabaseModule.Transactional.value.default=rollback database.driverClassName为测试数据库的Jdbc驱动 database.url为测试数据库的连接串 database.userName为测试数据库用户名 database.schemaNames为测试数据库的schema,mysql可以不需要,Oracle必填。 database.dialect填写为数据库类型,取值有mysql,oracle,derby等 DatabaseModule.Transactional.value.default指的是单元测试对数据库的修改的事务策略,有rollback,disable,commit等选择,我们一般选择回滚 rollback 三、Spring的集成 Unitils提供了Spring的集成功能,可以在单元测试中让Spring组装我们的Dao,自动注入依赖的DataSource等。 针对Spring集成,我们需要些前置条件 1、 将Dao依赖的Spring配置,包括Property解析、DataSource、事务管理等主要是一些基础配置放到Maven工程的test/resources/testapplication/appContext-common.xml中。 <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE" /> <property name="ignoreResourceNotFound" value="false" /> <property name="ignoreUnresolvablePlaceholders" value="true" /> <property name="locations"> <list> <value>classpath:testapplication/config.properties</value> </list> </property> </bean> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName"> <value>${jdbc.driverClassName}</value> </property> <property name="url"> <value>${jdbc.url}</value> </property> <property name="username"> <value>${jdbc.username}</value> </property> <property name="password"> <value>${jdbc.password}</value> </property> <property name="maxActive"> <value>${jdbc.maxActive}</value> </property> <property name="maxIdle"> <value>${jdbc.maxIdle}</value> </property> <property name="initialSize"> <value>${jdbc.maxIdle}</value> </property> <property name="maxWait"> <value>18000</value> </property> <property name="defaultAutoCommit"> <value>false</value> </property> </bean> <!-- 事务管理器配置,单数据源事务 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> 之所以单独把单元测试的基础Spring配置抽出来是因为这里注入的数据源和事务管理器,都用最简单的单数据库事务,简化单元测试的环境。避免实际开发中多数据源事务的问题影响结果。 2、 使用Unitils的Spring启动替换功能,将Spring中的正常的DataSource换为Unitils自身的DataSource。这样做的好处是数据准备的操作和业务sql在一个事务中进行,可以方便一起回滚,不对数据库造成影响。替换的DataSource也是一个Spring配置文件,放到test/resources/testapplication/testDataSource.xml中。 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd"> <bean id="dataSource" class="org.unitils.database.UnitilsDataSourceFactoryBean" /> </beans> 三、测试数据的准备 可以根据dbunit的xml格式准备测试数据,通过执行ExportData这个对象来导出现有测试库的数据,在命令行里面输入要导出的表名,即可把当前测试数据库的现有数据导出为xml 测试的xml文件默认放到test/resources下和测试的代码相同的package中,比如test/resources/com/xxx/dao/下。 四、单元测试用例的编写 单元测试用例需要继承UnitilsJUnit3这个基类,顾名思义这个测试套件是依赖Junit3的。Unitils另外也提供了UnitilsJUnit4的基类。下面是我们的一个测试样例代码 public class AccountDaoTest extends UnitilsJUnit3 { @SpringApplicationContext({ "classpath:testapplication/appContext-common.xml", "classpath:testapplication/testDatasource.xml", "classpath:META-INF/spring/applicationContext-*.xml" }) protected ApplicationContext applicationContext; @SpringBeanByType private AccountDao accountDao; @DataSet("ACCOUNT.xml") public void testGetAccount() { Account account = accountDao.getAccount("S31993k"); System.out.println(JSON.toJSON(account)); assertEquals(100, account.getBalance()); } @DataSet("ACCOUNT.xml") public void testGetAccountNull() { Account account = accountDao.getAccount("23"); assertEquals(null, account); } @DataSet("ACCOUNT.xml") @ExpectedDataSet("ACCOUNT_NEW.xml") public void testUpdateAccount() { accountDao.updateAccount("S31993k", 35); } } protected ApplicationContext applicationContext; 是Spring上下文加载后的变量。上面的@ SpringApplicationContext是指明要加载的Spring配置文件到一个变量,可以通过一个String数组和通配符加载多个配置,可以看到这里我们把common和测试数据源的testDatasource.xml都加载了。如果一个工程中,可以抽象出一个公用的测试基类,将Spring的上下文保存在基类中。 private AccountDao accountDao; 是我们要测试的目标对象,这里需要在前面加上@SpringBeanByType的标注,这样Unitils会自动根据类型,将目标对象从Spring上下文取出,注入到测试代码中 public void testUpdateAccount () 是测试插入的方法,其上的 @DataSet("ACCOUNT.xml") 指的是插入前的预制数据,Unitils和Dbunit会在执行该方法前,将EMPTY_TABLE.xml中的数据导入到数据库中,下面给出的是一个样例数据的xml。执行前,Unitils会清空该表,然后插入指定的测试数据。注:由于事务最后回滚,清空的动作不会提交,所以不用担心数据的损失。 <?xml version='1.0' encoding='UTF-8'?> <dataset> <tb_account account_id="S31993k" balance="100"/> </dataset> @ExpectedDataSet("ACCOUNT_NEW.xml") 为执行测试用例后的期望数据,Unitils会比较实际结果和期望值,看看是否一致。如果不一致则抛出测试失败。ACCOUNT_NEW.xml的内容如下 <?xml version='1.0' encoding='UTF-8'?> <dataset> <tb_account account_id="S31993k" balance="35"/> </dataset> public void testGetAccount()是查询一个已经存在的帐户。最后通过断言查询是否存在编号为“S31993k”。注:每次执行一个用例方法的时候,DBUnit都会重新初始化相关的数据表,所以不用担心前面的测试用例操作会影响当前的用例结果 public void testGetAccountNull 这是一个尝试查询一个不存在的帐户信息,最后Dao应该返回的是Null,这里用断言做了判断。 3. 经验总结 Unitils通过整合Dbunit和Spring,让我们单元测试的工作量小而效果好。 下面是做Dao单元测试的经验 1、 尽量让单元测试的数据库和其他测试的数据库分离开,避免相互影响。如果每个开发人员拥有自己的单元测试库那是最好的。 2、 Dbunit需要的数据集可以预先从数据库生成,而后面的测试可以重复利用这些数据集。数据集尽量小,保证只包含影响测试的数据。 3、 数据库的外键是单元测试的障碍,建议去掉外键。数据的完整性应该靠应用程序保证。 4、 没有断言就不能称作单元测试,单元测试一定要自动化,而不是靠人眼判断。 5、 如果Dao的单元测试会涉及到多个表(这种情况比较罕见),可以在一个xml中放置多个表的数据,Dbunit会自动识别导入到不同的表中。 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |