利用HSQLDB 進行Hibernate單元測試

xuniji123發表於2007-04-08

動機

  曾經使用許多方法在資料庫和目的碼之間傳輸資料。從手動編碼的SQL到JDO,然後再到EJB,我從未找到一種特別喜歡的方法。自從採用測試驅動開發(TDD)作為指導原則以來,這種不滿情緒變得更加強烈。

  單元測試的障礙應儘可能少。在關聯式資料庫中,障礙的範圍從外部依賴(資料庫在執行嗎?)到保持關係模型和物件模型同步的速度。由於這些原因,保持資料庫訪問程式碼與核心物件模型分離且無需涉及真實資料庫而進行儘可能多的測試是很重要的。

  通常這會導致我們進入下面兩種模式之一。第一種是具體化所有訪問域物件的資料以及資料與單獨類或介面之間的關係。這就是典型的能夠檢索、編輯、刪除和新增域實體的資料儲存物件。這在單元測試中是最容易模擬出來的,但趨向於把域模型物件作為不帶有任何關係行為的純資料物件。直接從父物件訪問子記錄是最理想的,而不是將父物件處理為第三方類來決定子記錄。

  其他方法已經使訪問介面的域物件進入資料對映層(一種la Martin Fowler的資料映象模式)。這具有推動域模型中的物件關係的優點,在域模型中,物件關係型介面只需表達一次即可。使用域模型的類不支援永續性機制,因為它本身內在化到域模型中。這使程式碼集中在設法解決的業務問題,而很少關注物件關係型對映機制。

  我的當前專案涉及到處理大量的棒球統計資料,並使用這些資料進行模擬。因為資料已經在關聯式資料庫中,所以對於我來說,有機會開發Hibernate物件關係型對映系統。我曾對Hibernate有很深刻的印象,但我遇到的一個問題是,在使用Hibernate進行單元測試的資料對映時,設法插入一個間接層。該附加層非常脆弱,編寫起來感到非常困難。實際部署版本簡單地透過了特定於Hibernate的實現。更壞的情況是,模擬版本比真正的“產品級”版本更復雜,只因為模擬版本里沒有基本物件儲存器和帶有Hibernate的對映。

  我也使用很多複雜的Hibernate查詢,想要對應用程式的重要部分進行單元測試。然而,對活動的資料庫進行測試不是好主意,因為這幾乎總是產生維護問題。另外,由於測試最好互相獨立,在測試上下文資料中使用相同的主鍵意味著必須在每次測試前建立程式碼來清理資料庫,當涉及到大量關係時就成為一個實際問題。

  透過使用HSQLDB和Hibernate強大的模式生成工具,能夠對應用程式對映層進行單元測試,並在物件查詢中找到不計其數的bug,這在以前手工測試時是做不到的。利用下面的技術概述,可以在開發過程中對整個應用程式進行測試,並且在測試有效區域內沒有損害。

設定HSQLDB

  以前使用HSQLDB 1.7.3.0 版。為了使用資料庫的記憶體版本,需要啟用org.hsqldb.jdbcDriver的靜態載入程式。當獲得JDBC連線時,就可以使用JDBC url例如jdbc:hspldb:mem:yourdb,這裡’yourdb’就是想要使用的記憶體資料庫的名稱。

  因為使用Hibernate (3.0 beta 4),所以我幾乎無需接觸實際活動的JDBC物件。相反,我可以讓Hibernate完成很多繁重的任務,包括從Hibernate對映檔案中自動建立資料庫模式。因為Hibernate建立自身專有的連線池,所以它會基於TestSchema類中的配置程式碼自動載入HSQLDB JDBC驅動程式。下面就是該類的靜態的初始化程式。

public class TestSchema {

    static {
        Configuration config = new Configuration().
            setProperty("hibernate.dialect", "org.hibernate.dialect.HSQLDialect").
            setProperty("hibernate.connection.driver_class", "org.hsqldb.jdbcDriver").
            setProperty("hibernate.connection.url", "jdbc:hsqldb:mem:baseball").
            setProperty("hibernate.connection.username", "sa").
            setProperty("hibernate.connection.password", "").
            setProperty("hibernate.connection.pool_size", "1").
            setProperty("hibernate.connection.autocommit", "true").
            setProperty("hibernate.cache.provider_class", "org.hibernate.cache.HashtableCacheProvider").
            setProperty("hibernate.hbm2ddl.auto", "create-drop").
            setProperty("hibernate.show_sql", "true").
            addClass(Player.class).
            addClass(BattingStint.class).
            addClass(FieldingStint.class).
            addClass(PitchingStint.class);

        HibernateUtil.setSessionFactory(config.buildSessionFactory());
    }

  Hibernate提供了許多不同的方式來配置該框架,包括程式方面的配置。上述程式碼設定了連線池。注意,使用HSQLDB的記憶體資料庫需要使用者名稱'sa’。還樣要確保指定一個空格作為口令。為了啟動Hibernate的自動模式生成功能,需設定hibernate.hbm2ddl.auto屬性為’creat-drop’。

  實際測試 我的專案是處理將大量的棒球資料,所以我新增了四個進行對映的類(Player、PintchingStint、,BattingSint和FieldStint)。最後建立Hibernate的會話工廠,並將其插入HibernateUtil類,該類只為Hibernate會話的整個應用程式提供一個訪問方法。HibernateUtil的程式碼如下:

import org.hibernate.*;
import org.hibernate.cfg.Configuration;

public class HibernateUtil {

    private static SessionFactory factory;

    public static synchronized Session getSession() {
        if (factory == null) {
            factory = new Configuration().configure().buildSessionFactory();
        }
        return factory.openSession();
    }

    public static void setSessionFactory(SessionFactory factory) {
        HibernateUtil.factory = factory;
    }
}
因為所有程式碼(經過單元測試的產品級程式碼)都是從HibernateUtil獲取Hibernate會話,所以能在同一個位置對其進行配置。為了對程式碼的第一位進行單元測試而訪問TestSchema類將會啟用靜態初始化程式,該程式將安裝Hibernate並且將測試SessionFactory插入到HibernateUtil中。對於產品級程式碼,可以使用標準hibernate.cfg.xml配置機制來初始化 SessionFactory。
  那麼單元測試中的外部特徵是什麼?下面的測試程式碼片段是用來檢查邏輯的,決定運動員在棒球聯盟比賽中是哪個位置的人選:

    public void testGetEligiblePositions() throws Exception {
        Player player = new Player("playerId");
        TestSchema.addPlayer(player);

        FieldingStint stint1 = new FieldingStint("playerId", 2004, "SEA", Position.CATCHER);
        stint1.setGames(20);
        TestSchema.addFieldingStint(stint1);

        Set positions = player.getEligiblePositions(2004);
        assertEquals(1, positions.size());
        assertTrue(positions.contains(Position.CATCHER));
    }


  第一次建立新Player例項並透過addPlayer()方法新增到TestSchema中。必須首先完成此步驟,因為FidldStint類和Player類之間有外來鍵關係。如果不首先新增該例項,在設法新增FieldingStint時將會出現外來鍵約束違例。一旦測試上下文就位,就可以測試getEligiblePositions()方法來檢索校正資料。下面是在TsetSchema中addPlayer()方法的程式碼。您將注意到使用Hibernate而不是bare-metal JDBC程式碼:

    public static void addPlayer(Player player) {
        if (player.getPlayerId() == null) {
            throw new IllegalArgumentException("No primary key specified");
        }

        Session session = HibernateUtil.getSession();
        Transaction transaction = session.beginTransaction();
        try {
            session.save(player, player.getPlayerId());
            transaction.commit();
        }
        finally {
            session.close();
        }
    }


  在單元測試中最重要的就是要保持測試例項是獨立的。因為該方法仍然涉及資料庫,所以需要一種方法在每個測試例項之前清理資料庫。在我的資料庫架構中有四個表,所以我在TestSchemaz上編寫了reset()方法,該方法從使用JDBC的表中刪除所有行。注意,因為HSQLDB能識別外來鍵,刪除表的順序是很重要的,下面是程式碼:

    public static void reset() throws SchemaException {
        Session session = HibernateUtil.getSession();
        try {
            Connection connection = session.connection();
            try {
                Statement statement = connection.createStatement();
                try {
                    statement.executeUpdate("delete from Batting");
                    statement.executeUpdate("delete from Fielding");
                    statement.executeUpdate("delete from Pitching");
                    statement.executeUpdate("delete from Player");
                    connection.commit();
                }
                finally {
                    statement.close();
                }
            }
            catch (HibernateException e) {
                connection.rollback();
                throw new SchemaException(e);
            }
            catch (SQLException e) {
                connection.rollback();
                throw new SchemaException(e);
            }
        }
        catch (SQLException e) {
            throw new SchemaException(e);
        }
        finally {
            session.close();
        }
    }


  當確定在Hibernate 3.0中進行大量刪除操作時,應該能從應用程式中刪除直接JDBC的最後一位。到此時為止,必須獲取資料庫連線並向資料庫直接提交SQL。 在確保沒有關閉連線的情況下,為了釋放資源,只關閉會話就足夠了。出於手工編寫許多JCBC程式碼來進行開發的習慣,第一個版本關閉了JDBC連線。因為透過配置Hibernate建立的連線池只帶有一個連結,在第一個之後就完全破壞了測試。一定要注意這種情況! 既然在測試類執行時(設想執行所有的測試例項)不能確定資料庫的狀態,應該在setUp()方法中包含資料庫清除,如下所示:

    public void setUp() throws Exception {
        TestSchema.reset();
    }
結束語
  在使用像Hibernate這種複雜的O/R對映程式時,必須能夠測試實際存在(real-live)的RDBMS,而不會發生任何針對已部署資料庫的爭論。雖然Hibernate有內建模式生成工具,讓此類測試特別簡單,但是在這裡展示的例子並不排除Hibernate,並且可能與JDO或TopLink一起執行。使用上面描述的設定,您不必離開舒適的IDE環境,但仍然可以對程式碼進行大量測試。
[@more@]

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/8271432/viewspace-909328/,如需轉載,請註明出處,否則將追究法律責任。

相關文章