在數據庫代碼測試中,一般情況使用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) 原文:dbunit" target=_new>http://blog.matrix.org.cn/page/Sarkuya?entry=%E8%AE%BE%E8%AE%A1%E8%87%AA%E5%B7%B1%E7%9A%84dbunit
|