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 閱讀(3534) 評論(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代碼 :(  回復  更多評論   

          主站蜘蛛池模板: 磴口县| 竹山县| 奉节县| 和田县| 珠海市| 宣城市| 泰来县| 彩票| 云南省| 鄂温| 延安市| 遂宁市| 淮滨县| 呼和浩特市| 兴城市| 措勤县| 舒城县| 苍梧县| 抚顺市| 昭觉县| 赣榆县| 宜宾县| 尼木县| 长治市| 克山县| 临沭县| 贵州省| 易门县| 勐海县| 定结县| 龙里县| 平果县| 巴南区| 应城市| 滁州市| 旅游| 鲁甸县| 泗水县| 榆树市| 台东县| 旬阳县|