<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    stone2083

    IBatis下DAO單元測試另類思路

    在說另類思路之前,先說下傳統的測試方法:
    0.準備一個干凈的測試數據庫環境
      這個是前提
    1.測試數據準備
      使用文本,excel,或者wiki等,準備測試sql以及測試數據
      利用dbfit,dbutil等工具將準備的測試數據導入到數據庫中
    2.執行dao方法
      執行被測試的dao方法
    3.測試結果斷言
      利用dbfit,dbutil等工具,斷言測試結果數據和預計是否一致
    4.所有數據回滾

    其實,對于這個流程來說,目前的dao測試框架,支持的已經比較完美了
    但是此類測試方法,也有明顯的缺點(或者不能叫缺點,叫使用比較麻煩的地方)
    如下:
    1.背上了一個數據庫環境.
      不輕量
      這是一個共享環境,誰也無法確保環境數據是否真正的干凈
    2.測試數據準備是一件麻煩的事情
      新表,10幾個字段毫不為奇;老表,50幾個字段甚至百來個字段,也偶有可見;無論是使用文本,excel,wiki,準備工作量,都是巨大的.
      準備的數據,部分字段內容可以是無意義的,部分字段內容又是需要符合測試意圖(testcase設計目的),部分字段還是其他表的關聯字段.從而導致后續維護人員無法了解準備數據意圖.
      (實踐中,也出現過,一同事在維護他人單元測試時,由于無法了解測試數據準備意圖,寧可重新刪除,自己準備一份)
    3.預計結果數據準備也是一件麻煩的事情
      理由如上

    所以,理論上是完美的測試方案,在實踐過程中,卻是一件麻煩的事情.導致DAO單元測試維護困難.


    分析了現狀,我們再來分析下,IBatis下DAO,程序員主要做了哪些編碼:
    1. 寫了一份sqlmap.xml配置文件
    2. 通過getSqlMapClientTemplate.doSomething($sqlID,$param), 執行語句
    (當然,沒有使用spring的同學,也是使用了類似sqlMapClient.doSomething($sqlID,$param)方法)

    而步驟2其實是框架替我們做了的事情,按照MOCK的思想,其實這部分代碼可以被MOCK的,那么我們是否可以做如下假設:
    只要sqlmap.xml中配置信息(主要包括resultmap和statement)是正確的,那么執行結果也應該是正確的.

    而我所謂的另類思路,就是基于這個假設,得出的:
    IBatis下,DAO單元測試,我們拋棄背負的數據庫環境,只要根據不同的條件,斷言不同的sql即可.

    于是乎,封裝了一個IbatisSqlTester,可以根據sqlmap中的statement和傳入的條件參數,生成sql語句.
    那么,DAO單元測試就簡單了,脫離下數據庫環境:
    public class ScoreDAOTest extends TestCase {
     
        @SpringBeanByName
        
    private IbatisSqlTester ibatisSqlTester;  //通過spring配置,需要注入sqlmapclient對象
     
        @Test
        
    public void testListTpScores() {
            Map
    <String, Object> param = new HashMap<String, Object>(1);
            param.put(
    "memberIds"new String[] { "stone""stone2083" });
            SqlStatement sql 
    = ibatisSqlTester.test("MS-LIST-SCORES", param);
            
    // sql全部匹配
            SqlAssert.isEqual("select * from score where member_id in ('stone','stone2083')", sql.toString());
            
    // sql包含member_id,athena2002,stone關鍵詞
            SqlAssert.keyWith(sql.toString(), "member_id""stone""stone2083");
            
    // sql符合某個 正則
            SqlAssert.regexWith(".* where member_id in .*", sql.toString());
            
            
    //其中,SqlAssert也可以換 成want.string()中的方法.
        }
    }

    優勢:
      脫離了數據庫環境
      脫離了表結構數據準備
      脫離了預計結果數據準備
      讓單元測試變成sql的斷言,編寫相對更簡單
    缺點:
     
    row mapper過程無法被測試


    最后,附上兩個核心的代碼類(還未完成),供大家參考:
    SqlStatement.java
    /**
     * <pre>
     * SqlStatement:Sql語句對象.
     * 包含:
     *  1.sql語句,類似  select * from offer where id = ? and member_id = ?
     *  2.參數值,類似 [1,stone2083]
     *  
     *  toString方法,返回執行的sql語句,如:
     *  select * from offer where id = '1' and member_id = 'stone2083'
     * </pre>
     * 
     * 
    @author Stone.J 2010-8-9 下午02:55:36
     
    */
    public class SqlStatement {

        
    //sql
        private String   sql;
        
    //sql參數
        private Object[] param;

        
    /**
         * <pre>
         * 輸出最終執行的sql內容.
         * 將sql和param進行merge,產生最終執行的sql語句
         * </pre>
         
    */
        @Override
        
    public String toString() {
            
    return merge();
        }

        
    /**
         * <pre>
         * 將sql進行格式化.
         * 
         * 目前只是簡單進行格式化.去除前后空格,已經重復空格
         * TODO:請使用統一格式化標準規,建議使用SqlFormater類,進行處理
         * </pre>
         * 
         * 
    @param sql
         * 
    @return
         
    */
        
    protected String format(String sql) {
            
    if (sql == null) {
                
    return null;
            }
            
    return sql.toLowerCase().trim().replaceAll("\\s{1,}"" ");
        }

        
    /**
         * <pre>
         * 將sql和param進行merge.
         * TODO:請嚴格按照SQL標準,進行merge sql內容
         * </pre>
         
    */
        
    protected String merge() {
            
    if (param == null || param.length == 0) {
                
    return this.sql;
            }
            String ret 
    = sql;
            
    for (Object p : param) {
                ret 
    = ret.replaceFirst("\\?""'" + p.toString() + "'");
            }
            
    return ret;
        }

        
    public String getSql() {
            
    return sql;
        }

        
    public void setSql(String sql) {
            
    this.sql = format(sql);
        }

        
    public Object[] getParam() {
            
    return param;
        }

        
    public void setParam(Object[] param) {
            
    this.param = param;
        }
    }

    IbatisSqlTester.java
    /**
     * <pre>
     * IBtatis SQL 測試
     * 一般IBatis DAO單元測試,主要就是在測試ibatis的配置文件.
     * IbatisSqlTester將根據提供的Sql Map Id 和 對應的參數,返回 {
    @link SqlStatement}對象,提供最終執行的sql語句
     * 通過外部SqlAssert對象,將預計Sql和實際產生的Sql進行對比,判斷是否正確
     * </pre>
     * 
     * 
    @author Stone.J 2010-8-9 下午02:58:46
     
    */
    public class IbatisSqlTester {

        
    // sqlMapClient
        private ExtendedSqlMapClient sqlMapClient;

        
    /**
         * 根據提供的SqlMap ID,得到 {
    @link SqlStatement}對象
         * 
         * 
    @param sqlId: sql map id
         * 
    @return @see {@link SqlStatement}
         
    */
        
    public SqlStatement test(String sqlId) {
            
    //得到MappedStatement對象
            MappedStatement ms = sqlMapClient.getMappedStatement(sqlId);
            
    if (ms == null) {
                
    //TODO:建議封轉自己的異常對象
                throw new RuntimeException("can't find MappedStatement.");
            }

            
    //按照Ibatis代碼,得到Sql和Param信息
            RequestScope request = new RequestScope();
            ms.initRequest(request);
            Sql sql 
    = ms.getSql();
            String sqlValue 
    = sql.getSql(request, null);

            
    //組轉返回對象
            SqlStatement ret = new SqlStatement();
            ret.setSql(sqlValue);
            
    return ret;
        }

        
    /**
         * 根據提供的SqlMap ID和對應的param信息,得到 {
    @link SqlStatement}對象
         * 
         * 
    @param sqlId: sql map id
         * 
    @param param: 參數內容
         * 
    @return @see {@link SqlStatement}
         
    */
        
    public SqlStatement test(String sqlId, Object param) {
            
    //得到MappedStatement對象
            MappedStatement ms = sqlMapClient.getMappedStatement(sqlId);
            
    if (ms == null) {
                
    //TODO:建議封轉自己的異常對象
                throw new RuntimeException("can't find MappedStatement.");
            }

            
    //按照Ibatis代碼,得到Sql和Param信息
            RequestScope request = new RequestScope();
            ms.initRequest(request);
            Sql sql 
    = ms.getSql();
            String sqlValue 
    = sql.getSql(request, param);
            Object[] sqlParam 
    = sql.getParameterMap(request, param).getParameterObjectValues(request, param);

            
    //組轉返回對象
            SqlStatement ret = new SqlStatement();
            ret.setSql(sqlValue);
            ret.setParam(sqlParam);
            
    return ret;
        }

        
    /**
         * 設置SqlMapClient對象
         
    */
        
    public void setSqlMapClient(ExtendedSqlMapClient sqlMapClient) {
            
    this.sqlMapClient = sqlMapClient;
        }

        
    /**
         * <pre>
         * 不推薦使用
         * 推薦使用: {
    @link IbatisSqlTester#setSqlMapClient(ExtendedSqlMapClient)}
         * TODO:請去除這個方法,或者增加初始化的方式
         * </pre>
         * 
         * 
    @param sqlMapConfig sqlMapConfig xml文件
         
    */
        
    public void setSqlMapConfig(String sqlMapConfig) {
            InputStream in 
    = null;
            
    try {
                File file 
    = ResourceUtils.getFile(sqlMapConfig);
                in 
    = new FileInputStream(file);
                
    this.sqlMapClient = (ExtendedSqlMapClient) SqlMapClientBuilder.buildSqlMapClient(in);
            } 
    catch (Exception e) {
                
    throw new RuntimeException("sqlMapConfig init error.", e);
            } 
    finally {
                
    if (in != null) {
                    
    try {
                        in.close();
                    } 
    catch (IOException e) {
                    }
                }
            }
        }

    }


    最后的最后附上所有代碼(通過單元測試代碼,可以看如何使用).歡迎大家的討論.
    sqltester
    builder

    posted on 2010-08-12 09:03 stone2083 閱讀(3531) 評論(9)  編輯  收藏 所屬分類: java

    Feedback

    # re: IBatis下DAO單元測試另類思路[未登錄] 2010-08-12 11:12 kylin

    可以看看DDStep
    http://www.ddsteps.org/  回復  更多評論   

    # re: IBatis下DAO單元測試另類思路 2010-08-12 12:18 sgz

    lz 很有想法  回復  更多評論   

    # re: IBatis下DAO單元測試另類思路 2010-08-12 16:58 stone2083

    @kylin
    簡單地看了下ddsteps,它是一套集成測試工具.主要包括:
    dbunit,selenium,mock web(http) server,easymock和spring.
    它對DB的支持,也是采用傳統的方式.(沒猜錯的話,是使用了,最多封裝了dbunit)
    并不能解決我現在遇到的問題:
    1.背負數據庫環境
    2.準備測試數據
    3.準備預計結果數據
    這些麻煩的工作量.

    而且在公司中,也已經有一套測試框架,我們要做的,并不是選擇框架(替換框架),而是選擇一種合適敏捷單元測試的思路和方案. 讓框架支持敏捷的方案而已.  回復  更多評論   

    # re: IBatis下DAO單元測試另類思路 2010-08-12 17:02 stone2083

    @sgz
    大多數時候,想法是被現實逼出來的 :)
    在我們這邊,dao幾乎沒有復雜的業務邏輯,僅僅是對SqlMapClientTemplate的使用而已.
    但是在針對DB的單元測試時,代價又是如此的巨大(主要還是在數據準備上).
    成本,收益比,不劃算,開發們抱怨多.

    分析現狀和開發們實際需求(寫dao,主要是擔心sql寫錯)后,才萌生了這個想法.  回復  更多評論   

    # re: IBatis下DAO單元測試另類思路 2010-08-13 07:51 藍劍

    @stone2083
    既然業務邏輯不復雜,那么準備的數據就不會很復雜,那還在乎這點工作量?
    sql既然都能測試了,還在乎這點數據準備?
    數據映射都不測試,還用ibatis干什么,直接jdbc就是了  回復  更多評論   

    # re: IBatis下DAO單元測試另類思路 2010-08-13 08:31 stone2083

    @藍劍
    DAO業務邏輯不復雜只指:在DAO方法中,不會有復雜的分支流程,往往只會調用一條SqlID執行sql.但是這不意味sql不復雜.
    打個比方,報表的生成,業務邏輯非常簡單(根據什么樣的條件,能看到什么樣的數據),但是sql絕對的復雜. :)
    dao方法,也只會有一句 getSqlMapClientTemplate.queryForList("....",param); // 簡單吧 :)

    數據準備的工作量低嗎?維護成本低嗎?至少在我實踐的項目中,沒有像sample那樣低(dbfit,dbunit,dbutil等sample,都是單表的說明,單表字段往往少于5個字段). 實際情況是:
    1.字段多
    2.表關聯 (尤其在tree結構的表,父節點的依賴,光是這樣的準備,都非常容易寫錯)
    3.對于查詢的語句(尤其是分頁),需要根據動態條件,準備好需要的數據
    4.數據準備意圖需要被傳承
    在我看來,并且實踐過來,挺不容易的.

    sql的assert,只要根據條件參數的不同,做不同預計sql的assert,成本絕對比結果數據校驗,來得低.

    至于最后一點.
    是用ibatis,還是jdbc;不是單元測試成本(方案)決定的;而是需求,應用,架構設計,部門崗位情況等決定的.
    我們有專業的dba,對所有sql要做review,總不能給一堆jdbc的文件給他們吧.ibatis就挺好了.
    再說了,對于ibatis下dao編碼,錯誤率是sql寫錯的高呢?還是row mapper錯誤高呢?所以如果因為這點,來否決全部,挺不公平的.  回復  更多評論   

    # re: IBatis下DAO單元測試另類思路 2010-08-13 08:46 stone2083

    提供這個思路,并不是說替代之前的方案,更不是對傳統測試方案的否定. 僅僅是為了多一種選擇.

      回復  更多評論   

    # re: IBatis下DAO單元測試另類思路[未登錄] 2010-08-13 11:10 kylin

    說說我們的單元測試:
    DAO的代碼是工具自動生成的,后臺是用iBtais實現的,sqlmap也是自動生成,所以不用單元測試,因為模式都是一樣的,開發人員不必寫SQL。
    Service API調用DAO接口,完成業務邏輯,這部分是需要做單元測試的,使用DDStep框架,測試的框架代碼也是自動生成,開發人員在框架代碼的基礎上需要做以下幾件事:
    1.準備測試用例(準備數據庫結構,測試數據輸入)
    2.編寫結果校驗代碼(結果數據輸出)
    其中:準備數據庫結構,測試數據輸入都是通過excel完成,容易修改,不用編代碼,看代碼(DDStep框架提供的機制)

    比較費時的就是校驗代碼的編寫。  回復  更多評論   

    # re: IBatis下DAO單元測試另類思路 2010-08-13 11:54 stone2083

    @kylin
    明白你們這邊的情況了.

    Service層的測試相對還是容易的,我們這邊也有一套測試框架(類似DDSteps,也是對一些業界測試工具的整合加改進).并且對Service依賴的外部環境,都做了隔離,主要包括:
    1.Mock Dao impl
    2.Mock Core Service impl (外部核心業務服務)
    3.Mock Search Engine impl
    4.Mock Cach impl
    ....
    測試重心,主要集中在Service內部邏輯的測試上.
    而公司使用的測試框架很好的支持了這些需求.


    難點還在DAO的測試上.
    數據準備的復雜度,取決于表設計的復雜度和sql的復雜度.尤其在ibatis支持dynamic語句下,要準備覆蓋測試sql語句的數據.挺繁瑣的.
    這并不是說使用excel,還是wiki等的問題.而是數據內容的準備上.

    每個測試數據的準備,都是為了一個特定的testcase設計目的的.而當字段多,并且表設計相對復雜的時候,這個準備意圖,挺難被傳承下去的.
    隨著項目,小需求的進行,我們這邊,差不多幾十人,都有可能修改同一個sql代碼 :(  回復  更多評論   

    主站蜘蛛池模板: 亚洲国产精品成人综合色在线| 一二三四免费观看在线电影 | 精品人妻系列无码人妻免费视频| 亚洲精品成人网站在线播放| 国产综合精品久久亚洲| 在线免费不卡视频| 国产91免费视频| 伊人久久免费视频| 久青草视频在线观看免费| 大桥未久亚洲无av码在线| 国产成人亚洲精品| 亚洲日产2021三区在线 | 国产精品1024在线永久免费| 亚洲中文字幕乱码AV波多JI| 亚洲男人电影天堂| 精品亚洲麻豆1区2区3区| 国产av天堂亚洲国产av天堂| 国产亚洲精品精品国产亚洲综合 | 亚洲av最新在线观看网址| 亚洲春色另类小说| 中文字幕亚洲综合久久2| 亚洲国产综合专区在线电影| 亚洲宅男天堂在线观看无病毒| 免费国产成人午夜私人影视| 成人毛片视频免费网站观看| 免费看污成人午夜网站| 91久久精品国产免费直播| 久久不见久久见免费视频7| 亚欧免费一级毛片| 久久精品免费一区二区| 精品女同一区二区三区免费站| 99视频在线精品免费| 日本在线看片免费人成视频1000| 国内精品一级毛片免费看| APP在线免费观看视频| 在线成人精品国产区免费| 毛片在线全部免费观看| 日韩精品极品视频在线观看免费| 久久免费区一区二区三波多野| 99久久99久久精品免费观看| 亚洲毛片免费观看|