DbUnit简介
为依赖于其他外部系统(如数据库或其他接口)的代码编写单元测试是一件很困难的工作。在这种情况下,有效的单元必须隔离测试对象和外部依赖,以便管理测试对象的状态和行为。
使用mock object对象,是隔离外部依赖的一个有效方法。如果我们的测试对象是依赖于DAO的代码,mock
object技术很方便。但如果测试对象变成了DAO本身,又如何进行单元测试呢?
开源的DbUnit项目,为以上的问题提供了一个相当优雅的解决方案。使用DbUnit,开发人员可以控制测试数据库的状态。进行一个DAO单元测试之前,DbUnit为数据库准备好初始化数据;而在测试结束时,DbUnit会把数据库状态恢复到测试前的状态。
下面的例子使用DbUnit为iBATIS SqlMap的DAO编写单元测试。
准备测试数据
首先,要为单元测试准备数据。使用DbUnit,我们可以用XML文件来准备测试数据集。下面的XML文件称为目标数据库的Seed
File,代表目标数据库的表名和数据,它为测试准备了两个Employee的数据。employee对应数据库的表名,employee_uid、start_date、first_name和last_name都是表employee的列名。
<?xml
version="1.0" encoding="GB2312"?>
<dataset>
<employee employee_uid="0001"
start_date="2001-01-01"
first_name="liutao"
last_name="liutao"
/>
<employee employee_uid="0002"
start_date="2001-04-01"
first_name="wangchuang"
last_name="wangchuang"
/>
</dataset>
缺省情况下,DbUnit在单元测试开始之前删除Seed
File中所有表的数据,然后导入Seed File的测试数据。在Seed File中不存在的表,DbUnit则不处理。
Seed File可以手工编写,也可以用程序导出现有的数据库数据并生成。
SqlMap代码
我们要测试的SqlMap映射文件如下所示:
<select id="queryEmployeeById" parameterClass="java.lang.String"
resultClass="domain.Employee">
select employee_uid as userId,
start_date as
startDate,
first_name as
firstName,
last_name as lastName
from EMPLOYEE where employee_uid=#value#
</select>
<delete id="removeEmployeeById" parameterClass="java.lang.String">
delete from EMPLOYEE where employee_uid=#value#
</delete>
<update id="updateEmpoyee" parameterClass="domain.Employee">
update EMPLOYEE
set start_date=#startDate#,
first_name=#firstName#,
last_name=#lastName#
where employee_uid=#userId#
</update>
<insert id="insertEmployee" parameterClass="domain.Employee">
insert into employee (employee_uid,
start_date, first_name,
last_name)
values (#userId#,
#startDate#, #firstName#, #lastName#)
</insert>
编写DbUnit TestCase
为了方便测试,首先为SqlMap的单元测试编写一个抽象的测试基类,代码如下。
public abstract
class BaseSqlMapTest extends DatabaseTestCase {
protected static SqlMapClient sqlMap;
protected IDatabaseConnection getConnection() throws
Exception {
return new DatabaseConnection(getJdbcConnection());
}
protected void setUp() throws Exception
{
super.setUp();
init();
}
protected void tearDown() throws
Exception {
super.tearDown();
getConnection().close();
if (sqlMap !=
null) {
DataSource ds = sqlMap.getDataSource();
Connection conn = ds.getConnection();
conn.close();
}
}
protected void init() throws Exception
{
initSqlMap("sqlmap/SqlMapConfig.xml",
null);
}
protected SqlMapClient getSqlMapClient()
{
return sqlMap;
}
protected void initSqlMap(String
configFile, Properties props)
throws Exception {
Reader reader
= Resources.getResourceAsReader(configFile);
sqlMap = SqlMapClientBuilder.buildSqlMapClient(reader,
props);
reader.close();
}
protected void initScript(String
script) throws Exception {
DataSource ds
= sqlMap.getDataSource();
Connection conn
= ds.getConnection();
Reader reader
= Resources.getResourceAsReader(script);
ScriptRunner runner
= new ScriptRunner();
runner.setStopOnError(false);
runner.setLogWriter(null);
runner.setErrorLogWriter(null);
runner.runScript(conn,
reader);
conn.commit();
conn.close();
reader.close();
}
private Connection getJdbcConnection()
throws Exception {
Properties props
= new Properties();
props.load(Resources.getResourceAsStream("sqlmap/SqlMapConfig.properties"));
Class driver =
Class.forName(props.getProperty("driver"));
Connection conn
= DriverManager.getConnection(props.getProperty("url"),
props.getProperty("username"),
props.getProperty("password"));
return conn;
}
}
然后为每个SqlMap映射文件编写一个测试用例,extends上面的抽象类。如编写Employ.xml的测试用例如下,它覆盖了DbUnit的DatabaseTestCase类的getDataSet方法。
public class
EmployeeDaoTest extends BaseSqlMapTest {
protected IDataSet getDataSet() throws Exception {
Reader reader = Resources.getResourceAsReader("config/employee_seed.xml");
return new FlatXmlDataSet(reader);
}
public void testQueryEmpoyeeById()
throws Exception {
String id = "0001";
Employee emp =
(Employee)sqlMap.queryForObject("queryEmployeeById",
id);
assertNotNull(emp);
assertEquals("0001",
emp.getUserId());
assertEquals("liutao",
emp.getFirstName());
}
public void testRemoveEmployeeById()
throws Exception {
String id = "0001";
int num = sqlMap.delete("removeEmployeeById",
id);
assertEquals(1,
num);
// 注意这里, 确认删除不能使用SqlMap的查询, 很奇怪!
ITable table = getConnection().createQueryTable("removed",
"select * from employee where employee_uid='0001'");
assertEquals(0, table.getRowCount());
}
public void testUpdateEmployee()
throws Exception {
String id = "0002";
Employee emp =
(Employee)sqlMap.queryForObject("queryEmployeeById",
id);
emp.setLastName("wch");
sqlMap.update("updateEmpoyee",
emp);
Employee emp1
= (Employee)sqlMap.queryForObject("queryEmployeeById",
id);
assertEquals("wch",
emp1.getLastName());
}
public void testInsertEmployee()
throws Exception {
Employee emp =
new Employee();
emp.setUserId("0005");
emp.setStartDate("2003-09-09");
emp.setFirstName("macy");
emp.setLastName("macy");
sqlMap.insert("insertEmployee",
emp);
Employee emp1
= (Employee)sqlMap.queryForObject("queryEmployeeById",
"0005");
assertEquals(emp.getFirstName(),
emp1.getFirstName());
assertEquals(emp.getStartDate(),
emp1.getStartDate());
}
}
以上例子中的绿色代码部分使用ITable接口来查询已删除的数据。因为使用SqlMapClient.queryForObject方法查询,已删除的数据还存在,真奇怪(有时间再研究)。
DbUnit的断言
我们可以使用DbUnit的Assertion类的方法来比较数据是否相同。
public class
Assertion {
public static void assertEquals(ITable
expected, ITable actual)
public static void assertEquals(IDataSet
expected, IDataSet actual)
}
DatabaseTestCase的getSetUpOperation和getTearDownOperation方法
缺省情况下,DbUnit执行每个测试前,都会执行CLEAN_INSERT操作,删除Seed
File中所有表的数据,并插入文件的测试数据。你可以通过覆盖getSetUpOperation和getTearDownOperation方法改变setUp和tearDown的行为。
protected DatabaseOperation
getSetUpOperation() throws Exception {
return DatabaseOperation.REFRESH;
}
protected DatabaseOperation getTearDownOperation() throws
Exception {
return DatabaseOperation.NONE;
}
REFRESH操作执行测试前并不执行CLEAN操作,只是导入文件中的数据,如果目标数据库数据已存在,DbUnit使用文件的数据来更新数据库。
使用Ant
上面的方法通过extends
DbUnit的DatabaseTestCase来控制数据库的状态。而 使用DbUnit的Ant
Task,完全可以通过Ant脚本的方式来实现。
<taskdef
name="dbunit" classname="org.dbunit.ant.DbUnitTask"/>
<!-- 执行set up 操作
-->
<dbunit driver="org.hsqldb.jdbcDriver"
url="jdbc:hsqldb:hsql://localhost/xdb"
userid="sa" password="">
<operation type="INSERT" src="employee_seed.xml"/>
</dbunit>
<!-- run all tests
in the source tree -->
<junit printsummary="yes"
haltonfailure="yes">
<formatter
type="xml"/>
<batchtest
fork="yes" todir="${reports.tests}">
<fileset dir="${src.tests}">
<include name="**/*Test*.java"/>
</fileset>
</batchtest>
</junit>
<!-- 执行tear down
操作 -->
<dbunit driver="org.hsqldb.jdbcDriver"
url="jdbc:hsqldb:hsql://localhost/xdb"
userid="sa" password="">
<operation type="DELETE" src="employee_seed.xml"/>
</dbunit>
以上的Ant脚本把junit
task放在DbUnit的Task中间,可以达到控制数据库状态的目标。
由此可知,DbUnit可以灵活控制目标数据库的测试状态,从而使编写SqlMap单元测试变得更加轻松。
本文抄袭了资源列表的“Effective
Unit Test with DbUnit”,但重新编写了代码示例。
网上资源
1、DbUnit
Framework
2、Effective
Unit Testing with DbUnit
3、Control
your test-environement with DbUnit and Anthill |