深入探索 JUnit 4
開始之前
關於本教程
引入 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. 有必要這麼難嗎?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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. 含註釋的測試
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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 中的測試宣告
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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 中測試一個異常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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’ 引數的異常測試
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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. 含超時值的測試
1 2 3 4 5 6 7 |
@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. 忽略這個測試
1 2 3 4 5 6 7 8 |
@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. 不靈活的韌體
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 |
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
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 |
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. 使用註釋的靈活韌體
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 |
import java.util.regex.Matcher;
|