http://starrynight.blogdriver.com/starrynight/621943.html
DbUnit簡介
為依賴于其他外部系統(tǒng)(如數(shù)據(jù)庫或其他接口)的代碼編寫單元測試是一件很困難的工作。在這種情況下,有效的單元必須隔離測試對象和外部依賴,以便管理測試對象的狀態(tài)和行為。
使用mock object對象,是隔離外部依賴的一個有效方法。如果我們的測試對象是依賴于DAO的代碼,mock object技術(shù)很方便。但如果測試對象變成了DAO本身,又如何進行單元測試呢?
開源的DbUnit項目,為以上的問題提供了一個相當優(yōu)雅的解決方案。使用DbUnit,開發(fā)人員可以控制測試數(shù)據(jù)庫的狀態(tài)。進行一個DAO單元測試之前,DbUnit為數(shù)據(jù)庫準備好初始化數(shù)據(jù);而在測試結(jié)束時,DbUnit會把數(shù)據(jù)庫狀態(tài)恢復到測試前的狀態(tài)。
下面的例子使用DbUnit為iBATIS SqlMap的DAO編寫單元測試。
準備測試數(shù)據(jù)
首先,要為單元測試準備數(shù)據(jù)。使用DbUnit,我們可以用XML文件來準備測試數(shù)據(jù)集。下面的XML文件稱為目標數(shù)據(jù)庫的Seed File,代表目標數(shù)據(jù)庫的表名和數(shù)據(jù),它為測試準備了兩個Employee的數(shù)據(jù)。employee對應數(shù)據(jù)庫的表名,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中所有表的數(shù)據(jù),然后導入Seed File的測試數(shù)據(jù)。在Seed File中不存在的表,DbUnit則不處理。
Seed File可以手工編寫,也可以用程序?qū)С霈F(xiàn)有的數(shù)據(jù)庫數(shù)據(jù)并生成。
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接口來查詢已刪除的數(shù)據(jù)。因為使用SqlMapClient.queryForObject方法查詢,已刪除的數(shù)據(jù)還存在,真奇怪(有時間再研究)。
DbUnit的斷言
我們可以使用DbUnit的Assertion類的方法來比較數(shù)據(jù)是否相同。
public class Assertion {
??? public static void assertEquals(ITable expected, ITable actual)
??? public static void assertEquals(IDataSet expected, IDataSet actual)
}
DatabaseTestCase的getSetUpOperation和getTearDownOperation方法
缺省情況下,DbUnit執(zhí)行每個測試前,都會執(zhí)行CLEAN_INSERT操作,刪除Seed File中所有表的數(shù)據(jù),并插入文件的測試數(shù)據(jù)。你可以通過覆蓋getSetUpOperation和getTearDownOperation方法改變setUp和tearDown的行為。
protected DatabaseOperation getSetUpOperation() throws Exception {
??? return DatabaseOperation.REFRESH;
}
protected DatabaseOperation
getTearDownOperation()
throws Exception {
???
return DatabaseOperation.NONE;
}
REFRESH操作執(zhí)行測試前并不執(zhí)行CLEAN操作,只是導入文件中的數(shù)據(jù),如果目標數(shù)據(jù)庫數(shù)據(jù)已存在,DbUnit使用文件的數(shù)據(jù)來更新數(shù)據(jù)庫。
使用Ant
上面的方法通過extends DbUnit的DatabaseTestCase來控制數(shù)據(jù)庫的狀態(tài)。而
使用DbUnit的Ant Task,完全可以通過Ant腳本的方式來實現(xiàn)。
<taskdef name="dbunit" classname="org.dbunit.ant.DbUnitTask"/>
<!-- 執(zhí)行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>
<!-- 執(zhí)行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中間,可以達到控制數(shù)據(jù)庫狀態(tài)的目標。
由此可知,DbUnit可以靈活控制目標數(shù)據(jù)庫的測試狀態(tài),從而使編寫SqlMap單元測試變得更加輕松。
本文抄襲了資源列表的“Effective Unit Test with DbUnit”,但重新編寫了代碼示例。
網(wǎng)上資源
1、DbUnit Framework
2、Effective Unit Testing with DbUnit
3、Control your test-environement with DbUnit and Anthill