程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 追求代碼質量 - JUnit 4與TestNG的對比

追求代碼質量 - JUnit 4與TestNG的對比

編輯:關於JAVA

經過長時間積極的開發之後,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 有一些共同的重要特性。這兩個框架都讓測試工作 簡單得令人吃驚(和愉快),給測試工作帶來了便利。二者也都擁有活躍的社區 ,為主動開發提供支持,同時生成豐富的文檔。

兩個框架的不同在於核 心設計。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 裡,在 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 所示。

正如我在本文開頭所說,JUnit 4 和 TestNG 在表面上是相似的。然而,設 計 JUnit 的目的是為了分析代碼單元,而 TestNG 的預期用途則針對高級測試 。對於大型測試套件,我們不希望在某一項測試失敗時就得重新運行數千項測試 ,TestNG 的靈活性在這裡尤為有用。這兩個框架都有自己的優勢,您可以隨意 同時使用它們。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved