在數據庫代碼測試中,一般情況使用2種方案:
一是使用mock objects;
二是使用DbUnit。
mock objects基於物理隔離層的概念,將涉及到數據庫操作的代碼,全用虛擬對象代替。這種方案,對業務領域裡的代碼來講是可行的,也比較方便,但對於數據庫操作層,此方案無用武之地,因為我們必須實實在在地與數據庫打交道。
而在數據庫測試中,因為我們力求將每個TestCase中眾多的測試方法完全隔離起來,不會因為一個測試方法因測試增加、刪除功能而影響到另一個測試方法,這樣,在每一個測試之前,數據庫的狀態是否穩定,甚至是完全不變,就顯得很重要了。而這點,正是數據庫測試的難點。
Dbunit解決了這個問題。其原理很簡單,就是在每個測試方法之前後,通過增刪一些固定的記錄,保持了數據庫的固定狀態,由此,我們可以在每個測試方法中自由地增刪記錄,而不用擔心會影響到別的測試方法。
但Dbunit也有一個問題,即它不能刪除非空的外鍵記錄。舉例來說,假設“員工”表中有一非空字段為“部門編號”,引用了“部門”表的id, 只要“員工”表存在任一記錄,“部門”表將不能被刪除,強行刪除將出現違犯約束(constraint violation)的異常。當然,如果必要,我們可以將數據庫的約束條件改為連鎖刪除,這樣,一旦我們刪除一名員工記錄,其所在的部門記錄也將從“部門”表中刪除。而此又會導致“員工”表中所有該部門的員工全被刪除。這是絕對不允許的。當然,作為測試,我們可以先刪除“員工”表,再刪除“部門”表。
但有時,某些表自己引用自己,如“組織”表中有一“上級組織編號”字段,是自己“組織編號”的外鍵,即,此字段引用了本表中其他記錄的“組織編號”。此時,我們必須先將這些引用了其他記錄的“組織編號”的記錄先刪除,才能刪除此表中的其他記錄。而Dbunit在實現上,只是用了一個簡單的"delete from ..."的SQL語句,不能解決這個問題。
Dbunit的原理是如此簡單,我們完全可以設計的“Dbunit”,通過多重循環語句,干脆利落地刪除自引用的整表。我們的“Dbunit”,可以命名為“SqlRunner”。
package com.sarkuya.util.database;
import Java.sql.Connection;
import Java.sql.DriverManager;
import Java.sql.ResultSet;
import Java.sql.SQLException;
import Java.sql.Statement;
public class SqlRunner {
static {
try {
Class.forName("org.hsqldb.jdbcDriver");
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
}
public static void executeUpdate(String sql) {
Connection conn;
Statement stmt;
try {
conn = DriverManager.getConnection("jdbc:hsqldb:mem:testingdb", "sa", "");
stmt = conn.createStatement();
stmt.executeUpdate(sql);
stmt.close();
conn.close();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
public static boolean isUndeletableForSelfReference (String 表名, String 字段名) {
Connection conn;
Statement stmt;
boolean result = true;
try {
conn = DriverManager.getConnection("jdbc:hsqldb:mem:testingdb", "sa", "");
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select count(*) from " + 表名 + " where " + 字段名 + " is not null");
rs.next();
if (rs.getInt(1) != 0) {
result = true;
}
else {
result = false;
}
rs.close();
stmt.close();
conn.close();
} catch (SQLException ex) {
ex.printStackTrace();
}
return result;
}
可以看出,我們使用了JDBC的SQL語句,而不是Hibernate語句。Hibernate的粉絲們可能大為不滿,為何不使用Hiberante? 別急,Hibernate的語句將被大量地應用於實際測試當中。但是根據測試先行的原則,任何一個基於Hiberante的語句都必須先測試再使用。而我們的這個“Dbunit”是運行在實際測試之前,無法經過測試。當然,我們可以先假定這段Hiberante代碼正確無誤,然後再實際測試它。這種方法也有一個缺點,因為測試代碼常常會因為重構而發生改變,當測試代碼改變時,這個“Dbunit”也將被迫發生改變。而用JDBC的SQL語句,可保持這段代碼相對獨立,不至於連誅九族。
executeUpdate()將執行“insert”、“delete”語句。重點在於isUndeletableForSelfReference()方法。此方法在某個表的某個字段非空時,會返回false,告訴我們,此表中尚有被引用的記錄存在,從而不能刪除此表。盡管只有兩個方法,但對於我們的“Dbunit”來講,已經足夠了。
在TestCase的setUp()中,我們利用其executeUpdate來增加一些必須的記錄。
protected void setUp() throws Exception {
SqlRunner.executeUpdate("insert into 組織分類 values(1, '教育系統')");
SqlRunner.executeUpdate("insert into 組織分類 values(2, '商貿系統')");
SqlRunner.executeUpdate("insert into 組織分類 values(3, '供應商家')");
SqlRunner.executeUpdate("insert into 組織分類 values(4, '政府')");
SqlRunner.executeUpdate("insert into 組織 values(1, '中國貿易部', '北京三環路558號', 2, null)");
SqlRunner.executeUpdate("insert into 組織 values(2, '北京貿易廳', '北京四環路8號', 2, 1)");
SqlRunner.executeUpdate("insert into 組織 values(3, '河北高科技技術服務有限公司', '石家莊市白龍路23號', 3, null)");
SqlRunner.executeUpdate("insert into 組織 values(4, '四川珠寶有限公司', '成都市藍天路56號', 3, null)");
SqlRunner.executeUpdate("insert into 組織 values(5, '北京昌平貿易局', '北京五環路18號', 2, 2)");
SqlRunner.executeUpdate("insert into 部門 values(1, '財務科', 2)");
SqlRunner.executeUpdate("insert into 部門 values(2, '市場部', 2)");
SqlRunner.executeUpdate("insert into 部門 values(3, '人事部', 2)");
}
其中,“組織”表的結構為:
編號(bigint),名稱(varchar),地址(varchar),組織分類編號(bigint),上級組織編號(bigint)
“部門”表的結構為:
編號(bigint),名稱(varchar),地址(varchar),組織編號(bigint)
在“組織”表中,編號為5的記錄引用了2的記錄,2的記錄引用了1的記錄。
而在tearDown()中,我們配合isUndeletableForSelfReference()來刪除相應記錄。
protected void tearDown() throws Exception {
SqlRunner.executeUpdate("delete from 部門");
while (SqlRunner.isUndeletableForSelfReference("組織", "上級組織編號")) {
SqlRunner.executeUpdate("delete from 組織 where 上級組織編號 is not null and 編號 not in (select 上級組織編號 from 組織 where 上級組織編號 is not null)");
}
SqlRunner.executeUpdate("delete from 組織");
SqlRunner.executeUpdate("delete from 組織分類");
}
因為“部門”引用“組織”,“組織”引用“組織分類”,因此我們必須依序刪除“部門”、“組織”及“組織分類”。難點在於while語句,其人工語義是,只要“組織”表中存在引用了其他記錄的“編號”的記錄,就會返回true,就先將這些引用的記錄刪除;只要“組織”表中不再有被引用的記錄了,我們可以安全地用“delete from 組織”刪除它們。
而在測試代碼中,在任何一個測試方法中,我們可以直接使用如下語句:
assertEquals(5, 組織Service.get組織數量());
對於數據庫測試代碼來講,速度是擺在第一位的,因此我們選擇了Hsqldb的內存數據庫方式。這種方式不能永久保存記錄,但只有測試期間,數據可用就行了。本人的實際測試代碼中,某個TestCase,共有28個測試方法,代碼將近千行,測試速度不到8秒,基本可以忍受。主要瓶頸在於setUp()及tearDown()總共運行了28遍。當然,setUp()中插入的數據越少,測試速度就越快,但每個測試方法中可能就需要增加一些工作量了。取捨完全在於你自己。
作者:Sarkuya(作者的blog:http://blog.matrix.org.cn/page/Sarkuya)
原文:http://blog.matrix.org.cn/page/Sarkuya?entry=%E8%AE%BE%E8%AE%A1%E8%87%AA%E5%B7%B1%E7%9A%84dbunit