stone2083

          IBatis下DAO單元測(cè)試另類思路

          在說另類思路之前,先說下傳統(tǒng)的測(cè)試方法:
          0.準(zhǔn)備一個(gè)干凈的測(cè)試數(shù)據(jù)庫(kù)環(huán)境
            這個(gè)是前提
          1.測(cè)試數(shù)據(jù)準(zhǔn)備
            使用文本,excel,或者wiki等,準(zhǔn)備測(cè)試sql以及測(cè)試數(shù)據(jù)
            利用dbfit,dbutil等工具將準(zhǔn)備的測(cè)試數(shù)據(jù)導(dǎo)入到數(shù)據(jù)庫(kù)中
          2.執(zhí)行dao方法
            執(zhí)行被測(cè)試的dao方法
          3.測(cè)試結(jié)果斷言
            利用dbfit,dbutil等工具,斷言測(cè)試結(jié)果數(shù)據(jù)和預(yù)計(jì)是否一致
          4.所有數(shù)據(jù)回滾

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

          所以,理論上是完美的測(cè)試方案,在實(shí)踐過程中,卻是一件麻煩的事情.導(dǎo)致DAO單元測(cè)試維護(hù)困難.


          分析了現(xiàn)狀,我們?cè)賮?lái)分析下,IBatis下DAO,程序員主要做了哪些編碼:
          1. 寫了一份sqlmap.xml配置文件
          2. 通過getSqlMapClientTemplate.doSomething($sqlID,$param), 執(zhí)行語(yǔ)句
          (當(dāng)然,沒有使用spring的同學(xué),也是使用了類似sqlMapClient.doSomething($sqlID,$param)方法)

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

          而我所謂的另類思路,就是基于這個(gè)假設(shè),得出的:
          IBatis下,DAO單元測(cè)試,我們拋棄背負(fù)的數(shù)據(jù)庫(kù)環(huán)境,只要根據(jù)不同的條件,斷言不同的sql即可.

          于是乎,封裝了一個(gè)IbatisSqlTester,可以根據(jù)sqlmap中的statement和傳入的條件參數(shù),生成sql語(yǔ)句.
          那么,DAO單元測(cè)試就簡(jiǎn)單了,脫離下數(shù)據(jù)庫(kù)環(huán)境:
          public class ScoreDAOTest extends TestCase {
           
              @SpringBeanByName
              
          private IbatisSqlTester ibatisSqlTester;  //通過spring配置,需要注入sqlmapclient對(duì)象
           
              @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關(guān)鍵詞
                  SqlAssert.keyWith(sql.toString(), "member_id""stone""stone2083");
                  
          // sql符合某個(gè) 正則
                  SqlAssert.regexWith(".* where member_id in .*", sql.toString());
                  
                  
          //其中,SqlAssert也可以換 成want.string()中的方法.
              }
          }

          優(yōu)勢(shì):
            脫離了數(shù)據(jù)庫(kù)環(huán)境
            脫離了表結(jié)構(gòu)數(shù)據(jù)準(zhǔn)備
            脫離了預(yù)計(jì)結(jié)果數(shù)據(jù)準(zhǔn)備
            讓單元測(cè)試變成sql的斷言,編寫相對(duì)更簡(jiǎn)單
          缺點(diǎn):
           
          row mapper過程無(wú)法被測(cè)試


          最后,附上兩個(gè)核心的代碼類(還未完成),供大家參考:
          SqlStatement.java
          /**
           * <pre>
           * SqlStatement:Sql語(yǔ)句對(duì)象.
           * 包含:
           *  1.sql語(yǔ)句,類似  select * from offer where id = ? and member_id = ?
           *  2.參數(shù)值,類似 [1,stone2083]
           *  
           *  toString方法,返回執(zhí)行的sql語(yǔ)句,如:
           *  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參數(shù)
              private Object[] param;

              
          /**
               * <pre>
               * 輸出最終執(zhí)行的sql內(nèi)容.
               * 將sql和param進(jìn)行merge,產(chǎn)生最終執(zhí)行的sql語(yǔ)句
               * </pre>
               
          */
              @Override
              
          public String toString() {
                  
          return merge();
              }

              
          /**
               * <pre>
               * 將sql進(jìn)行格式化.
               * 
               * 目前只是簡(jiǎn)單進(jìn)行格式化.去除前后空格,已經(jīng)重復(fù)空格
               * TODO:請(qǐng)使用統(tǒng)一格式化標(biāo)準(zhǔn)規(guī),建議使用SqlFormater類,進(jìn)行處理
               * </pre>
               * 
               * 
          @param sql
               * 
          @return
               
          */
              
          protected String format(String sql) {
                  
          if (sql == null) {
                      
          return null;
                  }
                  
          return sql.toLowerCase().trim().replaceAll("\\s{1,}"" ");
              }

              
          /**
               * <pre>
               * 將sql和param進(jìn)行merge.
               * TODO:請(qǐng)嚴(yán)格按照SQL標(biāo)準(zhǔn),進(jìn)行merge sql內(nèi)容
               * </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 測(cè)試
           * 一般IBatis DAO單元測(cè)試,主要就是在測(cè)試ibatis的配置文件.
           * IbatisSqlTester將根據(jù)提供的Sql Map Id 和 對(duì)應(yīng)的參數(shù),返回 {
          @link SqlStatement}對(duì)象,提供最終執(zhí)行的sql語(yǔ)句
           * 通過外部SqlAssert對(duì)象,將預(yù)計(jì)Sql和實(shí)際產(chǎn)生的Sql進(jìn)行對(duì)比,判斷是否正確
           * </pre>
           * 
           * 
          @author Stone.J 2010-8-9 下午02:58:46
           
          */
          public class IbatisSqlTester {

              
          // sqlMapClient
              private ExtendedSqlMapClient sqlMapClient;

              
          /**
               * 根據(jù)提供的SqlMap ID,得到 {
          @link SqlStatement}對(duì)象
               * 
               * 
          @param sqlId: sql map id
               * 
          @return @see {@link SqlStatement}
               
          */
              
          public SqlStatement test(String sqlId) {
                  
          //得到MappedStatement對(duì)象
                  MappedStatement ms = sqlMapClient.getMappedStatement(sqlId);
                  
          if (ms == null) {
                      
          //TODO:建議封轉(zhuǎn)自己的異常對(duì)象
                      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);

                  
          //組轉(zhuǎn)返回對(duì)象
                  SqlStatement ret = new SqlStatement();
                  ret.setSql(sqlValue);
                  
          return ret;
              }

              
          /**
               * 根據(jù)提供的SqlMap ID和對(duì)應(yīng)的param信息,得到 {
          @link SqlStatement}對(duì)象
               * 
               * 
          @param sqlId: sql map id
               * 
          @param param: 參數(shù)內(nèi)容
               * 
          @return @see {@link SqlStatement}
               
          */
              
          public SqlStatement test(String sqlId, Object param) {
                  
          //得到MappedStatement對(duì)象
                  MappedStatement ms = sqlMapClient.getMappedStatement(sqlId);
                  
          if (ms == null) {
                      
          //TODO:建議封轉(zhuǎn)自己的異常對(duì)象
                      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);

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

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

              
          /**
               * <pre>
               * 不推薦使用
               * 推薦使用: {
          @link IbatisSqlTester#setSqlMapClient(ExtendedSqlMapClient)}
               * TODO:請(qǐng)去除這個(gè)方法,或者增加初始化的方式
               * </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) {
                          }
                      }
                  }
              }

          }


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

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

          Feedback

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

          可以看看DDStep
          http://www.ddsteps.org/  回復(fù)  更多評(píng)論   

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

          lz 很有想法  回復(fù)  更多評(píng)論   

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

          @kylin
          簡(jiǎn)單地看了下ddsteps,它是一套集成測(cè)試工具.主要包括:
          dbunit,selenium,mock web(http) server,easymock和spring.
          它對(duì)DB的支持,也是采用傳統(tǒng)的方式.(沒猜錯(cuò)的話,是使用了,最多封裝了dbunit)
          并不能解決我現(xiàn)在遇到的問題:
          1.背負(fù)數(shù)據(jù)庫(kù)環(huán)境
          2.準(zhǔn)備測(cè)試數(shù)據(jù)
          3.準(zhǔn)備預(yù)計(jì)結(jié)果數(shù)據(jù)
          這些麻煩的工作量.

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

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

          @sgz
          大多數(shù)時(shí)候,想法是被現(xiàn)實(shí)逼出來(lái)的 :)
          在我們這邊,dao幾乎沒有復(fù)雜的業(yè)務(wù)邏輯,僅僅是對(duì)SqlMapClientTemplate的使用而已.
          但是在針對(duì)DB的單元測(cè)試時(shí),代價(jià)又是如此的巨大(主要還是在數(shù)據(jù)準(zhǔn)備上).
          成本,收益比,不劃算,開發(fā)們抱怨多.

          分析現(xiàn)狀和開發(fā)們實(shí)際需求(寫dao,主要是擔(dān)心sql寫錯(cuò))后,才萌生了這個(gè)想法.  回復(fù)  更多評(píng)論   

          # re: IBatis下DAO單元測(cè)試另類思路 2010-08-13 07:51 藍(lán)劍

          @stone2083
          既然業(yè)務(wù)邏輯不復(fù)雜,那么準(zhǔn)備的數(shù)據(jù)就不會(huì)很復(fù)雜,那還在乎這點(diǎn)工作量?
          sql既然都能測(cè)試了,還在乎這點(diǎn)數(shù)據(jù)準(zhǔn)備?
          數(shù)據(jù)映射都不測(cè)試,還用ibatis干什么,直接jdbc就是了  回復(fù)  更多評(píng)論   

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

          @藍(lán)劍
          DAO業(yè)務(wù)邏輯不復(fù)雜只指:在DAO方法中,不會(huì)有復(fù)雜的分支流程,往往只會(huì)調(diào)用一條SqlID執(zhí)行sql.但是這不意味sql不復(fù)雜.
          打個(gè)比方,報(bào)表的生成,業(yè)務(wù)邏輯非常簡(jiǎn)單(根據(jù)什么樣的條件,能看到什么樣的數(shù)據(jù)),但是sql絕對(duì)的復(fù)雜. :)
          dao方法,也只會(huì)有一句 getSqlMapClientTemplate.queryForList("....",param); // 簡(jiǎn)單吧 :)

          數(shù)據(jù)準(zhǔn)備的工作量低嗎?維護(hù)成本低嗎?至少在我實(shí)踐的項(xiàng)目中,沒有像sample那樣低(dbfit,dbunit,dbutil等sample,都是單表的說明,單表字段往往少于5個(gè)字段). 實(shí)際情況是:
          1.字段多
          2.表關(guān)聯(lián) (尤其在tree結(jié)構(gòu)的表,父節(jié)點(diǎn)的依賴,光是這樣的準(zhǔn)備,都非常容易寫錯(cuò))
          3.對(duì)于查詢的語(yǔ)句(尤其是分頁(yè)),需要根據(jù)動(dòng)態(tài)條件,準(zhǔn)備好需要的數(shù)據(jù)
          4.數(shù)據(jù)準(zhǔn)備意圖需要被傳承
          在我看來(lái),并且實(shí)踐過來(lái),挺不容易的.

          sql的assert,只要根據(jù)條件參數(shù)的不同,做不同預(yù)計(jì)sql的assert,成本絕對(duì)比結(jié)果數(shù)據(jù)校驗(yàn),來(lái)得低.

          至于最后一點(diǎn).
          是用ibatis,還是jdbc;不是單元測(cè)試成本(方案)決定的;而是需求,應(yīng)用,架構(gòu)設(shè)計(jì),部門崗位情況等決定的.
          我們有專業(yè)的dba,對(duì)所有sql要做review,總不能給一堆jdbc的文件給他們吧.ibatis就挺好了.
          再說了,對(duì)于ibatis下dao編碼,錯(cuò)誤率是sql寫錯(cuò)的高呢?還是row mapper錯(cuò)誤高呢?所以如果因?yàn)檫@點(diǎn),來(lái)否決全部,挺不公平的.  回復(fù)  更多評(píng)論   

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

          提供這個(gè)思路,并不是說替代之前的方案,更不是對(duì)傳統(tǒng)測(cè)試方案的否定. 僅僅是為了多一種選擇.

            回復(fù)  更多評(píng)論   

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

          說說我們的單元測(cè)試:
          DAO的代碼是工具自動(dòng)生成的,后臺(tái)是用iBtais實(shí)現(xiàn)的,sqlmap也是自動(dòng)生成,所以不用單元測(cè)試,因?yàn)槟J蕉际且粯拥模_發(fā)人員不必寫SQL。
          Service API調(diào)用DAO接口,完成業(yè)務(wù)邏輯,這部分是需要做單元測(cè)試的,使用DDStep框架,測(cè)試的框架代碼也是自動(dòng)生成,開發(fā)人員在框架代碼的基礎(chǔ)上需要做以下幾件事:
          1.準(zhǔn)備測(cè)試用例(準(zhǔn)備數(shù)據(jù)庫(kù)結(jié)構(gòu),測(cè)試數(shù)據(jù)輸入)
          2.編寫結(jié)果校驗(yàn)代碼(結(jié)果數(shù)據(jù)輸出)
          其中:準(zhǔn)備數(shù)據(jù)庫(kù)結(jié)構(gòu),測(cè)試數(shù)據(jù)輸入都是通過excel完成,容易修改,不用編代碼,看代碼(DDStep框架提供的機(jī)制)

          比較費(fèi)時(shí)的就是校驗(yàn)代碼的編寫。  回復(fù)  更多評(píng)論   

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

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

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


          難點(diǎn)還在DAO的測(cè)試上.
          數(shù)據(jù)準(zhǔn)備的復(fù)雜度,取決于表設(shè)計(jì)的復(fù)雜度和sql的復(fù)雜度.尤其在ibatis支持dynamic語(yǔ)句下,要準(zhǔn)備覆蓋測(cè)試sql語(yǔ)句的數(shù)據(jù).挺繁瑣的.
          這并不是說使用excel,還是wiki等的問題.而是數(shù)據(jù)內(nèi)容的準(zhǔn)備上.

          每個(gè)測(cè)試數(shù)據(jù)的準(zhǔn)備,都是為了一個(gè)特定的testcase設(shè)計(jì)目的的.而當(dāng)字段多,并且表設(shè)計(jì)相對(duì)復(fù)雜的時(shí)候,這個(gè)準(zhǔn)備意圖,挺難被傳承下去的.
          隨著項(xiàng)目,小需求的進(jìn)行,我們這邊,差不多幾十人,都有可能修改同一個(gè)sql代碼 :(  回復(fù)  更多評(píng)論   

          主站蜘蛛池模板: 珠海市| 隆尧县| 庄河市| 蒙自县| 中西区| 阜新| 仙游县| 左云县| 利津县| 江门市| 温宿县| 连平县| 舟曲县| 天柱县| 苍山县| 新巴尔虎右旗| 吉木萨尔县| 汉阴县| 蒙阴县| 静海县| 兴山县| 富川| 临朐县| 砚山县| 右玉县| 监利县| 青海省| 平陆县| 卢氏县| 斗六市| 南汇区| 金门县| 蚌埠市| 安义县| 芜湖市| 神池县| 唐海县| 华阴市| 丹凤县| 潮安县| 缙云县|