# 追求代碼質量: JUnit 4 與 TestNG 的對比
_為什么 TestNG 框架依然是大規模測試的較好選擇?_
JUnit 4 具有基于注釋的新框架,它包含了 TestNG 一些最優異的特性。但這是否意味著 JUnit 4 已經淘汰了 TestNG?Andrew Glover 探討了這兩種框架各自的獨特之處,并闡述了 TestNG 獨有的三種高級測試特性。
經過長時間積極的開發之后,JUnit 4.0 于今年年初發布了。JUnit 框架的某些最有趣的更改 —— 特別是對于本專欄的讀者來說 —— 正是通過巧妙地使用注釋實現的。除外觀和風格方面的顯著改進外,新框架的特性使測試用例的編制從結構規則中解放出來。使原來僵化的 fixture 模型更為靈活,有利于采取可配置程度更高的方法。因此,JUnit 框架不再強求把每一項測試工作定義為一個名稱以 `test` 開始的方法,并且現在可以只運行一次 fixture,而不是每次測試都需要運行一次。
雖然這些改變令人欣慰,但 JUnit 4 并不是第一個提供基于注釋的靈活模型的 Java? 測試框架。在修改 JUnit 之前很久,TestNG 就已建立為一個基于注釋的框架。
事實上,是 TestNG 在 Java 編程中_率先_ 實現了利用注釋進行測試,這使它成為 JUnit 的有力競爭對手。然而,自從 JUnit 4 發布后,很多開發者質疑:二者之間還有什么差別嗎?在本月的專欄中,我將討論 TestNG 不同于 JUnit 4 的一些特性,并提議采用一些方法,使得這兩個框架能繼續互相補充,而不是互相競爭。
## 您知道嗎?
在 Ant 中運行 JUnit 4 測試比預計的要難得多。事實上,一些團隊已發現,惟一的解決方法是升級到 Ant 1.7。
## 表面上的相似
JUnit 4 和 TestNG 有一些共同的重要特性。這兩個框架都讓測試工作簡單得令人吃驚(和愉快),給測試工作帶來了便利。二者也都擁有活躍的社區,為主動開發提供支持,同時生成豐富的文檔。
## 提高代碼質量
要找到您最迫切問題的答案,請不要錯過 Andrew 的 [論壇](http://www.ibm.com/developerworks/forums/dw_forum.jsp?S_TACT=105AGX52&cat=10&S_CMP=cn-a-j&forum=812)。
兩個框架的不同在于核心設計。JUnit _一直_ 是一個單元測試框架,也就是說,其構建目的是促進單個對象的測試,它確實能夠極其有效地完成此類任務。而 TestNG 則是用來解決_更高_ 級別的測試問題,因此,它具有 JUnit 中所沒有的一些特性。
### 一個簡單的測試用例
初看起來,JUnit 4 和 TestNG 中實現的測試非常相似。為了更好地理解我的意思,請看一下清單 1 中的代碼。這是一個 JUnit 4 測試,它有一個 macro-fixture(即僅在所有測試運行前調用一次的 fixture),這個 macro-fixture 由 `@BeforeClass` 屬性表示:
##### 清單 1\. 一個簡單的 JUnit 4 測試用例
```
package test.com.acme.dona.dep;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.junit.BeforeClass;
import org.junit.Test;
public class DependencyFinderTest {
private static DependencyFinder finder;
@BeforeClass
public static void init() throws Exception {
finder = new DependencyFinder();
}
@Test
public void verifyDependencies()
throws Exception {
String targetClss =
"test.com.acme.dona.dep.DependencyFind";
Filter[] filtr = new Filter[] {
new RegexPackageFilter("java|junit|org")};
Dependency[] deps =
finder.findDependencies(targetClss, filtr);
assertNotNull("deps was null", deps);
assertEquals("should be 5 large", 5, deps.length);
}
}
```
JUnit 用戶會立即注意到:這個類中沒有了以前版本的 JUnit 中所要求的一些_語法成分_。這個類沒有 `setUp()` 方法,也不對 `TestCase` 類進行擴展,甚至也沒有哪個方法的名稱以 `test` 開始。這個類還利用了 Java 5 的一些特性,例如靜態導入,很明顯地,它還使用了注釋。
### 更多的靈活性
在清單 2 中,您可以看到_同一個_ 測試項目。不過這次是用 TestNG 實現的。這里的代碼跟清單 1 中的測試代碼有個微妙的差別。發現了嗎?
##### 清單 2\. 一個 TestNG 測試用例
```
package test.com.acme.dona.dep;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Configuration;
import org.testng.annotations.Test;
public class DependencyFinderTest {
private DependencyFinder finder;
@BeforeClass
private void init(){
this.finder = new DependencyFinder();
}
@Test
public void verifyDependencies()
throws Exception {
String targetClss =
"test.com.acme.dona.dep.DependencyFind";
Filter[] filtr = new Filter[] {
new RegexPackageFilter("java|junit|org")};
Dependency[] deps =
finder.findDependencies(targetClss, filtr);
assertNotNull(deps, "deps was null" );
assertEquals(5, deps.length, "should be 5 large");
}
}
```
顯然,這兩個清單很相似。不過,如果仔細看,您會發現 TestNG 的編碼規則比 JUnit 4 更靈活。[清單 1](#code1) 里,在 JUnit 中我必須把 `@BeforeClass` 修飾的方法聲明為 `static`,這又要求我把 fixture,即 `finder` 聲明為 `static`。我還必須把 `init()` 聲明為 `public`。看看清單 2,您就會發現不同。這里不再需要那些規則了。我的 `init()` 方法既不是 `static`,也不是 `public`。
從最初起,TestNG 的靈活性就是其主要優勢之一,但這并非它惟一的賣點。TestNG 還提供了 JUnit 4 所不具備的其他一些特性。
* * *
## 依賴性測試
JUnit 框架想達到的一個目標就是測試隔離。它的缺點是:人們很難確定測試用例執行的順序,而這對于任何類型的依賴性測試都非常重要。開發者們使用了多種技術來解決這個問題,例如,按字母順序指定測試用例,或是更多地依靠 fixture 來適當地解決問題。
如果測試成功,這些解決方法都沒什么問題。但是,如果測試不成功,就會產生一個很麻煩的后果:_所有_ 后續的依賴測試也會失敗。在某些情況下,這會使大型測試套件報告出許多不必要的錯誤。例如,假設有一個測試套件測試一個需要登錄的 Web 應用程序。您可以創建一個有依賴關系的方法,通過登錄到這個應用程序來創建整個測試套件,從而避免 JUnit 的隔離機制。這種解決方法不錯,但是如果登錄失敗,即使登錄該應用程序后的其他功能都正常工作,整個測試套件依然會全部失敗!
### 跳過,而不是標為失敗
與 JUnit 不同,TestNG 利用 `Test` 注釋的 `dependsOnMethods` 屬性來應對測試的依賴性問題。有了這個便利的特性,就可以輕松指定依賴方法。例如,前面所說的登錄將在某個方法_之前_ 運行。此外,如果依賴方法失敗,它將被_跳過_,而不是標記為失敗。
##### 清單 3\. 使用 TestNG 進行依賴性測試
```
import net.sourceforge.jwebunit.WebTester;
public class AccountHistoryTest {
private WebTester tester;
@BeforeClass
protected void init() throws Exception {
this.tester = new WebTester();
this.tester.getTestContext().
setBaseUrl("http://div.acme.com:8185/ceg/");
}
@Test
public void verifyLogIn() {
this.tester.beginAt("/");
this.tester.setFormElement("username", "admin");
this.tester.setFormElement("password", "admin");
this.tester.submit();
this.tester.assertTextPresent("Logged in as admin");
}
@Test (dependsOnMethods = {"verifyLogIn"})
public void verifyAccountInfo() {
this.tester.clickLinkWithText("History", 0);
this.tester.assertTextPresent("GTG Data Feed");
}
}
```
在清單 3 中定義了兩個測試:一個驗證登錄,另一個驗證賬戶信息。請注意,通過使用 `Test` 注釋的 `dependsOnMethods = {"verifyLogIn"}` 子句,`verifyAccountInfo` 測試指定了它依賴 `verifyLogIn()` 方法。
通過 TestNG 的 Eclipse 插件(例如)運行該測試時,如果 `verifyLogIn` 測試失敗,TestNG 將直接跳過 `verifyAccountInfo` 測試,請參見圖 1:
##### 圖 1\. 在 TestNG 中跳過的測試

對于大型測試套件,TestNG 這種不標記為失敗,而只是跳過的處理方法可以減輕很多壓力。您的團隊可以集中精力查找為什么百分之五十的測試套件被跳過,而不是去找百分之五十的測試套件失敗的原因!更有利的是,TestNG 采取了只重新運行失敗測試的機制,這使它的依賴性測試設置更為完善。
* * *
## 失敗和重運行
在大型測試套件中,這種重新運行失敗測試的能力顯得尤為方便。這是 TestNG 獨有的一個特性。在 JUnit 4 中,如果測試套件包括 1000 項測試,其中 3 項失敗,很可能就會迫使您重新運行整個測試套件(修改錯誤以后)。不用說,這樣的工作可能會耗費幾個小時。
一旦 TestNG 中出現失敗,它就會創建一個 XML 配置文件,對失敗的測試加以說明。如果利用這個文件執行 TestNG 運行程序,TestNG 就_只_ 運行失敗的測試。所以,在前面的例子里,您只需重新運行那三個失敗的測試,而不是整個測試套件。
實際上,您可以通過清單 2 中的 Web 測試的例子自己看到這點。`verifyLogIn()` 方法失敗時,TestNG 自動創建一個 testng-failed.xml 文件。該文件將成為如清單 4 所示的替代性測試套件:
##### 清單 4\. 失敗測試的 XML 文件
```
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite thread-count="5" verbose="1" name="Failed suite [HistoryTesting]"
parallel="false" annotations="JDK5">
<test name="test.com.acme.ceg.AccountHistoryTest(failed)" junit="false">
<classes>
<class name="test.com.acme.ceg.AccountHistoryTest">
<methods>
<include name="verifyLogIn"/>
</methods>
</class>
</classes>
</test>
</suite>
```
運行小的測試套件時,這個特性似乎沒什么大不了。但是如果您的測試套件規模較大,您很快就會體會到它的好處。
* * *
## 參數化測試
TestNG 中另一個有趣的特性是_參數化測試_。在 JUnit 中,如果您想改變某個受測方法的參數組,就只能給_每個_ 不同的參數組編寫一個測試用例。多數情況下,這不會帶來太多麻煩。然而,我們有時會碰到一些情況,對其中的業務邏輯,需要運行的測試數目變化范圍很大。
在這樣的情況下,使用 JUnit 的測試人員往往會轉而使用 FIT 這樣的框架,因為這樣就可以用表格數據驅動測試。但是 TestNG 提供了開箱即用的類似特性。通過在 TestNG 的 XML 配置文件中放入參數化數據,就可以對不同的數據集重用同一個測試用例,甚至有可能會得到不同的結果。這種技術完美地避免了_只能_ 假定一切正常的測試,或是沒有對邊界進行有效驗證的情況。
在清單 5 中,我用 Java 1.4 定義了一個 TestNG 測試,該測試可接收兩個參數:`classname` 和 `size`。這兩個參數可以驗證某個類的層次結構(也就是說,如果傳入 `java.util.Vector`,則 `HierarchyBuilder` 所構建的 `Hierarchy` 的值將為 `2` )。
##### 清單 5\. 一個 TestNG 參數化測試
```
package test.com.acme.da;
import com.acme.da.hierarchy.Hierarchy;
import com.acme.da.hierarchy.HierarchyBuilder;
public class HierarchyTest {
/**
* @testng.test
* @testng.parameters value="class_name, size"
*/
public void assertValues(String classname, int size) throws Exception{
Hierarchy hier = HierarchyBuilder.buildHierarchy(classname);
assert hier.getHierarchyClassNames().length == size: "didn't equal!";
}
}
```
清單 5 列出了一個泛型測試,它可以采用不同的數據反復重用。請花點時間思考一下這個問題。如果有 10 個不同的參數組合需要在 JUnit 中測試,您只能寫 10 個測試用例。每個測試用例完成的任務基本是相同的,只是受測方法的參數有所改變。但是,如果使用參數化測試,就可以只定義_一個_ 測試用例,然后,(舉例來說)把所需的參數模式加到 TestNG 的測試套件文件中。清單 6 中展示了這中方法:
##### 清單 6\. 一個 TestNG 參數化測試套件文件
```
<!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd">
<suite name="Deckt-10">
<test name="Deckt-10-test">
<parameter name="class_name" value="java.util.Vector"/>
<parameter name="size" value="2"/>
<classes>
<class name="test.com.acme.da.HierarchyTest"/>
</classes>
</test>
</suite>
```
清單 6 中的 TestNG 測試套件文件只對該測試定義了一個參數組(`class_name` 為 `java.util.Vector`,且 `size` 等于 `2`),但卻具有無限的可能。這樣做的一個額外的好處是:將測試數據移動到 XML 文件的無代碼工件就意味著非程序員也可以指定數據。
* * *
## 高級參數化測試
盡管從一個 XML 文件中抽取數據會很方便,但偶爾會有些測試需要有復雜類型,這些類型無法用 `String` 或原語值來表示。TestNG 可以通過它的 `@DataProvider` 注釋處理這樣的情況。`@DataProvider` 注釋可以方便地把復雜參數類型映射到某個測試方法。例如,清單 7 中的 `verifyHierarchy` 測試中,我采用了重載的 `buildHierarchy` 方法,它可接收一個 `Class` 類型的數據, 它斷言(asserting)`Hierarchy` 的 `getHierarchyClassNames()` 方法應該返回一個適當的字符串數組:
##### 清單 7\. TestNG 中的 DataProvider 用法
```
package test.com.acme.da.ng;
import java.util.Vector;
import static org.testng.Assert.assertEquals;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import com.acme.da.hierarchy.Hierarchy;
import com.acme.da.hierarchy.HierarchyBuilder;
public class HierarchyTest {
@DataProvider(name = "class-hierarchies")
public Object[][] dataValues(){
return new Object[][]{
{Vector.class, new String[] {"java.util.AbstractList",
"java.util.AbstractCollection"}},
{String.class, new String[] {}}
};
}
@Test(dataProvider = "class-hierarchies")
public void verifyHierarchy(Class clzz, String[] names)
throws Exception{
Hierarchy hier = HierarchyBuilder.buildHierarchy(clzz);
assertEquals(hier.getHierarchyClassNames(), names,
"values were not equal");
}
}
```
`dataValues()` 方法通過一個多維數組提供與 `verifyHierarchy` 測試方法的參數值匹配的數據值。TestNG 遍歷這些數據值,并根據數據值調用了兩次 `verifyHierarchy`。在第一次調用時,`Class` 參數被設置為 `Vector.class` ,而 `String` 數組參數容納 “`java.util.AbstractList` ” 和 “ `java.util.AbstractCollection` ” 這兩個 `String` 類型的數據。這樣挺方便吧?
* * *
## 為什么只選擇其一?
我已經探討了對我而言,TestNG 的一些獨有優勢,但是它還有其他幾個特性是 JUnit 所不具備的。例如 TestNG 中使用了測試分組,它可以根據諸如運行時間這樣的特征來對測試分類。也可在 Java 1.4 中通過 javadoc 風格的注釋來使用它,如 [清單 5](#code5) 所示。
正如我在本文開頭所說,JUnit 4 和 TestNG 在表面上是相似的。然而,設計 JUnit 的目的是為了分析代碼單元,而 TestNG 的預期用途則針對高級測試。對于大型測試套件,我們不希望在某一項測試失敗時就得重新運行數千項測試,TestNG 的靈活性在這里尤為有用。這兩個框架都有自己的優勢,您可以隨意同時使用它們。
- 追求代碼質量
- 追求代碼質量: 對 Ajax 應用程序進行單元測試
- 追求代碼質量: 使用 TestNG-Abbot 實現自動化 GUI 測試
- 追求代碼質量: 用 AOP 進行防御性編程
- 追求代碼質量: 探究 XMLUnit
- 追求代碼質量: 用 JUnitPerf 進行性能測試
- 追求代碼質量: 通過測試分類實現敏捷構建
- 追求代碼質量: 可重復的系統測試
- 追求代碼質量: JUnit 4 與 TestNG 的對比
- 追求代碼質量: 馴服復雜的冗長代碼
- 追求代碼質量: 用代碼度量進行重構
- 追求代碼質量: 軟件架構的代碼質量
- 讓開發自動化: 除掉構建腳本中的氣味
- 追逐代碼質量: 決心采用 FIT
- 追求代碼質量: 不要被覆蓋報告所迷惑