您的位置:軟件測試 > 開源軟件測試 > 開源單元測試工具 > DBunit
如何在數(shù)據(jù)庫代碼測試設(shè)計自己的DbUnit
作者:網(wǎng)絡(luò)轉(zhuǎn)載 發(fā)布時間:[ 2012/12/28 14:14:38 ] 推薦標簽:

在數(shù)據(jù)庫代碼測試中,一般情況使用2種方案:
一是使用mock objects;
二是使用DbUnit。

mock objects基于物理隔離層的概念,將涉及到數(shù)據(jù)庫操作的代碼,全用虛擬對象代替。這種方案,對業(yè)務(wù)領(lǐng)域里的代碼來講是可行的,也比較方便,但對于數(shù)據(jù)庫操作層,此方案無用武之地,因為我們必須實實在在地與數(shù)據(jù)庫打交道。

而在數(shù)據(jù)庫測試中,因為我們力求將每個TestCase中眾多的測試方法完全隔離起來,不會因為一個測試方法因測試增加、刪除功能而影響到另一個測試方法,這樣,在每一個測試之前,數(shù)據(jù)庫的狀態(tài)是否穩(wěn)定,甚至是完全不變,顯得很重要了。而這點,正是數(shù)據(jù)庫測試的難點。

Dbunit解決了這個問題。其原理很簡單,是在每個測試方法之前后,通過增刪一些固定的記錄,保持了數(shù)據(jù)庫的固定狀態(tài),由此,我們可以在每個測試方法中自由地增刪記錄,而不用擔(dān)心會影響到別的測試方法。

但Dbunit也有一個問題,即它不能刪除非空的外鍵記錄。舉例來說,假設(shè)“員工”表中有一非空字段為“部門編號”,引用了“部門”表的id, 只要“員工”表存在任一記錄,“部門”表將不能被刪除,強行刪除將出現(xiàn)違犯約束(constraint violation)的異常。當然,如果必要,我們可以將數(shù)據(jù)庫的約束條件改為連鎖刪除,這樣,一旦我們刪除一名員工記錄,其所在的部門記錄也將從“部門”表中刪除。而此又會導(dǎo)致“員工”表中所有該部門的員工全被刪除。這是不允許的。當然,作為測試,我們可以先刪除“員工”表,再刪除“部門”表。

但有時,某些表自己引用自己,如“組織”表中有一“上級組織編號”字段,是自己“組織編號”的外鍵,即,此字段引用了本表中其他記錄的“組織編號”。此時,我們必須先將這些引用了其他記錄的“組織編號”的記錄先刪除,才能刪除此表中的其他記錄。而Dbunit在實現(xiàn)上,只是用了一個簡單的"delete from ..."的SQL語句,不能解決這個問題。

Dbunit的原理是如此簡單,我們完全可以設(shè)計的“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)用于實際測試當中。但是根據(jù)測試先行的原則,任何一個基于Hiberante的語句都必須先測試再使用。而我們的這個“Dbunit”是運行在實際測試之前,無法經(jīng)過測試。當然,我們可以先假定這段Hiberante代碼正確無誤,然后再實際測試它。這種方法也有一個缺點,因為測試代碼常常會因為重構(gòu)而發(fā)生改變,當測試代碼改變時,這個“Dbunit”也將被迫發(fā)生改變。而用JDBC的SQL語句,可保持這段代碼相對獨立,不至于連誅九族。

executeUpdate()將執(zhí)行“insert”、“delete”語句。重點在于isUndeletableForSelfReference()方法。此方法在某個表的某個字段非空時,會返回false,告訴我們,此表中尚有被引用的記錄存在,從而不能刪除此表。盡管只有兩個方法,但對于我們的“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, '四川珠寶有限公司', '成都市藍天路56號', 3, null)");
        SqlRunner.executeUpdate("insert into 組織 values(5, '北京昌平貿(mào)易局', '北京五環(huán)路18號', 2, 2)");
       
        SqlRunner.executeUpdate("insert into 部門 values(1, '財務(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 組織分類");
    }

因為“部門”引用“組織”,“組織”引用“組織分類”,因此我們必須依序刪除“部門”、“組織”及“組織分類”。難點在于while語句,其人工語義是,只要“組織”表中存在引用了其他記錄的“編號”的記錄,會返回true,先將這些引用的記錄刪除;只要“組織”表中不再有被引用的記錄了,我們可以安全地用“delete from 組織”刪除它們。

而在測試代碼中,在任何一個測試方法中,我們可以直接使用如下語句:

assertEquals(5, 組織Service.get組織數(shù)量());

對于數(shù)據(jù)庫測試代碼來講,速度是擺在第一位的,因此我們選擇了Hsqldb的內(nèi)存數(shù)據(jù)庫方式。這種方式不能保存記錄,但只有測試期間,數(shù)據(jù)可用行了。本人的實際測試代碼中,某個TestCase,共有28個測試方法,代碼將近千行,測試速度不到8秒,基本可以忍受。主要瓶頸在于setUp()及tearDown()總共運行了28遍。當然,setUp()中插入的數(shù)據(jù)越少,測試速度越快,但每個測試方法中可能需要增加一些工作量了。取舍完全在于你自己。

作者: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

軟件測試工具 | 聯(lián)系我們 | 投訴建議 | 誠聘英才 | 申請使用列表 | 網(wǎng)站地圖
滬ICP備07036474 2003-2017 版權(quán)所有 上海澤眾軟件科技有限公司 Shanghai ZeZhong Software Co.,Ltd