在數(shù)據(jù)庫代碼測試中,一般情況使用2種方案:
一是使用mock objects;
二是使用DbUnit。
mock objects基于物理隔離層的概念,將涉及到數(shù)據(jù)庫操作的代碼,全用虛擬對象代替。這種方案,對業(yè)務(wù)領(lǐng)域里的代碼來講是可行的,也比較方便,但對于數(shù)據(jù)庫操作層,此方案無用武之地,因?yàn)槲覀儽仨殞?shí)實(shí)在在地與數(shù)據(jù)庫打交道。
而在數(shù)據(jù)庫測試中,因?yàn)槲覀兞η髮⒚總(gè)TestCase中眾多的測試方法完全隔離起來,不會因?yàn)橐粋(gè)測試方法因測試增加、刪除功能而影響到另一個(gè)測試方法,這樣,在每一個(gè)測試之前,數(shù)據(jù)庫的狀態(tài)是否穩(wěn)定,甚至是完全不變,顯得很重要了。而這點(diǎn),正是數(shù)據(jù)庫測試的難點(diǎn)。
Dbunit解決了這個(gè)問題。其原理很簡單,是在每個(gè)測試方法之前后,通過增刪一些固定的記錄,保持了數(shù)據(jù)庫的固定狀態(tài),由此,我們可以在每個(gè)測試方法中自由地增刪記錄,而不用擔(dān)心會影響到別的測試方法。
但Dbunit也有一個(gè)問題,即它不能刪除非空的外鍵記錄。舉例來說,假設(shè)“員工”表中有一非空字段為“部門編號”,引用了“部門”表的id, 只要“員工”表存在任一記錄,“部門”表將不能被刪除,強(qiáng)行刪除將出現(xiàn)違犯約束(constraint violation)的異常。當(dāng)然,如果必要,我們可以將數(shù)據(jù)庫的約束條件改為連鎖刪除,這樣,一旦我們刪除一名員工記錄,其所在的部門記錄也將從“部門”表中刪除。而此又會導(dǎo)致“員工”表中所有該部門的員工全被刪除。這是不允許的。當(dāng)然,作為測試,我們可以先刪除“員工”表,再刪除“部門”表。
但有時(shí),某些表自己引用自己,如“組織”表中有一“上級組織編號”字段,是自己“組織編號”的外鍵,即,此字段引用了本表中其他記錄的“組織編號”。此時(shí),我們必須先將這些引用了其他記錄的“組織編號”的記錄先刪除,才能刪除此表中的其他記錄。而Dbunit在實(shí)現(xiàn)上,只是用了一個(gè)簡單的"delete from ..."的SQL語句,不能解決這個(gè)問題。
Dbunit的原理是如此簡單,我們完全可以設(shè)計(jì)的“Dbunit”,通過多重循環(huán)語句,干脆利落地刪除自引用的整表。我們的“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的語句將被大量地應(yīng)用于實(shí)際測試當(dāng)中。但是根據(jù)測試先行的原則,任何一個(gè)基于Hiberante的語句都必須先測試再使用。而我們的這個(gè)“Dbunit”是運(yùn)行在實(shí)際測試之前,無法經(jīng)過測試。當(dāng)然,我們可以先假定這段Hiberante代碼正確無誤,然后再實(shí)際測試它。這種方法也有一個(gè)缺點(diǎn),因?yàn)闇y試代碼常常會因?yàn)橹貥?gòu)而發(fā)生改變,當(dāng)測試代碼改變時(shí),這個(gè)“Dbunit”也將被迫發(fā)生改變。而用JDBC的SQL語句,可保持這段代碼相對獨(dú)立,不至于連誅九族。
executeUpdate()將執(zhí)行“insert”、“delete”語句。重點(diǎn)在于isUndeletableForSelfReference()方法。此方法在某個(gè)表的某個(gè)字段非空時(shí),會返回false,告訴我們,此表中尚有被引用的記錄存在,從而不能刪除此表。盡管只有兩個(gè)方法,但對于我們的“Dbunit”來講,已經(jīng)足夠了。
在TestCase的setUp()中,我們利用其executeUpdate來增加一些必須的記錄。
protected void setUp() throws Exception {
SqlRunner.executeUpdate("insert into 組織分類 values(1, '教育系統(tǒng)')");
SqlRunner.executeUpdate("insert into 組織分類 values(2, '商貿(mào)系統(tǒng)')");
SqlRunner.executeUpdate("insert into 組織分類 values(3, '供應(yīng)商家')");
SqlRunner.executeUpdate("insert into 組織分類 values(4, '政府')");
SqlRunner.executeUpdate("insert into 組織 values(1, '中國貿(mào)易部', '北京三環(huán)路558號', 2, null)");
SqlRunner.executeUpdate("insert into 組織 values(2, '北京貿(mào)易廳', '北京四環(huán)路8號', 2, 1)");
SqlRunner.executeUpdate("insert into 組織 values(3, '河北高科技技術(shù)服務(wù)有限公司', '石家莊市白龍路23號', 3, null)");
SqlRunner.executeUpdate("insert into 組織 values(4, '四川珠寶有限公司', '成都市藍(lán)天路56號', 3, null)");
SqlRunner.executeUpdate("insert into 組織 values(5, '北京昌平貿(mào)易局', '北京五環(huán)路18號', 2, 2)");
SqlRunner.executeUpdate("insert into 部門 values(1, '財(cái)務(wù)科', 2)");
SqlRunner.executeUpdate("insert into 部門 values(2, '市場部', 2)");
SqlRunner.executeUpdate("insert into 部門 values(3, '人事部', 2)");
}
其中,“組織”表的結(jié)構(gòu)為:
編號(bigint),名稱(varchar),地址(varchar),組織分類編號(bigint),上級組織編號(bigint)
“部門”表的結(jié)構(gòu)為:
編號(bigint),名稱(varchar),地址(varchar),組織編號(bigint)
在“組織”表中,編號為5的記錄引用了2的記錄,2的記錄引用了1的記錄。
而在tearDown()中,我們配合isUndeletableForSelfReference()來刪除相應(yīng)記錄。
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 組織分類");
}
因?yàn)?ldquo;部門”引用“組織”,“組織”引用“組織分類”,因此我們必須依序刪除“部門”、“組織”及“組織分類”。難點(diǎn)在于while語句,其人工語義是,只要“組織”表中存在引用了其他記錄的“編號”的記錄,會返回true,先將這些引用的記錄刪除;只要“組織”表中不再有被引用的記錄了,我們可以安全地用“delete from 組織”刪除它們。
而在測試代碼中,在任何一個(gè)測試方法中,我們可以直接使用如下語句:
assertEquals(5, 組織Service.get組織數(shù)量());
對于數(shù)據(jù)庫測試代碼來講,速度是擺在第一位的,因此我們選擇了Hsqldb的內(nèi)存數(shù)據(jù)庫方式。這種方式不能保存記錄,但只有測試期間,數(shù)據(jù)可用行了。本人的實(shí)際測試代碼中,某個(gè)TestCase,共有28個(gè)測試方法,代碼將近千行,測試速度不到8秒,基本可以忍受。主要瓶頸在于setUp()及tearDown()總共運(yùn)行了28遍。當(dāng)然,setUp()中插入的數(shù)據(jù)越少,測試速度越快,但每個(gè)測試方法中可能需要增加一些工作量了。取舍完全在于你自己。
作者: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