JAVA—咖啡館

          ——?dú)g迎訪(fǎng)問(wèn)rogerfan的博客,常來(lái)《JAVA——咖啡館》坐坐,喝杯濃香的咖啡,彼此探討一下JAVA技術(shù),交流工作經(jīng)驗(yàn),分享JAVA帶來(lái)的快樂(lè)!本網(wǎng)站部分轉(zhuǎn)載文章,如果有版權(quán)問(wèn)題請(qǐng)與我聯(lián)系。

          BlogJava 首頁(yè) 新隨筆 聯(lián)系 聚合 管理
            447 Posts :: 145 Stories :: 368 Comments :: 0 Trackbacks
          本文主要介紹了如何使用 JUnit 4 提供的各種功能開(kāi)展有效的單元測(cè)試,并通過(guò)一個(gè)實(shí)例演示了如何使用 Ant 執(zhí)行自動(dòng)化的單元測(cè)試。本文假設(shè)讀者對(duì) Eclipse 下進(jìn)行 Java 開(kāi)發(fā)有一定的經(jīng)驗(yàn),并了解 Java 5 中的注解(annotation)特性。

          引言

          毋庸置疑,程序員要對(duì)自己編寫(xiě)的代碼負(fù)責(zé),您不僅要保證它能通過(guò)編譯,正常地運(yùn)行,而且要滿(mǎn)足需求和設(shè)計(jì)預(yù)期的效果。單元測(cè)試正是驗(yàn)證代碼行為是否滿(mǎn)足預(yù)期的有效手段之一。但不可否認(rèn),做測(cè)試是件很枯燥無(wú)趣的事情,而一遍又一遍的測(cè)試則更是讓人生畏的工作。幸運(yùn)的是,單元測(cè)試工具 JUnit 使這一切變得簡(jiǎn)單藝術(shù)起來(lái)。

          JUnit 是 Java 社區(qū)中知名度最高的單元測(cè)試工具。它誕生于 1997 年,由 Erich Gamma 和 Kent Beck 共同開(kāi)發(fā)完成。其中 Erich Gamma 是經(jīng)典著作《設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》一書(shū)的作者之一,并在 Eclipse 中有很大的貢獻(xiàn);Kent Beck 則是一位極限編程(XP)方面的專(zhuān)家和先驅(qū)。

          麻雀雖小,五臟俱全。JUnit 設(shè)計(jì)的非常小巧,但是功能卻非常強(qiáng)大。Martin Fowler 如此評(píng)價(jià) JUnit:在軟件開(kāi)發(fā)領(lǐng)域,從來(lái)就沒(méi)有如此少的代碼起到了如此重要的作用。它大大簡(jiǎn)化了開(kāi)發(fā)人員執(zhí)行單元測(cè)試的難度,特別是 JUnit 4 使用 Java 5 中的注解(annotation)使測(cè)試變得更加簡(jiǎn)單。





          JUnit 4 初體驗(yàn)

          在開(kāi)始體驗(yàn) JUnit 4 之前,我們需要以下軟件的支持:

          • Eclipse:最為流行的 IDE,它全面集成了 JUnit,并從版本 3.2 開(kāi)始支持 JUnit 4。當(dāng)然 JUnit 并不依賴(lài)于任何 IDE。您可以從 http://www.eclipse.org/ 上下載最新的 Eclipse 版本。
          • Ant:基于 Java 的開(kāi)源構(gòu)建工具,您可以在 http://ant.apache.org/ 上得到最新的版本和豐富的文檔。Eclipse 中已經(jīng)集成了 Ant,但是在撰寫(xiě)本文時(shí),Eclipse 使用的 Ant 版本較低(必需 1.7 或者以上版本),不能很好的支持 JUnit 4。
          • JUnit:它的官方網(wǎng)站是 http://www.junit.org/。您可以從上面獲取關(guān)于 JUnit 的最新消息。如果您和本文一樣在 Eclipse 中使用 JUnit,就不必再下載了。

          首先為我們的體驗(yàn)新建一個(gè) Java 工程 —— coolJUnit。現(xiàn)在需要做的是,打開(kāi)項(xiàng)目 coolJUnit 的屬性頁(yè) -> 選擇“Java Build Path”子選項(xiàng) -> 點(diǎn)選“Add Library…”按鈕 -> 在彈出的“Add Library”對(duì)話(huà)框中選擇 JUnit(圖1),并在下一頁(yè)中選擇版本 4.1 后點(diǎn)擊“Finish”按鈕。這樣便把 JUnit 引入到當(dāng)前項(xiàng)目庫(kù)中了。


          圖1 為項(xiàng)目添加 JUnit 庫(kù)
          圖1 為項(xiàng)目添加 JUnit 庫(kù)
          請(qǐng)注意 JDK 的版本

          JUnit 4.1 是基于 Java 5 的升級(jí)版本,它使用了 Tiger 中的很多新特性來(lái)簡(jiǎn)化原有的使用方式。正因?yàn)槿绱耍⒉荒苤苯舆\(yùn)行在 JDK1.4.x 版本上。如果您需要在 JDK1.4.x 版本使用 JUnit 的話(huà),請(qǐng)使用 3.8.1 版本。

          可以開(kāi)始編寫(xiě)單元測(cè)試了嗎?等等……,您打算把單元測(cè)試代碼放在什么地方呢?把它和被測(cè)試代碼混在一起,這顯然會(huì)照成混亂,因?yàn)閱卧獪y(cè)試代碼是不會(huì)出現(xiàn)在最終產(chǎn)品中的。建議您分別為單元測(cè)試代碼與被測(cè)試代碼創(chuàng)建單獨(dú)的目錄,并保證測(cè)試代碼和被測(cè)試代碼使用相同的包名。這樣既保證了代碼的分離,同時(shí)還保證了查找的方便。遵照這條原則,我們?cè)陧?xiàng)目 coolJUnit 根目錄下添加一個(gè)新目錄 testsrc,并把它加入到項(xiàng)目源代碼目錄中(加入方式見(jiàn) 圖2)。


          圖2 修改項(xiàng)目源代碼目錄
          圖2 修改項(xiàng)目源代碼目錄

          現(xiàn)在我們得到了一條 JUnit 的最佳實(shí)踐:?jiǎn)卧獪y(cè)試代碼和被測(cè)試代碼使用一樣的包,不同的目錄。

          一切準(zhǔn)備就緒,一起開(kāi)始體驗(yàn)如何使用 JUnit 進(jìn)行單元測(cè)試吧。下面的例子來(lái)自筆者的開(kāi)發(fā)實(shí)踐:工具類(lèi) WordDealUtil 中的靜態(tài)方法 wordFormat4DB 是專(zhuān)用于處理 Java 對(duì)象名稱(chēng)向數(shù)據(jù)庫(kù)表名轉(zhuǎn)換的方法(您可以在代碼注釋中可以得到更多詳細(xì)的內(nèi)容)。下面是第一次編碼完成后大致情形:

          package com.ai92.cooljunit;
                      import java.util.regex.Matcher;
                      import java.util.regex.Pattern;
                      /**
                      * 對(duì)名稱(chēng)、地址等字符串格式的內(nèi)容進(jìn)行格式檢查
                      * 或者格式化的工具類(lèi)
                      *
                      * @author Ai92
                      */
                      public class WordDealUtil {
                      /**
                      * 將Java對(duì)象名稱(chēng)(每個(gè)單詞的頭字母大寫(xiě))按照
                      * 數(shù)據(jù)庫(kù)命名的習(xí)慣進(jìn)行格式化
                      * 格式化后的數(shù)據(jù)為小寫(xiě)字母,并且使用下劃線(xiàn)分割命名單詞
                      *
                      * 例如:employeeInfo 經(jīng)過(guò)格式化之后變?yōu)?employee_info
                      *
                      * @param name	Java對(duì)象名稱(chēng)
                      */
                      public static String wordFormat4DB(String name){
                      Pattern p = Pattern.compile("[A-Z]");
                      Matcher m = p.matcher(name);
                      StringBuffer sb = new StringBuffer();
                      while(m.find()){
                      m.appendReplacement(sb, "_"+m.group());
                      }
                      return m.appendTail(sb).toString().toLowerCase();
                      }
                      }
                      

          它是否能按照預(yù)期的效果執(zhí)行呢?嘗試為它編寫(xiě) JUnit 單元測(cè)試代碼如下:

          package com.ai92.cooljunit;
                      import static org.junit.Assert.assertEquals;
                      import org.junit.Test;
                      public class TestWordDealUtil {
                      //測(cè)試wordFormat4DB正常運(yùn)行的情況
                      @Test public void wordFormat4DBNormal(){
                      String target = "employeeInfo";
                      String result = WordDealUtil.wordFormat4DB(target);
                      assertEquals("employee_info", result);
                      }
                      }
                      

          很普通的一個(gè)類(lèi)嘛!測(cè)試類(lèi) TestWordDealUtil 之所以使用“Test”開(kāi)頭,完全是為了更好的區(qū)分測(cè)試類(lèi)與被測(cè)試類(lèi)。測(cè)試方法 wordFormat4DBNormal 調(diào)用執(zhí)行被測(cè)試方法 WordDealUtil.wordFormat4DB,以判斷運(yùn)行結(jié)果是否達(dá)到設(shè)計(jì)預(yù)期的效果。需要注意的是,測(cè)試方法 wordFormat4DBNormal 需要按照一定的規(guī)范書(shū)寫(xiě):

          1. 測(cè)試方法必須使用注解 org.junit.Test 修飾。
          2. 測(cè)試方法必須使用 public void 修飾,而且不能帶有任何參數(shù)。

          測(cè)試方法中要處理的字符串為“employeeInfo”,按照設(shè)計(jì)目的,處理后的結(jié)果應(yīng)該為“employee_info”。assertEquals 是由 JUnit 提供的一系列判斷測(cè)試結(jié)果是否正確的靜態(tài)斷言方法(位于類(lèi) org.junit.Assert 中)之一,我們使用它將執(zhí)行結(jié)果 result 和預(yù)期值“employee_info”進(jìn)行比較,來(lái)判斷測(cè)試是否成功。

          看看運(yùn)行結(jié)果如何。在測(cè)試類(lèi)上點(diǎn)擊右鍵,在彈出菜單中選擇 Run As JUnit Test。運(yùn)行結(jié)果如下圖所示:


          圖3 JUnit 運(yùn)行成功界面
          圖3 JUnit 運(yùn)行成功界面

          綠色的進(jìn)度條提示我們,測(cè)試運(yùn)行通過(guò)了。但現(xiàn)在就宣布代碼通過(guò)了單元測(cè)試還為時(shí)過(guò)早。記住:您的單元測(cè)試代碼不是用來(lái)證明您是對(duì)的,而是為了證明您沒(méi)有錯(cuò)。因此單元測(cè)試的范圍要全面,比如對(duì)邊界值、正常值、錯(cuò)誤值得測(cè)試;對(duì)代碼可能出現(xiàn)的問(wèn)題要全面預(yù)測(cè),而這也正是需求分析、詳細(xì)設(shè)計(jì)環(huán)節(jié)中要考慮的。顯然,我們的測(cè)試才剛剛開(kāi)始,繼續(xù)補(bǔ)充一些對(duì)特殊情況的測(cè)試:

          public class TestWordDealUtil {
                      ……
                      //測(cè)試 null 時(shí)的處理情況
                      @Test public void wordFormat4DBNull(){
                      String target = null;
                      String result = WordDealUtil.wordFormat4DB(target);
                      assertNull(result);
                      }
                      //測(cè)試空字符串的處理情況
                      @Test public void wordFormat4DBEmpty(){
                      String target = "";
                      String result = WordDealUtil.wordFormat4DB(target);
                      assertEquals("", result);
                      }
                      //測(cè)試當(dāng)首字母大寫(xiě)時(shí)的情況
                      @Test public void wordFormat4DBegin(){
                      String target = "EmployeeInfo";
                      String result = WordDealUtil.wordFormat4DB(target);
                      assertEquals("employee_info", result);
                      }
                      //測(cè)試當(dāng)尾字母為大寫(xiě)時(shí)的情況
                      @Test public void wordFormat4DBEnd(){
                      String target = "employeeInfoA";
                      String result = WordDealUtil.wordFormat4DB(target);
                      assertEquals("employee_info_a", result);
                      }
                      //測(cè)試多個(gè)相連字母大寫(xiě)時(shí)的情況
                      @Test public void wordFormat4DBTogether(){
                      String target = "employeeAInfo";
                      String result = WordDealUtil.wordFormat4DB(target);
                      assertEquals("employee_a_info", result);
                      }
                      }
                      

          再次運(yùn)行測(cè)試。很遺憾,JUnit 運(yùn)行界面提示我們有兩個(gè)測(cè)試情況未通過(guò)測(cè)試(圖4)——當(dāng)首字母大寫(xiě)時(shí)得到的處理結(jié)果與預(yù)期的有偏差,造成測(cè)試失敗(failure);而當(dāng)測(cè)試對(duì) null 的處理結(jié)果時(shí),則直接拋出了異常——測(cè)試錯(cuò)誤(error)。顯然,被測(cè)試代碼中并沒(méi)有對(duì)首字母大寫(xiě)和 null 這兩種特殊情況進(jìn)行處理,修改如下:

          //修改后的方法wordFormat4DB
                      /**
                      * 將Java對(duì)象名稱(chēng)(每個(gè)單詞的頭字母大寫(xiě))按照
                      * 數(shù)據(jù)庫(kù)命名的習(xí)慣進(jìn)行格式化
                      * 格式化后的數(shù)據(jù)為小寫(xiě)字母,并且使用下劃線(xiàn)分割命名單詞
                      * 如果參數(shù)name為null,則返回null
                      *
                      * 例如:employeeInfo 經(jīng)過(guò)格式化之后變?yōu)?employee_info
                      *
                      * @param name Java對(duì)象名稱(chēng)
                      */
                      public static String wordFormat4DB(String name){
                      if(name == null){
                      return null;
                      }
                      Pattern p = Pattern.compile("[A-Z]");
                      Matcher m = p.matcher(name);
                      StringBuffer sb = new StringBuffer();
                      while(m.find()){
                      if(m.start() != 0)
                      m.appendReplacement(sb, ("_"+m.group()).toLowerCase());
                      }
                      return m.appendTail(sb).toString().toLowerCase();
                      }
                      


          圖4 JUnit 運(yùn)行失敗界面
          圖4 JUnit 運(yùn)行失敗界面

          JUnit 將測(cè)試失敗的情況分為兩種:failure 和 error。Failure 一般由單元測(cè)試使用的斷言方法判斷失敗引起,它表示在測(cè)試點(diǎn)發(fā)現(xiàn)了問(wèn)題;而 error 則是由代碼異常引起,這是測(cè)試目的之外的發(fā)現(xiàn),它可能產(chǎn)生于測(cè)試代碼本身的錯(cuò)誤(測(cè)試代碼也是代碼,同樣無(wú)法保證完全沒(méi)有缺陷),也可能是被測(cè)試代碼中的一個(gè)隱藏的bug。

          請(qǐng)牢記!

          請(qǐng)牢記這一條 JUnit 最佳實(shí)踐:測(cè)試任何可能的錯(cuò)誤。單元測(cè)試不是用來(lái)證明您是對(duì)的,而是為了證明您沒(méi)有錯(cuò)。

          啊哈,再次運(yùn)行測(cè)試,綠條又重現(xiàn)眼前。通過(guò)對(duì) WordDealUtil.wordFormat4DB 比較全面的單元測(cè)試,現(xiàn)在的代碼已經(jīng)比較穩(wěn)定,可以作為 API 的一部分提供給其它模塊使用了。

          不知不覺(jué)中我們已經(jīng)使用 JUnit 漂亮的完成了一次單元測(cè)試。可以體會(huì)到 JUnit 是多么輕量級(jí),多么簡(jiǎn)單,根本不需要花心思去研究,這就可以把更多的注意力放在更有意義的事情上——編寫(xiě)完整全面的單元測(cè)試。





          JUnit 深入

          當(dāng)然,JUnit 提供的功能決不僅僅如此簡(jiǎn)單,在接下來(lái)的內(nèi)容中,我們會(huì)看到 JUnit 中很多有用的特性,掌握它們對(duì)您靈活的編寫(xiě)單元測(cè)試代碼非常有幫助。

          Fixture

          何謂 Fixture?它是指在執(zhí)行一個(gè)或者多個(gè)測(cè)試方法時(shí)需要的一系列公共資源或者數(shù)據(jù),例如測(cè)試環(huán)境,測(cè)試數(shù)據(jù)等等。在編寫(xiě)單元測(cè)試的過(guò)程中,您會(huì)發(fā)現(xiàn)在大部分的測(cè)試方法在進(jìn)行真正的測(cè)試之前都需要做大量的鋪墊——為設(shè)計(jì)準(zhǔn)備 Fixture 而忙碌。這些鋪墊過(guò)程占據(jù)的代碼往往比真正測(cè)試的代碼多得多,而且這個(gè)比率隨著測(cè)試的復(fù)雜程度的增加而遞增。當(dāng)多個(gè)測(cè)試方法都需要做同樣的鋪墊時(shí),重復(fù)代碼的“壞味道”便在測(cè)試代碼中彌漫開(kāi)來(lái)。這股“壞味道”會(huì)弄臟您的代碼,還會(huì)因?yàn)槭韬鲈斐慑e(cuò)誤,應(yīng)該使用一些手段來(lái)根除它。

          JUnit 專(zhuān)門(mén)提供了設(shè)置公共 Fixture 的方法,同一測(cè)試類(lèi)中的所有測(cè)試方法都可以共用它來(lái)初始化 Fixture 和注銷(xiāo) Fixture。和編寫(xiě) JUnit 測(cè)試方法一樣,公共 Fixture 的設(shè)置也很簡(jiǎn)單,您只需要:

          1. 使用注解 org,junit.Before 修飾用于初始化 Fixture 的方法。
          2. 使用注解 org.junit.After 修飾用于注銷(xiāo) Fixture 的方法。
          3. 保證這兩種方法都使用 public void 修飾,而且不能帶有任何參數(shù)。

          遵循上面的三條原則,編寫(xiě)出的代碼大體是這個(gè)樣子:

          //初始化Fixture方法
                      @Before public void init(){……}
                      //注銷(xiāo)Fixture方法
                      @After public void destroy(){……}
                      

          這樣,在每一個(gè)測(cè)試方法執(zhí)行之前,JUnit 會(huì)保證 init 方法已經(jīng)提前初始化測(cè)試環(huán)境,而當(dāng)此測(cè)試方法執(zhí)行完畢之后,JUnit 又會(huì)調(diào)用 destroy 方法注銷(xiāo)測(cè)試環(huán)境。注意是每一個(gè)測(cè)試方法的執(zhí)行都會(huì)觸發(fā)對(duì)公共 Fixture 的設(shè)置,也就是說(shuō)使用注解 Before 或者 After 修飾的公共 Fixture 設(shè)置方法是方法級(jí)別的(圖5)。這樣便可以保證各個(gè)獨(dú)立的測(cè)試之間互不干擾,以免其它測(cè)試代碼修改測(cè)試環(huán)境或者測(cè)試數(shù)據(jù)影響到其它測(cè)試代碼的準(zhǔn)確性。


          圖5 方法級(jí)別 Fixture 執(zhí)行示意圖
          圖5 方法級(jí)別 Fixture 執(zhí)行示意圖

          可是,這種 Fixture 設(shè)置方式還是引來(lái)了批評(píng),因?yàn)樗实拖拢貏e是在設(shè)置 Fixture 非常耗時(shí)的情況下(例如設(shè)置數(shù)據(jù)庫(kù)鏈接)。而且對(duì)于不會(huì)發(fā)生變化的測(cè)試環(huán)境或者測(cè)試數(shù)據(jù)來(lái)說(shuō),是不會(huì)影響到測(cè)試方法的執(zhí)行結(jié)果的,也就沒(méi)有必要針對(duì)每一個(gè)測(cè)試方法重新設(shè)置一次 Fixture。因此在 JUnit 4 中引入了類(lèi)級(jí)別的 Fixture 設(shè)置方法,編寫(xiě)規(guī)范如下:

          1. 使用注解 org,junit.BeforeClass 修飾用于初始化 Fixture 的方法。
          2. 使用注解 org.junit.AfterClass 修飾用于注銷(xiāo) Fixture 的方法。
          3. 保證這兩種方法都使用 public static void 修飾,而且不能帶有任何參數(shù)。

          類(lèi)級(jí)別的 Fixture 僅會(huì)在測(cè)試類(lèi)中所有測(cè)試方法執(zhí)行之前執(zhí)行初始化,并在全部測(cè)試方法測(cè)試完畢之后執(zhí)行注銷(xiāo)方法(圖6)。代碼范本如下:

          //類(lèi)級(jí)別Fixture初始化方法
                      @BeforeClass public static void dbInit(){……}
                      //類(lèi)級(jí)別Fixture注銷(xiāo)方法
                      @AfterClass public static void dbClose(){……}
                      


          圖6 類(lèi)級(jí)別 Fixture 執(zhí)行示意圖
          圖6 類(lèi)級(jí)別 Fixture 執(zhí)行示意圖

          異常以及時(shí)間測(cè)試

          注解 org.junit.Test 中有兩個(gè)非常有用的參數(shù):expected 和 timeout。參數(shù) expected 代表測(cè)試方法期望拋出指定的異常,如果運(yùn)行測(cè)試并沒(méi)有拋出這個(gè)異常,則 JUnit 會(huì)認(rèn)為這個(gè)測(cè)試沒(méi)有通過(guò)。這為驗(yàn)證被測(cè)試方法在錯(cuò)誤的情況下是否會(huì)拋出預(yù)定的異常提供了便利。舉例來(lái)說(shuō),方法 supportDBChecker 用于檢查用戶(hù)使用的數(shù)據(jù)庫(kù)版本是否在系統(tǒng)的支持的范圍之內(nèi),如果用戶(hù)使用了不被支持的數(shù)據(jù)庫(kù)版本,則會(huì)拋出運(yùn)行時(shí)異常 UnsupportedDBVersionException。測(cè)試方法 supportDBChecker 在數(shù)據(jù)庫(kù)版本不支持時(shí)是否會(huì)拋出指定異常的單元測(cè)試方法大體如下:

          @Test(expected=UnsupportedDBVersionException.class)
                      public void unsupportedDBCheck(){
                      ……
                      }
                      

          注解 org.junit.Test 的另一個(gè)參數(shù) timeout,指定被測(cè)試方法被允許運(yùn)行的最長(zhǎng)時(shí)間應(yīng)該是多少,如果測(cè)試方法運(yùn)行時(shí)間超過(guò)了指定的毫秒數(shù),則JUnit認(rèn)為測(cè)試失敗。這個(gè)參數(shù)對(duì)于性能測(cè)試有一定的幫助。例如,如果解析一份自定義的 XML 文檔花費(fèi)了多于 1 秒的時(shí)間,就需要重新考慮 XML 結(jié)構(gòu)的設(shè)計(jì),那單元測(cè)試方法可以這樣來(lái)寫(xiě):

          @Test(timeout=1000)
                      public void selfXMLReader(){
                      ……
                      }
                      

          忽略測(cè)試方法

          JUnit 提供注解 org.junit.Ignore 用于暫時(shí)忽略某個(gè)測(cè)試方法,因?yàn)橛袝r(shí)候由于測(cè)試環(huán)境受限,并不能保證每一個(gè)測(cè)試方法都能正確運(yùn)行。例如下面的代碼便表示由于沒(méi)有了數(shù)據(jù)庫(kù)鏈接,提示 JUnit 忽略測(cè)試方法 unsupportedDBCheck:

          @ Ignore(“db is down”)
                      @Test(expected=UnsupportedDBVersionException.class)
                      public void unsupportedDBCheck(){
                      ……
                      }
                      

          但是一定要小心。注解 org.junit.Ignore 只能用于暫時(shí)的忽略測(cè)試,如果需要永遠(yuǎn)忽略這些測(cè)試,一定要確認(rèn)被測(cè)試代碼不再需要這些測(cè)試方法,以免忽略必要的測(cè)試點(diǎn)。

          測(cè)試運(yùn)行器

          又一個(gè)新概念出現(xiàn)了——測(cè)試運(yùn)行器,JUnit 中所有的測(cè)試方法都是由它負(fù)責(zé)執(zhí)行的。JUnit 為單元測(cè)試提供了默認(rèn)的測(cè)試運(yùn)行器,但 JUnit 并沒(méi)有限制您必須使用默認(rèn)的運(yùn)行器。相反,您不僅可以定制自己的運(yùn)行器(所有的運(yùn)行器都繼承自 org.junit.runner.Runner),而且還可以為每一個(gè)測(cè)試類(lèi)指定使用某個(gè)具體的運(yùn)行器。指定方法也很簡(jiǎn)單,使用注解 org.junit.runner.RunWith 在測(cè)試類(lèi)上顯式的聲明要使用的運(yùn)行器即可:

          @RunWith(CustomTestRunner.class)
                      public class TestWordDealUtil {
                      ……
                      }
                      

          顯而易見(jiàn),如果測(cè)試類(lèi)沒(méi)有顯式的聲明使用哪一個(gè)測(cè)試運(yùn)行器,JUnit 會(huì)啟動(dòng)默認(rèn)的測(cè)試運(yùn)行器執(zhí)行測(cè)試類(lèi)(比如上面提及的單元測(cè)試代碼)。一般情況下,默認(rèn)測(cè)試運(yùn)行器可以應(yīng)對(duì)絕大多數(shù)的單元測(cè)試要求;當(dāng)使用 JUnit 提供的一些高級(jí)特性(例如即將介紹的兩個(gè)特性)或者針對(duì)特殊需求定制 JUnit 測(cè)試方式時(shí),顯式的聲明測(cè)試運(yùn)行器就必不可少了。

          測(cè)試套件

          在實(shí)際項(xiàng)目中,隨著項(xiàng)目進(jìn)度的開(kāi)展,單元測(cè)試類(lèi)會(huì)越來(lái)越多,可是直到現(xiàn)在我們還只會(huì)一個(gè)一個(gè)的單獨(dú)運(yùn)行測(cè)試類(lèi),這在實(shí)際項(xiàng)目實(shí)踐中肯定是不可行的。為了解決這個(gè)問(wèn)題,JUnit 提供了一種批量運(yùn)行測(cè)試類(lèi)的方法,叫做測(cè)試套件。這樣,每次需要驗(yàn)證系統(tǒng)功能正確性時(shí),只執(zhí)行一個(gè)或幾個(gè)測(cè)試套件便可以了。測(cè)試套件的寫(xiě)法非常簡(jiǎn)單,您只需要遵循以下規(guī)則:

          1. 創(chuàng)建一個(gè)空類(lèi)作為測(cè)試套件的入口。
          2. 使用注解 org.junit.runner.RunWith 和 org.junit.runners.Suite.SuiteClasses 修飾這個(gè)空類(lèi)。
          3. 將 org.junit.runners.Suite 作為參數(shù)傳入注解 RunWith,以提示 JUnit 為此類(lèi)使用套件運(yùn)行器執(zhí)行。
          4. 將需要放入此測(cè)試套件的測(cè)試類(lèi)組成數(shù)組作為注解 SuiteClasses 的參數(shù)。
          5. 保證這個(gè)空類(lèi)使用 public 修飾,而且存在公開(kāi)的不帶有任何參數(shù)的構(gòu)造函數(shù)。
          package com.ai92.cooljunit;
                      import org.junit.runner.RunWith;
                      import org.junit.runners.Suite;
                      ……
                      /**
                      * 批量測(cè)試 工具包 中測(cè)試類(lèi)
                      * @author Ai92
                      */
                      @RunWith(Suite.class)
                      @Suite.SuiteClasses({TestWordDealUtil.class})
                      public class RunAllUtilTestsSuite {
                      }
                      

          上例代碼中,我們將前文提到的測(cè)試類(lèi) TestWordDealUtil 放入了測(cè)試套件 RunAllUtilTestsSuite 中,在 Eclipse 中運(yùn)行測(cè)試套件,可以看到測(cè)試類(lèi) TestWordDealUtil 被調(diào)用執(zhí)行了。測(cè)試套件中不僅可以包含基本的測(cè)試類(lèi),而且可以包含其它的測(cè)試套件,這樣可以很方便的分層管理不同模塊的單元測(cè)試代碼。但是,您一定要保證測(cè)試套件之間沒(méi)有循環(huán)包含關(guān)系,否則無(wú)盡的循環(huán)就會(huì)出現(xiàn)在您的面前……。

          參數(shù)化測(cè)試

          回顧一下我們?cè)谛」?jié)“JUnit 初體驗(yàn)”中舉的實(shí)例。為了保證單元測(cè)試的嚴(yán)謹(jǐn)性,我們模擬了不同類(lèi)型的字符串來(lái)測(cè)試方法的處理能力,為此我們編寫(xiě)大量的單元測(cè)試方法。可是這些測(cè)試方法都是大同小異:代碼結(jié)構(gòu)都是相同的,不同的僅僅是測(cè)試數(shù)據(jù)和期望值。有沒(méi)有更好的方法將測(cè)試方法中相同的代碼結(jié)構(gòu)提取出來(lái),提高代碼的重用度,減少?gòu)?fù)制粘貼代碼的煩惱?在以前的 JUnit 版本上,并沒(méi)有好的解決方法,而現(xiàn)在您可以使用 JUnit 提供的參數(shù)化測(cè)試方式應(yīng)對(duì)這個(gè)問(wèn)題。

          參數(shù)化測(cè)試的編寫(xiě)稍微有點(diǎn)麻煩(當(dāng)然這是相對(duì)于 JUnit 中其它特性而言):

          1. 為準(zhǔn)備使用參數(shù)化測(cè)試的測(cè)試類(lèi)指定特殊的運(yùn)行器 org.junit.runners.Parameterized。
          2. 為測(cè)試類(lèi)聲明幾個(gè)變量,分別用于存放期望值和測(cè)試所用數(shù)據(jù)。
          3. 為測(cè)試類(lèi)聲明一個(gè)使用注解 org.junit.runners.Parameterized.Parameters 修飾的,返回值為 java.util.Collection 的公共靜態(tài)方法,并在此方法中初始化所有需要測(cè)試的參數(shù)對(duì)。
          4. 為測(cè)試類(lèi)聲明一個(gè)帶有參數(shù)的公共構(gòu)造函數(shù),并在其中為第二個(gè)環(huán)節(jié)中聲明的幾個(gè)變量賦值。
          5. 編寫(xiě)測(cè)試方法,使用定義的變量作為參數(shù)進(jìn)行測(cè)試。

          我們按照這個(gè)標(biāo)準(zhǔn),重新改造一番我們的單元測(cè)試代碼:

          package com.ai92.cooljunit;
                      import static org.junit.Assert.assertEquals;
                      import java.util.Arrays;
                      import java.util.Collection;
                      import org.junit.Test;
                      import org.junit.runner.RunWith;
                      import org.junit.runners.Parameterized;
                      import org.junit.runners.Parameterized.Parameters;
                      @RunWith(Parameterized.class)
                      public class TestWordDealUtilWithParam {
                      private String expected;
                      private String target;
                      @Parameters
                      public static Collection words(){
                      return Arrays.asList(new Object[][]{
                      {"employee_info", "employeeInfo"},		//測(cè)試一般的處理情況
                      {null, null},							//測(cè)試 null 時(shí)的處理情況
                      |-------10--------20--------30--------40--------50--------60--------70--------80--------9|
                      |-------- XML error:  The previous line is longer than the max of 90 characters ---------|
                      {"", ""},								//測(cè)試空字符串時(shí)的處理情況
                      |-------10--------20--------30--------40--------50--------60--------70--------80--------9|
                      |-------- XML error:  The previous line is longer than the max of 90 characters ---------|
                      {"employee_info", "EmployeeInfo"},		//測(cè)試當(dāng)首字母大寫(xiě)時(shí)的情況
                      {"employee_info_a", "employeeInfoA"},	//測(cè)試當(dāng)尾字母為大寫(xiě)時(shí)的情況
                      {"employee_a_info", "employeeAInfo"}	//測(cè)試多個(gè)相連字母大寫(xiě)時(shí)的情況
                      });
                      }
                      /**
                      * 參數(shù)化測(cè)試必須的構(gòu)造函數(shù)
                      * @param expected	期望的測(cè)試結(jié)果,對(duì)應(yīng)參數(shù)集中的第一個(gè)參數(shù)
                      * @param target	測(cè)試數(shù)據(jù),對(duì)應(yīng)參數(shù)集中的第二個(gè)參數(shù)
                      */
                      public TestWordDealUtilWithParam(String expected , String target){
                      this.expected = expected;
                      this.target = target;
                      }
                      /**
                      * 測(cè)試將 Java 對(duì)象名稱(chēng)到數(shù)據(jù)庫(kù)名稱(chēng)的轉(zhuǎn)換
                      */
                      @Test public void wordFormat4DB(){
                      assertEquals(expected, WordDealUtil.wordFormat4DB(target));
                      }
                      }
                      

          很明顯,代碼瘦身了。在靜態(tài)方法 words 中,我們使用二維數(shù)組來(lái)構(gòu)建測(cè)試所需要的參數(shù)列表,其中每個(gè)數(shù)組中的元素的放置順序并沒(méi)有什么要求,只要和構(gòu)造函數(shù)中的順序保持一致就可以了。現(xiàn)在如果再增加一種測(cè)試情況,只需要在靜態(tài)方法 words 中添加相應(yīng)的數(shù)組即可,不再需要復(fù)制粘貼出一個(gè)新的方法出來(lái)了。





          JUnit 和 Ant

          隨著項(xiàng)目的進(jìn)展,項(xiàng)目的規(guī)模在不斷的膨脹,為了保證項(xiàng)目的質(zhì)量,有計(jì)劃的執(zhí)行全面的單元測(cè)試是非常有必要的。但單靠JUnit提供的測(cè)試套件很難勝任這項(xiàng)工作,因?yàn)轫?xiàng)目中單元測(cè)試類(lèi)的個(gè)數(shù)在不停的增加,測(cè)試套件卻無(wú)法動(dòng)態(tài)的識(shí)別新加入的單元測(cè)試類(lèi),需要手動(dòng)修改測(cè)試套件,這是一個(gè)很容易遺忘得步驟,稍有疏忽就會(huì)影響全面單元測(cè)試的覆蓋率。

          當(dāng)然解決的方法有多種多樣,其中將 JUnit 與構(gòu)建利器 Ant 結(jié)合使用可以很簡(jiǎn)單的解決這個(gè)問(wèn)題。Ant —— 備受贊譽(yù)的 Java 構(gòu)建工具。它憑借出色的易用性、平臺(tái)無(wú)關(guān)性以及對(duì)項(xiàng)目自動(dòng)測(cè)試和自動(dòng)部署的支持,成為眾多項(xiàng)目構(gòu)建過(guò)程中不可或缺的獨(dú)立工具,并已經(jīng)成為事實(shí)上的標(biāo)準(zhǔn)。Ant 內(nèi)置了對(duì) JUnit 的支持,它提供了兩個(gè) Task:junit 和 junitreport,分別用于執(zhí)行 JUnit 單元測(cè)試和生成測(cè)試結(jié)果報(bào)告。使用這兩個(gè) Task 編寫(xiě)構(gòu)建腳本,可以很簡(jiǎn)單的完成每次全面單元測(cè)試的任務(wù)。

          不過(guò),在使用 Ant 運(yùn)行 JUnit 之前,您需要稍作一些配置。打開(kāi) Eclipse 首選項(xiàng)界面,選擇 Ant -> Runtime 首選項(xiàng)(見(jiàn)圖7),將 JUnit 4.1 的 JAR 文件添加到 Classpath Tab 頁(yè)中的 Global Entries 設(shè)置項(xiàng)里。記得檢查一下 Ant Home Entries 設(shè)置項(xiàng)中的 Ant 版本是否在 1.7.0 之上,如果不是請(qǐng)?zhí)鎿Q為最新版本的 Ant JAR 文件。


          圖7 Ant Runtime 首選項(xiàng)
          圖7 Ant Runtime 首選項(xiàng)

          剩下的工作就是要編寫(xiě) Ant 構(gòu)建腳本 build.xml。雖然這個(gè)過(guò)程稍嫌繁瑣,但這是一件一勞永逸的事情。現(xiàn)在我們就把前面編寫(xiě)的測(cè)試用例都放置到 Ant 構(gòu)建腳本中執(zhí)行,為項(xiàng)目 coolJUnit 的構(gòu)建腳本添加一下內(nèi)容:

          <?xml version="1.0"?>
                      <!-- =============================================
                      auto unittest task
                      ai92
                      ========================================== -->
                      <project name="auto unittest task" default="junit and report" basedir=".">
                      <property name="output folder" value="bin"/>
                      <property name="src folder" value="src"/>
                      <property name="test folder" value="testsrc"/>
                      <property name="report folder" value="report" />
                      <!-- - - - - - - - - - - - - - - - - -
                      target: test report folder init
                      - - - - - - - - - - - - - - - - - -->
                      <target name="test init">
                      <mkdir dir="${report folder}"/>
                      </target>
                      <!-- - - - - - - - - - - - - - - - - -
                      target: compile
                      - - - - - - - - - - - - - - - - - -->
                      <target name="compile">
                      <javac srcdir="${src folder}" destdir="${output folder}" />
                      <echo>compilation complete!</echo>
                      </target>
                      <!-- - - - - - - - - - - - - - - - - -
                      target: compile test cases
                      - - - - - - - - - - - - - - - - - -->
                      <target name="test compile" depends="test init">
                      <javac srcdir="${test folder}" destdir="${output folder}" />
                      <echo>test compilation complete!</echo>
                      </target>
                      <target name="all compile" depends="compile, test compile">
                      </target>
                      <!-- ========================================
                      target: auto test all test case and output report file
                      ===================================== -->
                      <target name="junit and report" depends="all compile">
                      <junit printsummary="on" fork="true" showoutput="true">
                      <classpath>
                      <fileset dir="lib" includes="**/*.jar"/>
                      <pathelement path="${output folder}"/>
                      </classpath>
                      <formatter type="xml" />
                      <batchtest todir="${report folder}">
                      <fileset dir="${output folder}">
                      <include name="**/Test*.*" />
                      </fileset>
                      </batchtest>
                      </junit>
                      <junitreport todir="${report folder}">
                      <fileset dir="${report folder}">
                      <include name="TEST-*.xml" />
                      </fileset>
                      <report format="frames" todir="${report folder}" />
                      </junitreport>
                      </target>
                      </project>
                      

          Target junit report 是 Ant 構(gòu)建腳本中的核心內(nèi)容,其它 target 都是為它的執(zhí)行提供前期服務(wù)。Task junit 會(huì)尋找輸出目錄下所有命名以“Test”開(kāi)頭的 class 文件,并執(zhí)行它們。緊接著 Task junitreport 會(huì)將執(zhí)行結(jié)果生成 HTML 格式的測(cè)試報(bào)告(圖8)放置在“report folder”下。

          為整個(gè)項(xiàng)目的單元測(cè)試類(lèi)確定一種命名風(fēng)格。不僅是出于區(qū)分類(lèi)別的考慮,這為 Ant 批量執(zhí)行單元測(cè)試也非常有幫助,比如前面例子中的測(cè)試類(lèi)都已“Test”打頭,而測(cè)試套件則以“Suite”結(jié)尾等等。


          圖8 junitreport 生成的測(cè)試報(bào)告
          圖8 junitreport 生成的測(cè)試報(bào)告

          現(xiàn)在執(zhí)行一次全面的單元測(cè)試變得非常簡(jiǎn)單了,只需要運(yùn)行一下 Ant 構(gòu)建腳本,就可以走完所有流程,并能得到一份詳盡的測(cè)試報(bào)告。您可以在 Ant 在線(xiàn)手冊(cè) 中獲得上面提及的每一個(gè) Ant 內(nèi)置 task 的使用細(xì)節(jié)。





          總結(jié)

          隨著越來(lái)越多的開(kāi)發(fā)人員開(kāi)始認(rèn)同并接受極限編程(XP)的思想,單元測(cè)試的作用在軟件工程中變得越來(lái)越重要。本文旨在將最新的單元測(cè)試工具 JUnit 4 介紹給您,以及如何結(jié)合 IDE Eclipse 和構(gòu)建工具 Ant 創(chuàng)建自動(dòng)化單元測(cè)試方案。并且還期望您能夠通過(guò)本文“感染”一些好的單元測(cè)試意識(shí),因?yàn)?JUnit 本身僅僅是一份工具而已,它的真正優(yōu)勢(shì)來(lái)自于它的思想和技術(shù)。






          下載

          描述 名字 大小 下載方法
          本文示例代碼 coolJUnit.zip 24 KB HTTP
          關(guān)于下載方法的信息


          參考資料

          學(xué)習(xí)
          • JUnit 4 搶先看(Elliotte Rusty Harold, developerWorks, 2005 年 10 月):Elliotte Rusty Harold 為大家揭開(kāi)了 JUnit 4 新框架的面紗。

          • 追求代碼質(zhì)量: JUnit 4 與 TestNG 的對(duì)比(Andrew Glover, developerWorks, 2006 年 9 月):Andrew Glover 探討了這兩種框架各自的獨(dú)特之處,并闡述了 TestNG 獨(dú)有的三種高級(jí)測(cè)試特性。

          • TestNG 使 Java 單元測(cè)試輕而易舉(Filippo Diotalevi, developerWorks, 2005 年 1 月):TestNG 不僅確實(shí)強(qiáng)大、創(chuàng)新、可擴(kuò)展、靈活,它還展示了 Java Annotations(JDK 5.0 中的重大新特性)的有趣應(yīng)用。

          • Tiger 中的注釋?zhuān)?1 部分: 向 Java 代碼中添加元數(shù)據(jù)(Brett McLaughlin, developerWorks, 2004 年 9 月):本文解釋了元數(shù)據(jù)如此有用的原因,向您介紹了 Java 語(yǔ)言中的注釋?zhuān)⒀芯苛?Tiger 的內(nèi)置注釋。

          • 利用 Ant 和 JUnit 進(jìn)行增量開(kāi)發(fā)(Malcolm Davis, developerWorks, 2000 年 11 月):本文通過(guò)使用代碼樣本說(shuō)明了單元測(cè)試的種種好處,特別是使用 Ant 和 JUnit 帶來(lái)的各種方便。

          • 追求代碼質(zhì)量系列(Andrew Glover, developerWorks):在這個(gè)系列中,Andrew Glover 將重點(diǎn)闡述有關(guān)保證代碼質(zhì)量的一些有時(shí)看上去有點(diǎn)神秘的東西。


          獲得產(chǎn)品和技術(shù)


          關(guān)于作者

           

          苑永凱,軟件設(shè)計(jì)師,畢業(yè)于山東大學(xué)。從事 Java EE 開(kāi)發(fā)三年有余,微有心得;雖筆拙,仍喜好記錄心得與眾人分享,樂(lè)此不疲。他的博客是 http://blog.csdn.net/ai92。您也可以通過(guò) yuanyk@gmail.com 與他聯(lián)系,

          posted on 2009-03-06 09:59 rogerfan 閱讀(359) 評(píng)論(0)  編輯  收藏 所屬分類(lèi): 【Java知識(shí)】【開(kāi)源技術(shù)】
          主站蜘蛛池模板: 永兴县| 桓台县| 马山县| 长汀县| 海兴县| 犍为县| 武乡县| 麻栗坡县| 阿拉善盟| 武胜县| 宁南县| 大厂| 岳普湖县| 佛学| 西宁市| 彰化市| 琼结县| 米林县| 慈利县| 松溪县| 台北市| 永川市| 尉氏县| 博野县| 余庆县| 清镇市| 大洼县| 葵青区| 武城县| 香河县| 于都县| 眉山市| 合肥市| 新津县| 太谷县| 法库县| 海盐县| 富阳市| 泉州市| 宁远县| 咸丰县|