JUnit 深刻

Fixture

何謂 Fixture ?它是指在執行一個或者多個測試方法時須要的一系列公共資源或者數據,例如測試環境,測試數據等等。在編寫單元測試的過程當中,您會發如今大部分的測試方法在進行真正的測試以前都須要作大量的鋪墊——爲設計準備 Fixture 而忙碌。這些鋪墊過程佔據的代碼每每比真正測試的代碼多得多,並且這個比率隨着測試的複雜程度的增長而遞增。當多個測試方法都須要作一樣的鋪墊時,重複代碼的「壞味道」便在測試代碼中瀰漫開來。這股「壞味道」會弄髒您的代碼,還會由於疏忽形成錯誤,應該使用一些手段來根除它。html

JUnit 專門提供了設置公共 Fixture 的方法,同一測試類中的全部測試方法均可以共用它來初始化 Fixture 和註銷 Fixture。和編寫 JUnit 測試方法同樣,公共 Fixture 的設置也很簡單,您只須要:java

  1. 使用註解 org,junit.Before 修飾用於初始化 Fixture 的方法。
  2. 使用註解 org.junit.After 修飾用於註銷 Fixture 的方法。
  3. 保證這兩種方法都使用 public void 修飾,並且不能帶有任何參數。

遵循上面的三條原則,編寫出的代碼大致是這個樣子:數據庫

1
2
3
4
5
// 初始化 Fixture 方法
@Before public void init(){ …… }
 
// 註銷 Fixture 方法
@After public void destroy(){ …… }

這樣,在每個測試方法執行以前,JUnit 會保證 init 方法已經提早初始化測試環境,而當此測試方法執行完畢以後,JUnit 又會調用 destroy 方法註銷測試環境。注意是每個測試方法的執行都會觸發對公共 Fixture 的設置,也就是說使用註解 Before 或者 After 修飾的公共 Fixture 設置方法是方法級別的(圖 5)。這樣即可以保證各個獨立的測試之間互不干擾,以避免其它測試代碼修改測試環境或者測試數據影響到其它測試代碼的準確性。數組

圖 5 方法級別 Fixture 執行示意圖

圖 5 方法級別 Fixture 執行示意圖

但是,這種 Fixture 設置方式仍是引來了批評,由於它效率低下,特別是在設置 Fixture 很是耗時的狀況下(例如設置數據庫連接)。並且對於不會發生變化的測試環境或者測試數據來講,是不會影響到測試方法的執行結果的,也就沒有必要針對每個測試方法從新設置一次 Fixture。所以在 JUnit 4 中引入了類級別的 Fixture 設置方法,編寫規範以下:函數

  1. 使用註解 org,junit.BeforeClass 修飾用於初始化 Fixture 的方法。
  2. 使用註解 org.junit.AfterClass 修飾用於註銷 Fixture 的方法。
  3. 保證這兩種方法都使用 public static void 修飾,並且不能帶有任何參數。

類級別的 Fixture 僅會在測試類中全部測試方法執行以前執行初始化,並在所有測試方法測試完畢以後執行註銷方法(圖 6)。代碼範本以下:工具

1
2
3
4
5
// 類級別 Fixture 初始化方法
@BeforeClass public static void dbInit(){ …… }
    
// 類級別 Fixture 註銷方法
     @AfterClass public static void dbClose(){ …… }
圖 6 類級別 Fixture 執行示意圖

圖 6 類級別 Fixture 執行示意圖

異常以及時間測試

註解 org.junit.Test 中有兩個很是有用的參數:expected 和 timeout。參數 expected 表明測試方法指望拋出指定的異常,若是運行測試並無拋出這個異常,則 JUnit 會認爲這個測試沒有經過。這爲驗證被測試方法在錯誤的狀況下是否會拋出預約的異常提供了便利。舉例來講,方法 supportDBChecker 用於檢查用戶使用的數據庫版本是否在系統的支持的範圍以內,若是用戶使用了不被支持的數據庫版本,則會拋出運行時異常 UnsupportedDBVersionException。測試方法 supportDBChecker 在數據庫版本不支持時是否會拋出指定異常的單元測試方法大致以下:性能

1
2
3
4
@Test(expected=UnsupportedDBVersionException.class)
     public void unsupportedDBCheck(){
        ……
}

註解 org.junit.Test 的另外一個參數 timeout,指定被測試方法被容許運行的最長時間應該是多少,若是測試方法運行時間超過了指定的毫秒數,則 JUnit 認爲測試失敗。這個參數對於性能測試有必定的幫助。例如,若是解析一份自定義的 XML 文檔花費了多於 1 秒的時間,就須要從新考慮 XML 結構的設計,那單元測試方法能夠這樣來寫:單元測試

1
2
3
4
@Test(timeout=1000)
     public void selfXMLReader(){
        ……
}

忽略測試方法

JUnit 提供註解 org.junit.Ignore 用於暫時忽略某個測試方法,由於有時候因爲測試環境受限,並不能保證每個測試方法都能正確運行。例以下面的代碼便表示因爲沒有了數據庫連接,提示 JUnit 忽略測試方法 unsupportedDBCheck:測試

1
2
3
4
5
@ Ignore(「db is down」)
@Test(expected=UnsupportedDBVersionException.class)
     public void unsupportedDBCheck(){
        ……
}

可是必定要當心。註解 org.junit.Ignore 只能用於暫時的忽略測試,若是須要永遠忽略這些測試,必定要確認被測試代碼再也不須要這些測試方法,以避免忽略必要的測試點。ui

測試運行器

又一個新概念出現了——測試運行器,JUnit 中全部的測試方法都是由它負責執行的。JUnit 爲單元測試提供了默認的測試運行器,但 JUnit 並無限制您必須使用默認的運行器。相反,您不只能夠定製本身的運行器(全部的運行器都繼承自 org.junit.runner.Runner),並且還能夠爲每個測試類指定使用某個具體的運行器。指定方法也很簡單,使用註解 org.junit.runner.RunWith 在測試類上顯式的聲明要使用的運行器便可:

1
2
3
4
@RunWith(CustomTestRunner.class)
  public class TestWordDealUtil {
……
  }

顯而易見,若是測試類沒有顯式的聲明使用哪個測試運行器,JUnit 會啓動默認的測試運行器執行測試類(好比上面說起的單元測試代碼)。通常狀況下,默認測試運行器能夠應對絕大多數的單元測試要求;當使用 JUnit 提供的一些高級特性(例如即將介紹的兩個特性)或者針對特殊需求定製 JUnit 測試方式時,顯式的聲明測試運行器就必不可少了。

測試套件

在實際項目中,隨着項目進度的開展,單元測試類會愈來愈多,但是直到如今咱們還只會一個一個的單獨運行測試類,這在實際項目實踐中確定是不可行的。爲了解決這個問題,JUnit 提供了一種批量運行測試類的方法,叫作測試套件。這樣,每次須要驗證系統功能正確性時,只執行一個或幾個測試套件即可以了。測試套件的寫法很是簡單,您只須要遵循如下規則:

  1. 建立一個空類做爲測試套件的入口。
  2. 使用註解 org.junit.runner.RunWith 和 org.junit.runners.Suite.SuiteClasses 修飾這個空類。
  3. 將 org.junit.runners.Suite 做爲參數傳入註解 RunWith,以提示 JUnit 爲此類使用套件運行器執行。
  4. 將須要放入此測試套件的測試類組成數組做爲註解 SuiteClasses 的參數。
  5. 保證這個空類使用 public 修飾,並且存在公開的不帶有任何參數的構造函數。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.ai92.cooljunit;
 
  import org.junit.runner.RunWith;
  import org.junit.runners.Suite;
……
 
  /**
  * 批量測試 工具包 中測試類
  * @author Ai92
  */
  @RunWith(Suite.class)
  @Suite.SuiteClasses({TestWordDealUtil.class})
  public class RunAllUtilTestsSuite {
  }

上例代碼中,咱們將前文提到的測試類 TestWordDealUtil 放入了測試套件 RunAllUtilTestsSuite 中,在 Eclipse 中運行測試套件,能夠看到測試類 TestWordDealUtil 被調用執行了。測試套件中不只能夠包含基本的測試類,並且能夠包含其它的測試套件,這樣能夠很方便的分層管理不一樣模塊的單元測試代碼。可是,您必定要保證測試套件之間沒有循環包含關係,不然無盡的循環就會出如今您的面前……。

參數化測試

回顧一下咱們在小節「JUnit 初體驗」中舉的實例。爲了保證單元測試的嚴謹性,咱們模擬了不一樣類型的字符串來測試方法的處理能力,爲此咱們編寫大量的單元測試方法。但是這些測試方法都是大同小異:代碼結構都是相同的,不一樣的僅僅是測試數據和指望值。有沒有更好的方法將測試方法中相同的代碼結構提取出來,提升代碼的重用度,減小複製粘貼代碼的煩惱?在之前的 JUnit 版本上,並無好的解決方法,而如今您可使用 JUnit 提供的參數化測試方式應對這個問題。

參數化測試的編寫稍微有點麻煩(固然這是相對於 JUnit 中其它特性而言):

  1. 爲準備使用參數化測試的測試類指定特殊的運行器 org.junit.runners.Parameterized。
  2. 爲測試類聲明幾個變量,分別用於存放指望值和測試所用數據。
  3. 爲測試類聲明一個使用註解 org.junit.runners.Parameterized.Parameters 修飾的,返回值爲 java.util.Collection 的公共靜態方法,並在此方法中初始化全部須要測試的參數對。
  4. 爲測試類聲明一個帶有參數的公共構造函數,並在其中爲第二個環節中聲明的幾個變量賦值。
  5. 編寫測試方法,使用定義的變量做爲參數進行測試。

咱們按照這個標準,從新改造一番咱們的單元測試代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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"},      // 測試通常的處理狀況
                 {null, null},                           // 測試 null 時的處理狀況
                 {"", ""},                               // 測試空字符串時的處理狀況
                 {"employee_info", "EmployeeInfo"},      // 測試當首字母大寫時的狀況
                 {"employee_info_a", "employeeInfoA"},   // 測試當尾字母爲大寫時的狀況
                 {"employee_a_info", "employeeAInfo"}    // 測試多個相連字母大寫時的狀況
             });
         }
     
          /**
          * 參數化測試必須的構造函數
          * @param expected     指望的測試結果,對應參數集中的第一個參數
          * @param target     測試數據,對應參數集中的第二個參數
          */
         public TestWordDealUtilWithParam(String expected , String target){
             this.expected = expected;
             this.target = target;
         }
     
          /**
          * 測試將 Java 對象名稱到數據庫名稱的轉換
          */
         @Test public void wordFormat4DB(){
             assertEquals(expected, WordDealUtil.wordFormat4DB(target));
         }
}

很明顯,代碼瘦身了。在靜態方法 words 中,咱們使用二維數組來構建測試所須要的參數列表,其中每一個數組中的元素的放置順序並無什麼要求,只要和構造函數中的順序保持一致就能夠了。如今若是再增長一種測試狀況,只須要在靜態方法 words 中添加相應的數組便可,再也不須要複製粘貼出一個新的方法出來了。

 

 

 

來源:https://www.ibm.com/developerworks/cn/java/j-lo-junit4/

相關文章
相關標籤/搜索