DbUnit是測試數據庫的利器,不過要想弄明白還是需要一番研究。好在它的源代碼不多,文檔也還算全。我就在此做一個總結吧。
DbUnit.NET是DbUnit的.NET版,不過只推出了alpha版,而且自從06年以后就不再更新了。Stack Overflow上有一個帖子,提出了一些替代方案。
現在的DbUnit要求在測試時繼承DBTestCase
,而不是之前的DatabaseTestCase
(前者繼承自后者,而后者繼承了junit的TestCase
)。DatabaseTestCase
包含兩個抽象方法,getConnection()
和getDataSet()
,前者用來獲取數據庫連接,后者獲取要測試的數據集。
數據集
DbUnit可以把所有表的記錄存在一個數據集中:既可以是數據庫中的表,也可以是文件中的數據。我們在此用FlatXmlDataSet
來演示。
順便提一句,DbUnit中還存在另一種格式的數據集XmlDataSet
,它們的區別如下:
在FaltXmlDataSet
對應的XML文件里,元素名稱對應數據庫表名,元素的屬性(attribute)對應表的列。如:
<dataset>
<Person Name="Kirin" Age="31" Location="Beijing"/>
<Person Name="Jade" Age="30"/>
</dataset>
要注意,如果數據庫中某一條字段為null,在flat XML中將不會顯示該attribute。另外,FlatXmlDataSet
用XML文件中該表的第一行數據來制定表的結構。因此,如果數據庫中某個字段所有記錄都為null,或者恰巧第一條記錄為null,那么得到的表結構與原數據庫的表結構就不一致了,測試就會失敗。FlatXmlDataSet中存在一個column sensing的概念,在從文件加載數據時,將該屬性設置為true,就會根據第一行展現出來的表結構,自動將別的行的列補齊。
在XmlDataSet
對應的XML文件里,用元素的子元素對應表的列。如:
<dataset>
<Person>
<Name>Kirin</Name>
<Age>31</Age>
<Location>Beijing</Location>
</Person>
<Person>
<Name>Jade</Name>
<Age>30</Age>
<Location/>
</Person>
</dataset>
null用空元素來表示。
將數據庫導出到XML文件
我們可以手寫XML來准備數據,也可以從數據庫中導出現有的數據,用FlatXmlDataSet.write()
靜態方法即可,例如:
QueryDataSet dataSet = new QueryDataSet(connection);
dataSet.addTable(TABLE_NAME, "select * from " + TABLE_NAME);
dataSet.addTable(...);
FlatXmlDataSet.write(dataSet, new FileOutputStream("data.xml"));
重寫getDataSet
有了文件數據,我們就需要重寫getDataSet()
,讓它加載文件中的數據並返回。
@Override
protected IDataSet getDataSet() throws Exception {
// set column sensing as true, so it can dynamically and columns with null value.
return new FlatXmlDataSetBuilder()
.setColumnSensing(true)
.build(new FileInputStream("data.xml"));
}
IDatabaseTester
DBTestCase
重寫了getConnection()
,並把它設置為final,將獲取connection的操作委托給IDatabaseTester
,我們可以通過重寫getDatabaseTester()
方法來設置具體的IDatabaseTester
。Dbunit中,IDatabaseTester
的實現類一共有四個:
- DefaultDatabaseTester
- JdbcDatabaseTester
- DataSourceDatabaseTester
- JndiDatabaseTester
它們的用途不言自明。
DatabaseTestCase
重寫了TestCase
里的setUp()
和tearDown()
方法。
protected void setUp() throws Exception
{
super.setUp();
final IDatabaseTester databaseTester = getDatabaseTester();
assertNotNull( "DatabaseTester is not set", databaseTester );
databaseTester.setSetUpOperation( getSetUpOperation() );
databaseTester.setDataSet( getDataSet() );
databaseTester.setOperationListener(getOperationListener());
databaseTester.onSetup();
}
protected void tearDown() throws Exception
{
try {
final IDatabaseTester databaseTester = getDatabaseTester();
assertNotNull( "DatabaseTester is not set", databaseTester );
databaseTester.setTearDownOperation( getTearDownOperation() );
databaseTester.setDataSet( getDataSet() );
databaseTester.setOperationListener(getOperationListener());
databaseTester.onTearDown();
} finally {
tester = null;
super.tearDown();
}
}
可以看出它們的大體意圖:為tester設置操作、數據集和監聽器,然后執行相應的操作。獲取數據集的是抽象方法,需要我們來實現。監聽器主要負責在得到數據連接或setUp、tearDown結束后執行的操作,使用默認實現即可。我們主要來說說getSetUpOperation
和getTearDownOperation
返回的DatabaseOperation
。
DatabaseOperation
DatabaseOperation
定義了對數據庫進行的操作,它是一個抽象類,通過靜態字段提供了幾種內置的實現:
- NONE:不執行任何操作,是
getTearDownOperation
的默認返回值。 - UPDATE:將數據集中的內容更新到數據庫中。它假設數據庫中已經有對應的記錄,否則將失敗。
- INSERT:將數據集中的內容插入到數據庫中。它假設數據庫中沒有對應的記錄,否則將失敗。
- REFRESH:將數據集中的內容刷新到數據庫中。如果數據庫有對應的記錄,則更新,沒有則插入。
- DELETE:刪除數據庫中與數據集對應的記錄。
- DELETE_ALL:刪除表中所有的記錄,如果沒有對應的表,則不受影響。
- TRUNCATE_TABLE:與DELETE_ALL類似,更輕量級,不能rollback。
- CLEAN_INSERT:是一個組合操作,是DELETE_ALL和INSERT的組合。是
getSetUpOeration
的默認返回值。
由此我們可以總結出,在一個測試執行前后,DbUnit會為我們做哪些工作:
- 移除數據庫中的所有記錄(CLEAN_INSERT中的DELETE_ALL)。
- 將數據集中的數據加載到數據庫中(CLEAN_INSERT中的INSERT)。
- 運行測試。
- 測試運行完畢后,不執行任何操作。
我們可以根據需要,在測試類中重寫setUp
和tearDown
,以實現定制的需求。比如,數據庫中已經有一些數據,我們不希望數據集中的數據對它們產生任何影響,這時可以先將數據庫中的數據備份到內存中,等測試完成后再恢復到數據庫中,代碼如下:
private IDataSet dataSetBackup;
private static final String[] TABLE_NAMES = new String[] { "..." };
@Override
protected void setUp() throws Exception {
dataSetBackup = new CachedDataSet(getConnection().createDataSet(TABLE_NAMES));
super.setUp();
}
@Override
protected void tearDown() throws Exception {
try {
final IDatabaseTester databaseTester = getDatabaseTester();
assertNotNull( "DatabaseTester is not set", databaseTester );
databaseTester.setTearDownOperation( getTearDownOperation() );
databaseTester.setDataSet( dataSetBackup ); // 這里不使用getDataSet(),而是使用備份的數據庫中數據
databaseTester.setOperationListener(getOperationListener());
databaseTester.onTearDown();
} finally {
tester = null;
dataSetBackup = null;
//super.tearDown(); // 這里不再調用基類的tearDown
}
}
@Override
protected DatabaseOperation getTearDownOperation() {
return DatabaseOperation.CLEAN_INSERT;
}
測試前用CLEAN_INSERT,是用數據集覆蓋數據庫,測試后用CLEAN_INSERT,使用備份的數據庫覆蓋之前插入到數據庫中的數據集。
完整的基類代碼在這里。
好了,現在可以開始測試了。