程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 設計自己的DbUnit

設計自己的DbUnit

編輯:關於JAVA

在數據庫代碼測試中,一般情況使用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


  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved