1. gzyueqian
      13352868059
      首頁 > 新聞中心 > > 正文

      對J2EE中的DAO組件編寫單元測試

      更新時間: 2007-12-07 10:03:33來源: 粵嵌教育瀏覽量:1074

        單元測試作為保證軟件質量及重構的基礎,早已獲得廣大開發人員的認可。單元測試是一種細粒度的測試,越來越多的開發人員在提交功能模塊時也同時提交相應的單元測試。對于大多數開發人員來講,編寫單元測試已經成為開發過程中必須的流程和實踐。

        對普通的邏輯組件編寫單元測試是一件容易的事情,由于邏輯組件通常只需要內存資源,因此,設置好輸入輸出即可編寫有效的單元測試。對于稍微復雜一點的組件,例如Servlet,我們可以自行編寫模擬對象,以便模擬HttpRequest和HttpResponse等對象,或者,使用EasyMock之類的動態模擬庫,可以對任意接口實現相應的模擬對象,從而對依賴接口的組件進行有效的單元測試。

        在J2EE開發中,對DAO組件編寫單元測試往往是一件非常復雜的任務。和其他組件不通,DAO組件通常依賴于底層數據庫,以及JDBC接口或者某個ORM框架(如Hibernate),對DAO組件的測試往往還需引入事務,這更增加了編寫單元測試的復雜性。雖然使用EasyMock也可以模擬出任意的JDBC接口對象,或者ORM框架的主要接口,但其復雜性往往非常高,需要編寫大量的模擬代碼,且代碼復用度很低,甚至不如直接在真實的數據庫環境下測試。不過,使用真實數據庫環境也有一個明顯的弊端,我們需要準備數據庫環境,準備初始數據,并且每次運行單元測試后,其數據庫現有的數據將直接影響到下一次測試,難以實現“即時運行,反復運行”單元測試的良好實踐。

        本文針對DAO組件給出一種較為合適的單元測試的編寫策略。在JavaEE開發網的開發過程中,為了對DAO組件進行有效的單元測試,我們采用HSQLDB這一小巧的純Java數據庫作為測試時期的數據庫環境,配合Ant,實現了自動生成數據庫腳本,測試前自動初始化數據庫,極大地簡化了DAO組件的單元測試的編寫。

        在Java領域,JUnit作為個單元測試框架已經獲得了廣泛的應用,無可爭議地成為Java領域單元測試的標準框架。本文以的JUnit 4版本為例,演示如何創建對DAO組件的單元測試用例。

        JavaEEdev的持久層使用Hibernate 3.2,底層數據庫為MySQL。為了演示如何對DAO進行單元測試,我們將其簡化為一個DAOTest工程:
                     

        由于將Hibernate的Transaction綁定在Thread上,因此,HibernateUtil類負責初始化SessionFactory以及獲取當前的Session:

        public class HibernateUtil {
        private static final SessionFactory sessionFactory;
        static {
        try {
        sessionFactory = new AnnotationConfiguration()
        .configure()
        .buildSessionFactory();
        }
        catch(Exception e) {
        throw new ExceptionInInitializerError(e);
        }
        }
        public static Session getCurrentSession() {
        return sessionFactory.getCurrentSession();
        }
        }
        HibernateUtil還包含了一些輔助方法,如:

        public static Object query(Class clazz, Serializable id);
        public static void createEntity(Object entity);
        public static Object queryForObject(String hql, Object[] params);
        public static List queryForList(String hql, Object[] params);

        在此不再多述。

        實體類User使用JPA注解,代表一個用戶:

        @Entity
        @Table(name="T_USER")
        public class User {
        public static final String REGEX_USERNAME = "[a-z0-9][a-z0-9\\-]{1,18}[a-z0-9]";
        public static final String REGEX_PASSWORD = "[a-f0-9]{32}";
        public static final String REGEX_EMAIL = "([0-9a-zA-Z]([-.\\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\\w]*[0-9a-zA-Z]\\.)+[a-zA-Z]{2,9})";
        private String username; // 用戶名
        private String password; // MD5口令
        private boolean admin; // 是否是管理員
        private String email; // 電子郵件
        private int emailValidation; // 電子郵件驗證碼
        private long createdDate; // 創建時間
        private long lockDate; // 鎖定時間
        public User() {}
        public User(String username, String password, boolean admin, long lastSignOnDate) {
        this.username = username;
        this.password = password;
        this.admin = admin;
        }
        @Id
        @Column(updatable=false, length=20)
        @Pattern(regex=REGEX_USERNAME)
        public String getUsername() { return username; }
        public void setUsername(String username) { this.username = username; }
        @Column(nullable=false, length=32)
        @Pattern(regex=REGEX_PASSWORD)
        public String getPassword() { return password; }
        public void setPassword(String password) { this.password = password; }
        @Column(nullable=false, length=50)
        @Pattern(regex=REGEX_EMAIL)
        public String getEmail() { return email; }
        public void setEmail(String email) { this.email = email; }
        @Column(nullable=false)
        public boolean getAdmin() { return admin; }
        public void setAdmin(boolean admin) { this.admin = admin; }
        @Column(nullable=false, updatable=false)
        public long getCreatedDate() { return createdDate; }
        public void setCreatedDate(long createdDate) { this.createdDate = createdDate; }
        @Column(nullable=false)
        public int getEmailValidation() { return emailValidation; }
        public void setEmailValidation(int emailValidation) { this.emailValidation = emailValidation; }
        @Column(nullable=false)
        public long getLockDate() { return lockDate; }
        public void setLockDate(long lockDate) { this.lockDate = lockDate; }
        @Transient
        public boolean getEmailValidated() { return emailValidation==0; }
        @Transient
        public boolean getLocked() {
        return !admin && lockDate>0 && lockDate>System.currentTimeMillis();
        }
        }

        實體類PasswordTicket代表一個重置口令的請求:

        @Entity
        @Table(name="T_PWDT")
        public class PasswordTicket {
        private String id;
        private User user;
        private String ticket;
        private long createdDate;
        @Id
        @Column(nullable=false, updatable=false, length=32)
        @GeneratedValue(generator="system-uuid")
        @GenericGenerator(name="system-uuid", strategy="uuid")
        public String getId() { return id; }
        protected void setId(String id) { this.id = id; }
        @ManyToOne
        @JoinColumn(nullable=false, updatable=false)
        public User getUser() { return user; }
        public void setUser(User user) { this.user = user; }
        @Column(nullable=false, updatable=false, length=32)
        public String getTicket() { return ticket; }
        public void setTicket(String ticket) { this.ticket = ticket; }
        @Column(nullable=false, updatable=false)
        public long getCreatedDate() { return createdDate; }
        public void setCreatedDate(long createdDate) { this.createdDate = createdDate; }
        }

        UserDao接口定義了對用戶的相關操作
        
        public interface UserDao {
        User queryForSignOn(String username);
        User queryUser(String username);
        void createUser(User user);
        void updateUser(User user);
        boolean updateEmailValidation(String username, int ticket);
        String createPasswordTicket(User user);
        boolean updatePassword(String username, String oldPassword, String newPassword);
        boolean queryResetPassword(User user, String ticket);
        boolean updateResetPassword(User user, String ticket, String password);
        void updateLock(User user, long lockTime);
        void updateUnlock(User user);
        }

        UserDaoImpl是其實現類:

        public class UserDaoImpl implements UserDao {
        public User queryForSignOn(String username) {
        User user = queryUser(username);
        if(user.getLocked())
        throw new LockException(user.getLockDate());
        return user;
        }
        public User queryUser(String username) {
        return (User) HibernateUtil.query(User.class, username);
        }
        public void createUser(User user) {
        user.setEmailValidation((int)(Math.random() * 1000000) + 0xf);
        HibernateUtil.createEntity(user);
        }
        // 其余方法略
        ...
        }

        由于將Hibernate事務綁定在Thread上,因此,實際的客戶端調用DAO組件時,還必須加入事務代碼:

        Transaction tx = HibernateUtil.getCurrentSession().beginTransaction();
        try {
        dao.xxx();
        tx.commit();
        }
        catch(Exception e) {
        tx.rollback();
        throw e;
        }

        下面,我們開始對DAO組件編寫單元測試。前面提到了HSQLDB這一小巧的純Java數據庫。HSQLDB除了提供完整的JDBC驅動以及事務支持外,HSQLDB還提供了進程外模式(與普通數據庫類似)和進程內模式(In-Process),以及文件和內存兩種存儲模式。我們將HSQLDB設定為進程內模式及僅使用內存存儲,這樣,在運行JUnit測試時,可以直接在測試代碼中啟動HSQLDB。測試完畢后,由于測試數據并沒有保存在文件上,因此,不必清理數據庫。

        此外,為了執行批量測試,在每個獨立的DAO單元測試運行前,我們都執行一個初始化腳本,重新建立所有的表。該初始化腳本是通過HibernateTool自動生成的,稍后我們還會討論。下圖是單元測試的執行順序:

                    

        在編寫測試類之前,我們首先準備了一個TransactionCallback抽象類,該類通過Template模式將DAO調用代碼通過事務包裝起來:

        public abstract class TransactionCallback {
        public final Object execute() throws Exception {
        Transaction tx = HibernateUtil.getCurrentSession().beginTransaction();
        try {
        Object r = doInTransaction();
        tx.commit();
        return r;
        }
        catch(Exception e) {
        tx.rollback();
        throw e;
        }
        }
        // 模板方法:
        protected abstract Object doInTransaction() throws Exception;
        }

        其原理是使用JDK提供的動態代理。由于JDK的動態代理只能對接口代理,因此,要求DAO組件必須實現接口。如果只有具體的實現類,則只能考慮CGLIB之類的第三方庫,在此我們不作更多討論。

        下面我們需要編寫DatabaseFixture,負責啟動HSQLDB數據庫,并在@Before方法中初始化數據庫表。該DatabaseFixture可以在所有的DAO組件的單元測試類中復用:

        public class DatabaseFixture {
        private static Server server = null; // 持有HSQLDB的實例
        private static final String DATABASE_NAME = "javaeedev"; // 數據庫名稱
        private static final String SCHEMA_FILE = "schema.sql"; // 數據庫初始化腳本
        private static final List<String> initSqls = new ArrayList<String>();
        @BeforeClass // 啟動HSQLDB數據庫
        public static void startDatabase() throws Exception {
        if(server!=null)
        return;
        server = new Server();
        server.setDatabaseName(0, DATABASE_NAME);
        server.setDatabasePath(0, "mem:" + DATABASE_NAME);
        server.setSilent(true);
        server.start();
        try {
        Class.forName("org.hsqldb.jdbcDriver");
        }
        catch(ClassNotFoundException cnfe) {
        throw new RuntimeException(cnfe);
        }
        LineNumberReader reader = null;
        try {
        reader = new LineNumberReader(new    InputStreamReader(DatabaseFixture.class.getClassLoader().getResourceAsStream(SCHEMA_FILE)));
        for(;;) {
        String line = reader.readLine();
        if(line==null) break;
        // 將text類型的字段改為varchar(2000),因為HSQLDB不支持text:
        line = line.trim().replace(" text ", " varchar(2000) ").replace("  text,", " varchar(2000),");
        if(!line.equals(""))
        initSqls.add(line);
        }
        }
        catch(IOException e) {
        throw new RuntimeException(e);
        }
        finally {
        if(reader!=null) {
        try{ reader.close(); } catch(IOException e) {}
        }
        }
        }
        @Before // 執行初始化腳本
        public void initTables() {
        for(String sql : initSqls) {
        executeSQL(sql);
        }
        }
        static Connection getConnection() throws SQLException {
        return DriverManager.getConnection("jdbc:hsqldb:mem:" + DATABASE_NAME, "sa", "");
        }
        static void close(Statement stmt) {
        if(stmt!=null) {
        try {
        stmt.close();
        }
        catch(SQLException e) {}
        }
        }
        static void close(Connection conn) {
        if(conn!=null) {
        try {
        conn.close();
        }
        catch(SQLException e) {}
        }
        }
        static void executeSQL(String sql) {
        Connection conn = null;
        Statement stmt = null;
        try {
        conn = getConnection();
        boolean autoCommit = conn.getAutoCommit();
        conn.setAutoCommit(true);
        stmt = conn.createStatement();
        stmt.execute(sql);
        conn.setAutoCommit(autoCommit);
        }
        catch(SQLException e) {
        log.warn("Execute failed: " + sql + "\nException: " + e.getMessage());
        }
        finally {
        close(stmt);
        close(conn);
        }
        }
        public static Object createProxy(final Object target) {
        return Proxy.newProxyInstance(
        target.getClass().getClassLoader(),
        target.getClass().getInterfaces(),
        new InvocationHandler() {
        public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable {
        return new TransactionCallback() {
        @Override
        protected Object doInTransaction() throws Exception {
        return method.invoke(target, args);
        }
        }.execute();
        }
        }
        );
        }
        }

        注意DatabaseFixture的createProxy()方法,它將一個普通的DAO對象包裝為在事務范圍內執行的代理對象,即對于一個普通的DAO對象的方法調用前后,自動地開啟事務并根據異常情況提交或回滾事務。

        下面是UserDaoImpl的單元測試類:

        public class UserDaoImplTest extends DatabaseFixture {
        private UserDao userDao = new UserDaoImpl();
        private UserDao proxy = (UserDao)createProxy(userDao);
        @Test
        public void testQueryUser() {
        User user = newUser("test");
        proxy.createUser(user);
        User t = proxy.queryUser("test");
        assertEquals(user.getEmail(), t.getEmail());
        }
        }

        由于UserDaoImplTest從DatabaseFixture繼承,因此,@Before方法在每個@Test方法調用前自動調用,這樣,每個@Test方法執行前,數據庫都是一個經過初始化的“干凈”的表。

        對于普通的測試,如UserDao.queryUser()方法,直接調用proxy.queryUser()即可在事務內執行查詢,獲得返回結果。

        對于異常測試,例如期待一個ResourceNotFoundException,就不能直接調用proxy.queryUser()方法,否則,將得到一個UndeclaredThrowableException:

               

       這是因為通過反射調用拋出的異常被代理類包裝為UndeclaredThrowableException,因此,對于異常測試,只能使用原始的userDao對象配合TransactionCallback實現:

        @Test(expected=ResourceNotFoundException.class)
        public void testQueryNonExistUser() throws Exception {
        new TransactionCallback() {
        protected Object doInTransaction() throws Exception {
        userDao.queryUser("nonexist");
        return null;
        }
        }.execute();
        }

        到此為止,對DAO組件的單元測試已經實現完畢。下一步,我們需要使用HibernateTool自動生成數據庫腳本,免去維護SQL語句的麻煩。相關的Ant腳本片段如下: 

        <target name="make-schema" depends="build" description="create schema">
        <taskdef name="hibernatetool" classname="org.hibernate.tool.ant.HibernateToolTask">
        <classpath refid="build-classpath"/>
        </taskdef>
        <taskdef name="annotationconfiguration" classname="org.hibernate.tool.ant.AnnotationConfigurationTask">
        <classpath refid="build-classpath"/>
        </taskdef>
        <annotationconfiguration configurationfile="${src.dir}/hibernate.cfg.XML"/>
        <hibernatetool destdir="${gen.dir}">
        <classpath refid="build-classpath"/>
        <annotationconfiguration configurationfile="${src.dir}/hibernate.cfg.xml"/>
        <hbm2ddl
        export="false"
        drop="true"
        create="true"
        delimiter=";"
        outputfilename="schema.sql"
        destdir="${src.dir}"
        />
        </hibernatetool>
        </target>

        完整的Ant腳本以及Hibernate配置文件請參考項目工程源代碼。

        利用HSQLDB,我們已經成功地簡化了對DAO組件進行單元測試。我發現這種方式能夠找出許多常見的bug:

        HQL語句的語法錯誤,包括SQL關鍵字和實體類屬性的錯誤拼寫,反復運行單元測試就可以不斷地修復許多這類錯誤,而不需要等到通過Web頁面請求而調用DAO時才發現問題;
        傳入了不一致或者順序錯誤的HQL參數數組,導致Hibernate在運行期報錯;
        一些邏輯錯誤,包括不允許的null屬性(常常由于忘記設置實體類的屬性),更新實體時引發的數據邏輯狀態不一致。
        
        總之,單元測試需要根據被測試類的實際情況,編寫簡單有效的測試用例。本文旨在給出一種編寫DAO組件單元測試的有效方法。

      免費預約試聽課

      亚洲另类欧美综合久久图片区_亚洲中文字幕日产无码2020_欧美日本一区二区三区桃色视频_亚洲AⅤ天堂一区二区三区

      
      

      1. 亚洲中文精品有码视频在线 | 特级AV片在线播放 | 亚洲第一中文字幕精品视频 | 午夜久久福利小视频 | 伊人久久综合精品永久图片 | 在线网站亚洲观看 |