JUnit 4 與 JUnit 3

JUnit 是 Java? 語言 事實上的 標準單元測試庫。JUnit 4 是該庫三年以來最具里程碑意義的一次發佈。它的新特性主要是經過採用 Java 5 中的標記(annotation)而不是利用子類、反射或命名機制來識別測試,從而簡化測試。在本文中,執着的代碼測試人員 Elliotte Harold 以 JUnit 4 爲例,詳細介紹瞭如何在本身的工做中使用這個新框架。注意,本文假設讀者具備 JUnit 的使用經驗。
JUnit 由 Kent Beck 和 Erich Gamma 開發,幾乎毫無疑問是迄今所開發的最重要的第三方 Java 庫。正如 Martin Fowler 所說,「在軟件開發領域,歷來就沒有如此少的代碼起到了如此重要的做用」。JUnit 引導並促進了測試的盛行。因爲 JUnit,Java 代碼變得更健壯,更可靠,bug 也比之前更少。JUnit(它自己的靈感來自 Smalltalk 的 SUnit)衍生了許多 xUnit 工具,將單元測試的優點應用於各類語言。nUnit (.NET)、pyUnit (Python)、CppUnit (C++)、dUnit (Delphi) 以及其餘工具,影響了各類平臺和語言上的程序員的測試工做。
然而,JUnit 僅僅是一個工具而已。真正的優點來自於 JUnit 所採用的思想和技術,而不是框架自己。單元測試、測試先行的編程和測試驅動的開發並不是都要在 JUnit 中實現,任何比較 GUI 的編程都必須用 Swing 來完成。JUnit 自己的最後一次更新差很少是三年之前了。儘管它被證實比大多數框架更健壯、更持久,可是也發現了 bug;而更重要的是,Java 不斷在發展。Java 語言如今支持泛型、枚舉、可變長度參數列表和註釋,這些特性爲可重用的框架設計帶來了新的可能。
JUnit 的停滯不前並無被那些想要廢棄它的程序員所戰勝。挑戰者包括 Bill Venners 的 Artima SuiteRunner 以及 Cedric Beust 的 TestNG 等。這些庫有一些可圈可點的特性,可是都沒有達到 JUnit 的知名度和市場佔有份額。它們都沒有在諸如 Ant、Maven 或 Eclipse 之類的產品中具備普遍的開箱即用支持。因此 Beck 和 Gamma 着手開發了一個新版本的 JUnit,它利用 Java 5 的新特性(尤爲是註釋)的優點,使得單元測試比起用最初的 JUnit 來講更加簡單。用 Beck 的話來講,「JUnit 4 的主題是經過進一步簡化 JUnit,鼓勵更多的開發人員編寫更多的測試。」JUnit 4 儘管保持了與現有 JUnit 3.8 測試套件的向後兼容,可是它仍然承諾是自 JUnit 1.0 以來 Java 單元測試方面最重大的改進。
注意:該框架的改進是至關前沿的。儘管 JUnit 4 的大輪廓很清晰,可是其細節仍然能夠改變。這意味着本文是對 JUnit 4 搶先看,而不是它的最終效果。
之前全部版本的 JUnit 都使用命名約定和反射來定位測試。例如,下面的代碼測試 1+1 等於 2:
import junit.framework.TestCase;
public class AdditionTest extends TestCase {
  private int x = 1;
  private int y = 1;
  
  public void testAddition() {
    int z = x + y;
    assertEquals(2, z);
  }
}

而在 JUnit 4 中,測試是由 @Test 註釋來識別的,以下所示:
import org.junit.Test;
import junit.framework.TestCase;
public class AdditionTest extends TestCase {
  private int x = 1;
  private int y = 1;
  
  @Test public void testAddition() {
    int z = x + y;
    assertEquals(2, z);
  }
}

使用註釋的優勢是再也不須要將全部的方法命名爲 testFoo()testBar(),等等。例如,下面的方法也能夠工做:
import org.junit.Test;
import junit.framework.TestCase;
public class AdditionTest extends TestCase {
  private int x = 1;
  private int y = 1;
  
  @Test public void additionTest() {
    int z = x + y;
    assertEquals(2, z);
  }
}

下面這個方法也一樣可以工做:
import org.junit.Test;
import junit.framework.TestCase;
public class AdditionTest extends TestCase {
  private int x = 1;
  private int y = 1;
  
  @Test public void addition() {
    int z = x + y;
    assertEquals(2, z);
  }
}

這容許您遵循最適合您的應用程序的命名約定。例如,我介紹的一些例子採用的約定是,測試類對其測試方法使用與被測試的類相同的名稱。例如, List.contains()ListTest.contains() 測試, List.add()ListTest.addAll() 測試,等等。
TestCase 類仍然能夠工做,可是您再也不須要擴展它了。只要您用 @Test 來註釋測試方法,就能夠將測試方法放到任何類中。可是您須要導入 junit.Assert 類以訪問各類 assert 方法,以下所示:
import org.junit.Assert;
public class AdditionTest {
  private int x = 1;
  private int y = 1;
  
  @Test public void addition() {
    int z = x + y;
    Assert.assertEquals(2, z);
  }
}

您也可使用 JDK 5 中新特性(static import),使得與之前版本同樣簡單:
import static org.junit.Assert.assertEquals;
public class AdditionTest {
  private int x = 1;
  private int y = 1;
  
  @Test public void addition() {
    int z = x + y;
    assertEquals(2, z);
  }
}

這種方法使得測試受保護的方法很是容易,由於測試案例類如今能夠擴展包含受保護方法的類了。




JUnit 3 測試運行程序(test runner)會在運行每一個測試以前自動調用 setUp() 方法。該方法通常會初始化字段,打開日誌記錄,重置環境變量,等等。例如,下面是摘自 XOM 的 XSLTransformTest 中的 setUp() 方法:
protected void setUp() {
        
    System.setErr(new PrintStream(new ByteArrayOutputStream()));
        
    inputDir = new File("data");
    inputDir = new File(inputDir, "xslt");
    inputDir = new File(inputDir, "input");
        
}

在 JUnit 4 中,您仍然能夠在每一個測試方法運行以前初始化字段和配置環境。然而,完成這些操做的方法再也不須要叫作 setUp(),只要用 @Before 註釋來指示便可,以下所示:
@Before protected void initialize() {
        
    System.setErr(new PrintStream(new ByteArrayOutputStream()));
        
    inputDir = new File("data");
    inputDir = new File(inputDir, "xslt");
    inputDir = new File(inputDir, "input");
        
}

甚至能夠用 @Before 來註釋多個方法,這些方法都在每一個測試以前運行:
@Before protected void findTestDataDirectory() {
    inputDir = new File("data");
    inputDir = new File(inputDir, "xslt");
    inputDir = new File(inputDir, "input");
}
    
 @Before protected void redirectStderr() {
    System.setErr(new PrintStream(new ByteArrayOutputStream()));
}

清除方法與此相似。在 JUnit 3 中,您使用 tearDown() 方法,該方法相似於我在 XOM 中爲消耗大量內存的測試所使用的方法:
protected void tearDown() {
  doc = null;
  System.gc();   
} 

對於 JUnit 4,我能夠給它取一個更天然的名稱,並用 @After 註釋它:
@After protected void disposeDocument() {
  doc = null;
  System.gc();   
} 

@Before 同樣,也能夠用 @After 來註釋多個清除方法,這些方法都在每一個測試以後運行。
最後,您再也不須要在超類中顯式調用初始化和清除方法,只要它們不被覆蓋便可,測試運行程序將根據須要自動爲您調用這些方法。超類中的 @Before 方法在子類中的 @Before 方法以前被調用(這反映了構造函數調用的順序)。 @After 方法以反方向運行:子類中的方法在超類中的方法以前被調用。不然,多個 @Before@After 方法的相對順序就得不到保證。
JUnit 4 也引入了一個 JUnit 3 中沒有的新特性:類範圍的 setUp()tearDown() 方法。任何用 @BeforeClass 註釋的方法都將在該類中的測試方法運行以前恰好運行一次,而任何用 @AfterClass 註釋的方法都將在該類中的全部測試都運行以後恰好運行一次。
例如,假設類中的每一個測試都使用一個數據庫鏈接、一個網絡鏈接、一個很是大的數據結構,或者還有一些對於初始化和事情安排來講比較昂貴的其餘資源。不要在每一個測試以前都從新建立它,您能夠建立它一次,並還原它一次。該方法將使得有些測試案例運行起來快得多。例如,當我測試調用第三方庫的代碼中的錯誤處理時,我一般喜歡在測試開始以前重定向 System.err,以便輸出不被預期的錯誤消息打亂。而後我在測試結束後還原它,以下所示:
// This class tests a lot of error conditions, which
// Xalan annoyingly logs to System.err. This hides System.err 
// before each test and restores it after each test.
private PrintStream systemErr;
    
@BeforeClass protected void redirectStderr() {
    systemErr = System.err; // Hold on to the original value
    System.setErr(new PrintStream(new ByteArrayOutputStream()));
}
    
@AfterClass protected void tearDown() {
    // restore the original value
    System.setErr(systemErr);
}

沒有必要在每一個測試以前和以後都這樣作。可是必定要當心對待這個特性。它有可能會違反測試的獨立性,並引入非預期的混亂。若是一個測試在某種程度上改變了 @BeforeClass 所初始化的一個對象,那麼它有可能會影響其餘測試的結果。它有可能在測試套件中引入順序依賴,並隱藏 bug。與任何優化同樣,只在剖析和基準測試證實您具備實際的問題以後才實現這一點。這就是說,我看到了不止一個測試套件運行時間如此之長,以致不能像它所須要的那樣常常運行,尤爲是那些須要創建不少網絡和數據庫鏈接的測試。(例如,LimeWire 測試套件運行時間超過兩小時。)要加快這些測試套件,以便程序員能夠更加常常地運行它們,您能夠作的就是減小 bug。




異常測試是 JUnit 4 中的最大改進。舊式的異常測試是在拋出異常的代碼中放入 try 塊,而後在 try 塊的末尾加入一個 fail() 語句。例如,該方法測試被零除拋出一個 ArithmeticException
public void testDivisionByZero() {
    
    try {
        int n = 2 / 0;
        fail("Divided by zero!");
    }
    catch (ArithmeticException success) {
        assertNotNull(success.getMessage());
    }
    
}

該方法不只難看,並且試圖挑戰代碼覆蓋工具,由於無論測試是經過仍是失敗,總有一些代碼不被執行。在 JUnit 4 中,您如今能夠編寫拋出異常的代碼,並使用註釋來聲明該異常是預期的:
@Test(expected=ArithmeticException.class) 
  public void divideByZero() {
    int n = 2 / 0;
}

若是該異常沒有拋出(或者拋出了一個不一樣的異常),那麼測試就將失敗。可是若是您想要測試異常的詳細消息或其餘屬性,則仍然須要使用舊式的 try-catch 樣式。




也許您有一個測試運行的時間很是地長。不是說這個測試應該運行得更快,而是說它所作的工做從根本上比較複雜或緩慢。須要訪問遠程網絡服務器的測試一般都屬於這一類。若是您不在作可能會中斷該類測試的事情,那麼您可能想要跳過運行時間長的測試方法,以縮短編譯-測試-調試周期。或者也許是一個由於超出您的控制範圍的緣由而失敗的測試。例如,W3C XInclude 測試套件測試 Java 還不支持的一些 Unicode 編碼的自動識別。沒必要總是被迫盯住那些紅色波浪線,這類測試能夠被註釋爲 @Ignore,以下所示:
// Java doesn't yet support 
// the UTF-32BE and UTF32LE encodings
    @Ignore public void testUTF32BE() 
      throws ParsingException, IOException, XIncludeException {
      
        File input = new File(
          "data/xinclude/input/UTF32BE.xml"
        );
        Document doc = builder.build(input);
        Document result = XIncluder.resolve(doc);
        Document expectedResult = builder.build(
          new File(outputDir, "UTF32BE.xml")
        );
        assertEquals(expectedResult, result);
                
    }

測試運行程序將不運行這些測試,可是它會指出這些測試被跳過了。例如,當使用文本界面時,會輸出一個「I」(表明 ignore),而不是爲經過的測試輸出所經歷的時間,也不是爲失敗的測試輸出「E」:
$ java -classpath .:junit.jar org.junit.runner.JUnitCore 
  nu.xom.tests.XIncludeTest
JUnit version 4.0rc1
.....I..
Time: 1.149
OK (7 tests)

可是必定要當心。最初編寫這些測試可能有必定的緣由。若是永遠忽略這些測試,那麼它們指望測試的代碼可能會中斷,而且這樣的中斷可能不能被檢測到。忽略測試只是一個權宜之計,不是任何問題的真正解決方案。






測試性能是單元測試最爲痛苦的方面之一。JUnit 4 沒有徹底解決這個問題,可是它對這個問題有所幫助。測試能夠用一個超時參數來註釋。若是測試運行的時間超過指定的毫秒數,則測試失敗。例如,若是測試花費超過半秒時間去查找之前設置的一個文檔中的全部元素,那麼該測試失敗:
@Test(timeout=500) public void retrieveAllElementsInDocument() {
    doc.query("//*");
} 

除了簡單的基準測試以外,時間測試也對網絡測試頗有用。在一個測試試圖鏈接到的遠程主機或數據庫宕機或變慢時,您能夠忽略該測試,以便不阻塞全部其餘的測試。好的測試套件執行得足夠快,以致程序員能夠在每一個測試發生重大變化以後運行這些測試,有可能一天運行幾十次。設置一個超時使得這一點更加可行。例如,若是解析 [url]http://www.ibiblio.org/xml[/url] 花費了超過 2 秒,那麼下面的測試就會超時:
@Test(timeout=2000) 
  public void remoteBaseRelativeResolutionWithDirectory()
   throws IOException, ParsingException {
      builder.build("http://www.ibiblio.org/xml");
  } 







JUnit 4 爲比較數組添加了兩個 assert() 方法:
public static void assertEquals(Object[] expected, Object[] actual)
public static void assertEquals(String message, Object[] expected, 
Object[] actual)

這兩個方法以最直接的方式比較數組:若是數組長度相同,且每一個對應的元素相同,則兩個數組相等,不然不相等。數組爲空的狀況也做了考慮。






JUnit 4 基本上是一個新框架,而不是舊框架的升級版本。JUnit 3 開發人員可能會找到一些原來沒有的特性。
最明顯的刪節就是 GUI 測試運行程序。若是您想在測試經過時看到賞心悅目的綠色波浪線,或者在測試失敗時看到使人焦慮的紅色波浪線,那麼您須要一個具備集成 JUnit 支持的 IDE,好比 Eclipse。無論是 Swing 仍是 AWT 測試運行程序都不會被升級或捆綁到 JUnit 4 中。
下一個驚喜是,失敗(assert 方法檢測到的預期的錯誤)與錯誤(異常指出的非預期的錯誤)之間再也不有任何差異。儘管 JUnit 3 測試運行程序仍然能夠區別這些狀況,而 JUnit 4 運行程序將再也不可以區分。
最後,JUnit 4 沒有 suite() 方法,這些方法用於從多個測試類構建一個測試套件。相反,可變長參數列表用於容許將不肯定數量的測試傳遞給測試運行程序。
我對消除了 GUI 測試運行程序並不感到過高興,可是其餘更改彷佛有可能增長 JUnit 的簡單性。只要考慮有多少文檔和 FAQ 當前專門用於解釋這幾點,而後考慮對於 JUnit 4,您再也不須要解釋這幾點了。






當前,尚未 JUnit 4 的庫版本。若是您想要體驗新的版本,那麼您須要從 SourceForge 上的 CVS 知識庫獲取它。分支(branch)是「Version4」(參見 參考資料)。注意,不少的文檔沒有升級,仍然是指以舊式的 3.x 方式作事。Java 5 對於編譯 JUnit 4 是必需的,由於 JUnit 4 大量用到註釋、泛型以及 Java 5 語言級的其餘特性。
自 JUnit 3 以來,從命令行運行測試的語法發生了一點變化。您如今使用 org.junit.runner.JUnitCore 類:
$ java -classpath .:junit.jar org.junit.runner.JUnitCore 
  TestA TestB TestC...
JUnit version 4.0rc1
Time: 0.003
OK (0 tests)

Beck 和 Gamma 努力維持向前和向後兼容。JUnit 4 測試運行程序能夠運行 JUnit 3 測試,不用作任何更改。只要將您想要運行的每一個測試的全限定類名傳遞給測試運行程序,就像針對 JUnit 4 測試同樣。運行程序足夠智能,能夠分辨出哪一個測試類依賴於哪一個版本的 JUnit,並適當地調用它。
向後兼容要困難一些,可是也能夠在 JUnit 3 測試運行程序中運行 JUnit 4 測試。這一點很重要,因此諸如 Eclipse 之類具備集成 JUnit 支持的工具能夠處理 JUnit 4,而不須要更新。爲了使 JUnit 4 測試能夠運行在 JUnit 3 環境中,能夠將它們包裝在 JUnit4TestAdapter 中。將下面的方法添加到您的 JUnit 4 測試類中應該就足夠了:
public static junit.framework.Test suite() {
  return new JUnit4TestAdapter(AssertionTest.class);    
}

可是因爲 Java 比較多變,因此 JUnit 4 一點都不向後兼容。JUnit 4 徹底依賴於 Java 5 特性。對於 Java 1.4 或更早版本,它將不會編譯或運行。






JUnit 4 遠沒有結束。不少重要的方面沒有說起,包括大部分的文檔。我不推薦如今就將您的測試套件轉換成註釋和 JUnit 4。即便如此,開發仍在快速進行,而且 JUnit 4 前景很是看好。儘管 Java 2 程序員在可預見的將來仍然須要使用 JUnit 3.8,可是那些已經轉移到 Java 5 的程序員則應該很快考慮使他們的測試套件適合於這個新的框架,以便匹配。
相關文章
相關標籤/搜索