使用 Java 5 註釋精簡測試
JUnit 4 放棄了過去嚴格的命名規範和繼承層次,轉而推崇 Java™ 5 註釋的靈活性。在本教程中,作爲對他的關於代碼質量這個流行系列的補充,測試專家 Andrew Glover 介紹瞭如何充分利用由註釋實現的新功能,包括參數測試、異常測試及計時測試。他也介紹了 JUnit 4 的靈活固件,展示瞭如何使用註釋(而不是套件)在運行測試前對測試進行邏輯分組。該教程包括幾個在 Eclipse 下運行的測試樣例,以及如何在較早的 Ant 版本中運行 JUnit 4 的指導。
0 評論:
在 IBM Bluemix 雲平臺上開發並部署您的下一個應用。
開始之前
關於本教程
引入 Java 5 註釋爲 JUnit 帶來了顯著改變,使它從一個受廣大開發人員瞭解和喜愛的測試框架轉變成了一個更爲精簡但卻不那麼爲人熟知的框架。在本教程中,我將探討 JUnit 4 最重要的轉變,並介紹一些您也許已經耳聞但還沒用過的激動人心的新功能。
目標
本教程將循序漸進地向您講述 JUnit 4 的基本概念,側重於新的 Java 5 註釋。通過這個一小時教程的學習,您將能夠理解 JUnit 4 的主要改變,也將熟悉這些功能,如異常測試、參數測試以及新的靈活固件模型。您還將瞭解如何聲明測試,如何使用註釋(而不是套件)在運行測試前從邏輯上對其分組,如何在 Eclipse 3.2 或 Ant 中運行測試,以及如何從命令行運行測試。
先決條件
爲更好地學習本教程,您應該大體熟悉 Java 開發。本教程也假設您理解開發人員測試的價值,並熟悉基本模式匹配。爲學習運行 JUnit 4 測試這個章節,您應該能夠將 Eclipse 3.2 作爲一個 IDE 使用,也應該能夠使用 Ant 1.6 或更新版本。本教程不要求您熟悉 JUnit 以前的版本。
系統需求
爲學習本教程及試驗本教程中的代碼,需要一份 Sun 的 JDK 1.5.0_09(或更新版本)的工作安裝版,或針對 Java 技術 1.5.0 SR3 的 IBM 開發工具包的工作安裝版。對於在 Eclipse 中運行 JUnit 4 這些章節,需要一份 Eclipse 3.2 或更新版本的工作安裝版。對於有關 Ant 的章節,需要 1.6 版或更新版。
本教程推薦的系統配置如下:
- 系統要支持 Sun JDK 1.5.0_09 (或更新版本)或針對 Java 技術 1.5.0 SR3 的 IBM 開發工具包,至少有 500 MB 主存。
- 至少有 20 MB 磁盤空間來安裝軟件組件和文中提到的樣例。
本教程的說明基於 Microsoft Windows 操作系統。教程中涵蓋的所有工具也可以在 Linux 和 UNIX 系統中運行。
JUnit 4 的新功能
藉助 Java 5 註釋,JUnit 4 比從前更輕(量級),也更加靈活。JUnit 4 放棄了嚴格的命名規範和繼承層次,轉向了一些令人激動的新功能。下面是一份關於 JUnit 4 新功能的快速列表:
- 參數測試
- 異常測試
- 超時測試
- 靈活固件
- 忽略測試的簡單方法
- 對測試進行邏輯分組的新方法
首先,我要解釋 JUnit 4 最重要最令人激動的改變,爲在稍後的章節中介紹這些功能和更多新功能做好準備。
摒棄舊規則
在將 Java 5 註釋添加到 JUnit 4 之前,該框架已經建立起兩條對其運行能力至爲重要的規則。第一條規則是:JUnit 明確要求任何作爲邏輯測試而編寫的方法要以 test 這個詞開頭。任何以該詞開頭的方法,如 testUserCreate
,均應按照一個定義良好的測試過程來執行,從而保證固件在測試方法前和測試方法後均要執行。第二條規則:爲了讓
JUnit 識別包含測試的類對象,要求類本身從 JUnit 的 TestCase
(或它的一些派生類)中擴展。破壞了這兩條規則中任意一條規則的測試將不會運行。
清單 1 是一個在 JUnit 4 之前編寫的 JUnit 測試
清單 1. 有必要這麼難嗎?
import java.util.regex.Matcher; import java.util.regex.Pattern; import junit.framework.TestCase; public class RegularExpressionTest extends TestCase { private String zipRegEx = "^\\d{5}([\\-]\\d{4})?$"; private Pattern pattern; protected void setUp() throws Exception { this.pattern = Pattern.compile(this.zipRegEx); } public void testZipCode() throws Exception{ Matcher mtcher = this.pattern.matcher("22101"); boolean isValid = mtcher.matches(); assertTrue("Pattern did not validate zip code", isValid); } }
許多人辯稱 JUnit 4 使用註釋是受到 TestNG 和 .NET 的 NUnit 的影響。參見 參考資料,瞭解更多有關其他測試框架中的註釋的信息。
引入新方法
JUnit 4 使用 Java 5 註釋來徹底淘汰了這兩條規則。現在,不再需要類層次,而且那些想要實現測試功能的方法只需要用一個新定義的 @Test
註釋來修飾就可以了。
清單 2 顯示了與 清單 1 相同的測試,只不過這次用註釋進行了重新定義:
清單 2. 含註釋的測試
import java.util.regex.Matcher; import java.util.regex.Pattern; import org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.assertTrue; public class RegularExpressionTest { private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$"; private static Pattern pattern; @BeforeClass public static void setUpBeforeClass() throws Exception { pattern = Pattern.compile(zipRegEx); } @Test public void verifyGoodZipCode() throws Exception{ Matcher mtcher = this.pattern.matcher("22101"); boolean isValid = mtcher.matches(); assertTrue("Pattern did not validate zip code", isValid); } }
清單 2 中的測試在編碼上也許並不會比原來簡單很多,但卻一定更加容易理解。
簡化文檔
註釋的一個有用的副作用是它們將方法要做的事明確地文檔化,而不 需要對該框架的內部模型有深入的理解。還有什麼比用 @Test
修飾測試方法更簡潔明瞭的呢?這是對舊版
JUnit 的巨大改進,舊版 JUnit 要求您對 JUnit 規範要相當熟悉,即使您想要的僅僅是理解每個方法對一個完整測試用例的貢獻。
在解析已經寫好的測試方面,註釋能提供很多幫助,但當您看到註釋爲編寫測試的過程帶來的額外幫助後,就會更被它們所吸引。
用註釋進行測試
Java 5 註釋讓 JUnit 4 成爲了一個與以往版本顯著不同的框架。在本節中,您將瞭解如何在一些關鍵的地方(如測試聲明、異常測試以及超時測試)使用註釋,以及如何忽略不想要或無用的測試。
測試聲明
在 JUnit 4 中聲明一個測試實際上就是用 @Test
註釋修飾測試方法。注意,不需要從任何特定的類中擴展,如清單
3 所示:
清單 3. JUnit 4 中的測試聲明
import java.util.regex.Matcher; import java.util.regex.Pattern; import org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.assertFalse; public class RegularExpressionTest { private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$"; private static Pattern pattern; @BeforeClass public static void setUpBeforeClass() throws Exception { pattern = Pattern.compile(zipRegEx); } @Test public void verifyZipCodeNoMatch() throws Exception{ Matcher mtcher = this.pattern.matcher("2211"); boolean notValid = mtcher.matches(); assertFalse("Pattern did validate zip code", notValid); } }
有關靜態導入的一點說明
我使用了 Java 5 的靜態導入功能來導入清單 3 中 Assert
類的 assertFalse()
方法。這是因爲不同於以往的
JUnit 版本,測試類不從TestCase
中擴展。
異常測試
在以往的 JUnit 版本中,指定測試拋出 Exception
通常都是一個很好的做法。只有在測試一個特別的異常時,纔會想要忽略這條規則。如果測試拋出一個異常,該框架會報告一次失敗。
如果真的想要測試一個特別的異常,JUnit 4 的 @Test
註釋支持一個 expected
參數,該參數意在表示測試在執行中拋出的異常類型。
下面以一個簡單的比較來說明新參數的不同之處。
JUnit 3.8 中的異常測試
清單 4 中的 JUnit 3.8 測試(命名爲 testZipCodeGroupException()
)驗證了試圖獲取第三組正則表達式(我聲明的)將會導致一個IndexOutOfBoundsException
:
清單 4. 在 JUnit 3.8 中測試一個異常
import java.util.regex.Matcher; import java.util.regex.Pattern; import junit.framework.TestCase; public class RegularExpressionTest extends TestCase { private String zipRegEx = "^\\d{5}([\\-]\\d{4})?$"; private Pattern pattern; protected void setUp() throws Exception { this.pattern = Pattern.compile(this.zipRegEx); } public void testZipCodeGroupException() throws Exception{ Matcher mtcher = this.pattern.matcher("22101-5051"); boolean isValid = mtcher.matches(); try{ mtcher.group(2); fail("No exception was thrown"); }catch(IndexOutOfBoundsException e){ } } }
這個舊版的 JUnit 需要我爲這麼一個簡單的測試寫那麼多代碼 —— 即編寫一個 try
/catch
,如果沒有捕捉到異常,就會讓測試失敗。
JUnit 4 中的異常測試
除了使用新的 expected
參數外,清單
5 中的異常測試和清單 4 中的沒多大區別。(注意,我可以通過將 IndexOutOfBoundsException
異常傳入到 @Test
註釋來翻新清單
4 中的測試。)
清單 5. 含 ‘expected’ 參數的異常測試
import java.util.regex.Matcher; import java.util.regex.Pattern; import org.junit.BeforeClass; import org.junit.Test; public class RegularExpressionJUnit4Test { private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$"; private static Pattern pattern; @BeforeClass public static void setUpBeforeClass() throws Exception { pattern = Pattern.compile(zipRegEx); } @Test(expected=IndexOutOfBoundsException.class) public void verifyZipCodeGroupException() throws Exception{ Matcher mtcher = this.pattern.matcher("22101-5051"); boolean isValid = mtcher.matches(); mtcher.group(2); } }
超時測試
在 JUnit 4 中,測試用例可以將超時值作爲參數。正如在清單 6 中所見,timeout
值代表測試能夠運行的最長時間:如果時間超過,測試就會失敗。
清單 6. 含超時值的測試
@Test(timeout=1) public void verifyFastZipCodeMatch() throws Exception{ Pattern pattern = Pattern.compile("^\\d{5}([\\-]\\d{4})?$"); Matcher mtcher = pattern.matcher("22011"); boolean isValid = mtcher.matches(); assertTrue("Pattern did not validate zip code", isValid); }
含超時的測試很容易:用跟着 timeout
值的 @Test
修飾一個方法,就能獲得一個自動的超時測試!
忽略測試
在 JUnit 4 以前,忽略壞掉的或不完整的測試讓人很頭疼。如果想讓框架忽略一個特別的測試,不得不修改測試名,故意不讓它遵循測試的命名規則。例如,我經常把一個 “_” 放到測試名前面,來提示該測試不在當前運行。
JUnit 4 引入了一個被適當命名爲 @Ignore
的註釋,它迫使該框架忽略掉一個特別的測試方法。也可以傳入一條消息來向恰巧進行這項忽略測試的可信的開發人員傳達您的決定。
@Ignore 註釋
清單 7 展示了忽略掉一個正則表達式仍不起作用的測試是多麼簡單:
清單 7. 忽略這個測試
@Ignore("this regular expression isn't working yet") @Test public void verifyZipCodeMatch() throws Exception{ Pattern pattern = Pattern.compile("^\\d{5}([\\-]\\d{4})"); Matcher mtcher = pattern.matcher("22011"); boolean isValid = mtcher.matches(); assertTrue("Pattern did not validate zip code", isValid); }
報告被忽略的測試
在 Eclipse 中運行這項測試會報告一項被忽略的測試,如圖 1 所示:
圖 1. Eclipse 中出現被忽略的測試
測試固件
測試固件並不是 JUnit 4 的新功能,但固件模型卻是新的且改良過的。在本節中,我會解釋爲何及在何處需要使用固件,然後介紹舊版的不靈活固件與 JUnit 4 中大放異彩的新模型之間的區別。
爲什麼使用固件?
固件通過一個契約來倡導重用,該契約確保該特殊邏輯在測試之前或之後運行。在舊版的 JUnit 中,不管是否實現一個固件,這個契約都是隱式的。但 JUnit 4 卻通過註釋將固件顯式化,這意味着只有在您真的決定使用固件時,該契約才成爲強制的。
通過一個確保固件能在測試之前或之後運行的契約,可以編碼可重用邏輯。例如,這種邏輯可能是初始化一個將在多個測試用例中測試的類,也可能是在運行一個數據依賴測試前填充數據庫。不論是哪一種邏輯,使用固件都會確保一個更容易管理的測試用例:依賴於普通邏輯的測試用例。
當運行許多使用相同邏輯的測試,且一些測試或全部測試失敗時,固件會變得特別方便。與其在每個測試設置的邏輯間切換,不如只在一個地方歸納導致失敗的原因。除此之外,如果一些測試通過而另一些失敗,您就能避免將該固件邏輯作爲全部失敗的源頭來檢查。
不靈活固件
舊版的 JUnit 使用一個在某種程度上不太靈活的固件模型,要求用 setUp()
和 tearDown()
方法將每個測試方法包裝起來。在清單
8 中可以看到該模型的一個潛在缺陷,其中實現了 setUp()
方法,因而運行了兩次
—— 爲每個定義的測試運行一次:
清單 8. 不靈活的固件
import java.util.regex.Matcher; import java.util.regex.Pattern; import junit.framework.TestCase; public class RegularExpressionTest extends TestCase { private String zipRegEx = "^\\d{5}([\\-]\\d{4})?$"; private Pattern pattern; protected void setUp() throws Exception { this.pattern = Pattern.compile(this.zipRegEx); } public void testZipCodeGroup() throws Exception{ Matcher mtcher = this.pattern.matcher("22101-5051"); boolean isValid = mtcher.matches(); assertEquals("group(1) didn't equal -5051", "-5051", mtcher.group(1)); } public void testZipCodeGroupException() throws Exception{ Matcher mtcher = this.pattern.matcher("22101-5051"); boolean isValid = mtcher.matches(); try{ mtcher.group(2); fail("No exception was thrown"); }catch(IndexOutOfBoundsException e){ } } }
圍繞固件工作
在 JUnit 之前的版本中,使用 TestSetup
裝飾器,指定一個固件只運行一次是可能的,但這是一個很麻煩的操作,如清單
9 所示(注意所要求的 suite()
方法):
清單 9. JUnit 4 之前版本的 TestSetup
import java.util.regex.Matcher; import java.util.regex.Pattern; import junit.extensions.TestSetup; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; import junit.textui.TestRunner; public class OneTimeRegularExpressionTest extends TestCase { private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$"; private static Pattern pattern; public static Test suite() { TestSetup setup = new TestSetup( new TestSuite(OneTimeRegularExpressionTest.class)) { protected void setUp() throws Exception { pattern = Pattern.compile(zipRegEx); } }; return setup; } public void testZipCodeGroup() throws Exception { Matcher mtcher = pattern.matcher("22101-5051"); boolean isValid = mtcher.matches(); assertEquals("group(1) didn't equal -5051", "-5051", mtcher.group(1)); } public void testZipCodeGroupException() throws Exception { Matcher mtcher = pattern.matcher("22101-5051"); boolean isValid = mtcher.matches(); try { mtcher.group(2); fail("No exception was thrown"); } catch (IndexOutOfBoundsException e) { } } }
一句話,在 JUnit 4 之前,使用固件往往得不償失。
4.0 版中的靈活性
JUnit 4 使用註釋來減少固件花費的成本,允許爲每個測試運行一次固件,或爲整個類運行一次固件,或一次也不運行。有四種固件註釋:針對類層次的固件有兩種,針對方法層次的固件有兩種。在類層次,有 @BeforeClass
和 @AfterClass
,在方法(或測試)層次,有 @Before
和@After
。
清單 10 中的測試用例包括一個使用 @Before
註釋的固件,該固件針對兩個測試運行:
清單 10. 使用註釋的靈活固件
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
public class RegularExpressionJUnit4Test {
private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
private static Pattern pattern;
@Before
public static void setUpBeforeClass() throws Exception {
pattern = Pattern.compile(zipRegEx);
}
@Test
public void verifyZipCodeNoMatch() throws Exception{
Matcher mtcher = this.pattern.matcher("2211");
boolean notValid = mtcher.matches();
assertFalse("Pattern did validate zip code", notValid);
}
@Test(expected=IndexOutOfBoundsException.class)
public void verifyZipCodeGroupException() throws Exception{
Matcher mtcher = this.pattern.matcher("22101-5051");
boolean isValid = mtcher.matches();
mtcher.group(2);
}
}
一次性固件
如果只想運行一次固件會怎樣呢?與其實現一箇舊式的裝飾器,如清單 9 所示,您不如使用 @BeforeClass
註釋,如清單
11 所示:
清單 11. JUnit 4 中的一次性設置
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
public class RegularExpressionJUnit4Test {
private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
private static Pattern pattern;
@BeforeClass
public static void setUpBeforeClass() throws Exception {
pattern = Pattern.compile(zipRegEx);
}
@Test
public void verifyZipCodeNoMatch() throws Exception{
Matcher mtcher = this.pattern.matcher("2211");
boolean notValid = mtcher.matches();
assertFalse("Pattern did validate zip code", notValid);
}
@Test(expected=IndexOutOfBoundsException.class)
public void verifyZipCodeGroupException() throws Exception{
Matcher mtcher = this.pattern.matcher("22101-5051");
boolean isValid = mtcher.matches();
mtcher.group(2);
}
}
tearDown() 功能
爲了防止您疑惑,順便提一下,舊的 tearDown()
功能還沒從新固件模型中去除。如果想要執行一個 tearDown()
,只要創建一個新方法並根據需要使用 @After
或 @AfterClass
即可。
靈活性的特例
可以爲 JUnit 4 中的一個測試用例指定多個固件。新的註釋驅動型固件並不阻止您創建多個 @BeforeClass
固件方法。請牢記,儘管如此,在當前的
JUnit 4 版本中,尚不能指定先運行哪個固件方法,這一點在您決定使用多個固件時就會變得十分棘手。
運行:在 JUnit 4 中測試
新的改進版 JUnit 4 最顯著的特性是沒有套件 —— 套件機制用於將測試從邏輯上分組並將這些測試作爲一個單個單元來運行。在本節中,我會介紹替代了套件的新的精簡註釋,並介紹如何在 Eclipse 和 Ant 下運行 JUnit 4 測試。
舊式套件
找到清單 12 所示的舊式 JUnit 套件(這個套件將兩個邏輯測試類分組,並將它們作爲一個單個的單元運行),就能看到不同之處:
清單 12. 舊式 JUnit 套件
import junit.framework.Test; import junit.framework.TestSuite; public class JUnit3Suite { public static Test suite() { TestSuite suite = new TestSuite(); suite.addTest(OneTimeRegularExpressionTest.suite()); suite.addTestSuite(RegularExpressionTest.class); return suite; } }
兩個新註釋
在 JUnit 4 中,套件語義被兩個新註釋所替代。第一個是 @RunWith
,設計它是爲了方便讓不同的運行器(除了構建進框架的運行器)執行一個特別的測試類。JUnit
4 綁定一個叫做 Suite
的套件運行器,必須在 @RunWith
註釋中指定這個運行器。不僅如此,還必須提供另一項叫做@SuiteClasses
的註釋,它將一個意欲表示測試套件的類列表作爲參數。
清單 13. 這些註釋太棒了
import org.junit.runner.RunWith; import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; @RunWith(Suite.class) @SuiteClasses({ParametricRegularExpressionTest.class, RegularExpressionTest.class, TimedRegularExpressionTest.class}) public class JUnit4Suite { }
如果新 JUnit 4 註釋令您感到不可思議,別擔心。顯然 JUnit 4 的創造者們也有同感。在 Javadocs 中,他們解釋說隨着他們“對人們實際使用它的方式的不斷了解”,運行器 API 將會有所改變。至少他們是誠實的!
在 Eclipse 中運行 JUnit 4 測試
您可以選擇通過如 Eclipse 的 IDE 或命令行來運行 JUnit 4 測試類。在 Eclipse 3.2 或以上版本中運行 JUnit 測試的方法是選擇 Run As JUnit 測試選項。通過命令行運行測試要求執行org.junit.runner.JUnitCore
類,並將完整有效的測試名作爲參數傳遞。
例如,在 Eclipse 中,如果不想使用綁定的 JUnit 運行器,可以定義一個新運行配置,並首先指定 JUnitCore
類,如我在圖
2 中所爲:
圖 2. 在 Eclipse 中運行 JUnit 4 命令行測試的第一步
指定一個測試
下一步,需要通過將完整有效的測試名添加到 Arguments 標籤的 “Program arguments” 文本框中,來指定運行哪個測試,如圖 3 所示:
圖 3. 在 Eclipse 中運行 JUnit 命令行測試的第二步
Ant 和 JUnit 4
現在,Ant 和 JUnit 成爲完美組合已久,許多開發人員預料這種關係在引入 JUnit 4 後只會變得更好。但結果是,存在一定問題。如果您正在運行 Ant 1.7 之前的任何版本,將不能輕易地運行現成的 JUnit 4 測試。那並不是說您不能運行這些測試 —— 而只是不能立刻運行這些測試。
不般配的一對
在 Ant(1.7 以前的版本)中運行 JUnit 4 測試(在清單 14 中)會產生一些有趣的結果;
清單 14. 一個簡單的 JUnit 4 測試類
import java.util.regex.Matcher; import java.util.regex.Pattern; import org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.assertTrue; public class RegularExpressionTest { private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$"; private static Pattern pattern; @BeforeClass public static void setUpBeforeClass() throws Exception { pattern = Pattern.compile(zipRegEx); } @Test public void verifyGoodZipCode() throws Exception{ Matcher mtcher = this.pattern.matcher("22101"); boolean isValid = mtcher.matches(); assertTrue("Pattern did not validate zip code", isValid); } }
多處失敗
在 Ant 中使用脆弱的 junit
任務會導致清單
15 中的錯誤:
清單 15. 一堆錯誤
[junit] Running test.com.acme.RegularExpressionTest [junit] Tests run: 1, Failures: 1, Errors: 0, Time elapsed: 0.047 sec [junit] Testsuite: test.com.acme.RegularExpressionTest [junit] Tests run: 1, Failures: 1, Errors: 0, Time elapsed: 0.047 sec [junit] Testcase: warning took 0.016 sec [junit] FAILED [junit] No tests found in test.com.acme.RegularExpressionTest [junit] junit.framework.AssertionFailedError: No tests found in test.com.acme.RegularExpressionTest [junit] Test test.com.acme.RegularExpressionTest FAILED
相應的解決方案
如果想要在 Ant 1.7 版之前的版本上運行 JUnit 4 測試,必須用 suite()
方法來翻新測試用例,該方法返回一個 JUnit4TestAdapter
實例,如清單
16 所示:
清單 16. 舊方法的新用法
public static junit.framework.Test suite(){ return new JUnit4TestAdapter(RegularExpressionTest.class); }
由於和 @Test
註釋的名稱相似,所以必須讓這個實例中的 Test
的返回類型名稱完整。一旦 suite()
方法準備就緒,任何版本的
Ant 都會愉快地運行您的 JUnit 4 測試!
參數測試
偶爾,應用程序的業務邏輯要求您編寫許多不定量的測試來保證其健壯。在 JUnit 之前的版本中,這種場景很不方便,主要是因爲一個測試中方法的參數組各不相同,意味着要爲每一個單獨的組編寫一個測試用例。
JUnit 4 引入了一項卓越的新功能,即能夠創建由參數值供給的通用測試。結果是,您可以創建一個單個的測試用例並多次運行 —— 爲您創建的每個參數運行一次。
參數的簡潔性
在 JUnit 4 中創建參數測試只需要五個步驟:
- 創建一個不含參數的通用測試。
-
創建一個返回
Collection
類型的static
feeder 方法,並用@Parameter
註釋加以修飾。 - 爲在步驟 1 中定義的通用方法所要求的參數類型創建類成員。
- 創建一個持有這些參數類型的構造函數,並把這些參數類型和步驟 3 中定義的類成員相應地聯繫起來。
-
通過
@RunWith
註釋,指定測試用例和Parameterized
類一起運行。
我將逐一介紹這些步驟。
步驟 1. 創建一個通用測試
清單 17 顯示了驗證一個正則表達式各種值的通用測試。注意 phrase
和 match
值未經定義。
清單 17. 一個通用測試
@Test public void verifyGoodZipCode() throws Exception{ Matcher mtcher = this.pattern.matcher(phrase); boolean isValid = mtcher.matches(); assertEquals("Pattern did not validate zip code", isValid, match); }
步驟 2. 創建一個 feeder 方法
下一步是創建一個 feeder 方法,必須將該方法聲明爲 static
並返回一個 Collection
類型。需要用 @Parameters
註釋來修飾該方法。在該方法內部,僅僅創建一個多維 Object
數組,並將該數組轉換爲 List
,如清單
18 所示:
清單 18. 含 @Parameters 註釋的 feeder 方法
@Parameters public static Collection regExValues() { return Arrays.asList(new Object[][] { {"22101", true }, {"221x1", false }, {"22101-5150", true }, {"221015150", false }}); }
步驟 3. 創建兩個類成員
由於這些參數是 String
和 boolean
類型的,所以下一步要創建兩個類成員:
清單 19. 聲明兩個類成員
private String phrase; private boolean match;
步驟 4. 創建一個構造函數
接下來創建的構造函數將類成員和參數值聯繫起來,如清單 20 所示:
清單 20. 匹配值的構造函數
public ParametricRegularExpressionTest(String phrase, boolean match) { this.phrase = phrase; this.match = match; }
步驟 5. 指定 Parameterized 類
最後,在類層次指定此測試必須和 Parameterized
類一起運行,如清單
21 所示:
清單 21. 指定 Parameterized 和 @RunWith 註釋
@RunWith(Parameterized.class) public class ParametricRegularExpressionTest { //... }
運行測試
執行該測試類時,通用 verifyGoodZipCode()
測試方法運行四次,爲定義在清單
18 的 regExValues()
數據
feeder 方法中的每個值對運行一次。
例如,如果在 Eclipse 下運行這個測試,會報告運行四個測試,如圖 4 所示:
圖 4. 在 Eclipse 中運行的參數測試
還有什麼新功能?
除了到目前爲止探討過的重要改變,JUnit 4 還引入了一些小的功能;即,新斷言方法的添加和一個終結狀態的去除。
新斷言
JUnit 4 添加了一個用於比較數組內容的新斷言方法。這並不是什麼重大舉動,但它的確意味着您將不必再在數組內容間迭代,也不必再斷言每個獨立條目了。
例如,清單 22 中的代碼在舊版的 JUnit 中是不可能的。這個測試失敗是由於每個數組第二個元素中的一個微小的區別。
清單 22. JUnit 4 中的 assertEquals 現在支持數組
@Test public void verifyArrayContents() throws Exception{ String[] actual = new String[] {"JUnit 3.8.x", "JUnit 4", "TestNG"}; String[] var = new String[] {"JUnit 3.8.x", "JUnit 4.1", "TestNG 5.5"}; assertEquals("the two arrays should not be equal", actual, var); }
不再有錯誤!
JUnit 4 中一個雖然小但卻很好的改變是它去除了錯誤的概念。之前的版本會既報告失敗數,也報告錯誤數,在 JUnit 4 中,測試要麼通過,要麼失敗。
有趣的是,去除了一個狀態的同時,也添加了一個新狀態,即與忽略測試功能相關的狀態。當執行一系列測試時,JUnit 4 報告運行的測試數、失敗的測試數和忽略掉的測試數。
結束語
JUnit 4 顯著地背離了以往的設計並不意味着該框架執行起來和以往完全不同 —— 原有框架的強大功能和簡潔性仍完好無損。事實上,如果深入發掘該框架,您會發現,儘管添加了一些強制性的新功能,但新框架並未犧牲任何曾引發開發人員測試革命的核心原則。
在本教程中,您親歷了了解 JUnit 4 的全過程,從測試聲明到參數測試。您還發現了一些新功能,如超時測試和異常測試,瞭解了一些熟悉功能的改變,如固件和邏輯分組。您也看到了 Eclipse 中的測試是如何運行的,學習了一個允許在任何版本的 Ant 中(即使是 Ant 1.7 之前的版本)運行測試的簡單方法。
在本教程中,如果有一件事是我希望您瞭解的,那就是:註釋並沒有削弱 JUnit 的功能,相反,它們的確帶來了相當可觀的易用性。試試註釋吧:它們會讓您在不知不覺中就能來去自如地駕馭測試的編寫!