本文主要介紹了如何使用 JUnit 4 提供的各種功能開展有效的單元測試,并通過一個實(shí)例演示了如何使用 Ant 執(zhí)行自動化的單元測試。本文假設(shè)讀者對 Eclipse 下進(jìn)行 Java 開發(fā)有一定的經(jīng)驗(yàn),并了解 Java 5 中的注解(annotation)特性。
引言
毋庸置疑,程序員要對自己編寫的代碼負(fù)責(zé),您不僅要保證它能通過編譯,正常地運(yùn)行,而且要滿足需求和設(shè)計(jì)預(yù)期的效果。單元測試正是驗(yàn)證代碼行為是否滿足預(yù)期的有效手段之一。但不可否認(rèn),做測試是件很枯燥無趣的事情,而一遍又一遍的測試則更是讓人生畏的工作。幸運(yùn)的是,單元測試工具 JUnit 使這一切變得簡單藝術(shù)起來。
JUnit 是 Java 社區(qū)中知名度最高的單元測試工具。它誕生于 1997 年,由 Erich Gamma 和 Kent Beck 共同開發(fā)完成。其中 Erich Gamma 是經(jīng)典著作《設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》一書的作者之一,并在 Eclipse 中有很大的貢獻(xiàn);Kent Beck 則是一位極限編程(XP)方面的專家和先驅(qū)。
麻雀雖小,五臟俱全。JUnit 設(shè)計(jì)的非常小巧,但是功能卻非常強(qiáng)大。Martin Fowler 如此評價(jià) JUnit:在軟件開發(fā)領(lǐng)域,從來就沒有如此少的代碼起到了如此重要的作用。它大大簡化了開發(fā)人員執(zhí)行單元測試的難度,特別是 JUnit 4 使用 Java 5 中的注解(annotation)使測試變得更加簡單。
JUnit 4 初體驗(yàn)
在開始體驗(yàn) JUnit 4 之前,我們需要以下軟件的支持:
- Eclipse:最為流行的 IDE,它全面集成了 JUnit,并從版本 3.2 開始支持 JUnit 4。當(dāng)然 JUnit 并不依賴于任何 IDE。您可以從 http://www.eclipse.org/ 上下載最新的 Eclipse 版本。
- Ant:基于 Java 的開源構(gòu)建工具,您可以在 http://ant.apache.org/ 上得到最新的版本和豐富的文檔。Eclipse 中已經(jīng)集成了 Ant,但是在撰寫本文時(shí),Eclipse 使用的 Ant 版本較低(必需 1.7 或者以上版本),不能很好的支持 JUnit 4。
- JUnit:它的官方網(wǎng)站是 http://www.junit.org/。您可以從上面獲取關(guān)于 JUnit 的最新消息。如果您和本文一樣在 Eclipse 中使用 JUnit,就不必再下載了。
首先為我們的體驗(yàn)新建一個 Java 工程 —— coolJUnit?,F(xiàn)在需要做的是,打開項(xiàng)目 coolJUnit 的屬性頁 -> 選擇“Java Build Path”子選項(xiàng) -> 點(diǎn)選“Add Library…”按鈕 -> 在彈出的“Add Library”對話框中選擇 JUnit(圖1),并在下一頁中選擇版本 4.1 后點(diǎn)擊“Finish”按鈕。這樣便把 JUnit 引入到當(dāng)前項(xiàng)目庫中了。
圖1 為項(xiàng)目添加 JUnit 庫
 |
請注意 JDK 的版本
JUnit 4.1 是基于 Java 5 的升級版本,它使用了 Tiger 中的很多新特性來簡化原有的使用方式。正因?yàn)槿绱?,它并不能直接運(yùn)行在 JDK1.4.x 版本上。如果您需要在 JDK1.4.x 版本使用 JUnit 的話,請使用 3.8.1 版本。
|
|
可以開始編寫單元測試了嗎?等等……,您打算把單元測試代碼放在什么地方呢?把它和被測試代碼混在一起,這顯然會照成混亂,因?yàn)閱卧獪y試代碼是不會出現(xiàn)在最終產(chǎn)品中的。建議您分別為單元測試代碼與被測試代碼創(chuàng)建單獨(dú)的目錄,并保證測試代碼和被測試代碼使用相同的包名。這樣既保證了代碼的分離,同時(shí)還保證了查找的方便。遵照這條原則,我們在項(xiàng)目 coolJUnit 根目錄下添加一個新目錄 testsrc,并把它加入到項(xiàng)目源代碼目錄中(加入方式見 圖2)。
圖2 修改項(xiàng)目源代碼目錄
現(xiàn)在我們得到了一條 JUnit 的最佳實(shí)踐:單元測試代碼和被測試代碼使用一樣的包,不同的目錄。
一切準(zhǔn)備就緒,一起開始體驗(yàn)如何使用 JUnit 進(jìn)行單元測試吧。下面的例子來自筆者的開發(fā)實(shí)踐:工具類 WordDealUtil 中的靜態(tài)方法 wordFormat4DB 是專用于處理 Java 對象名稱向數(shù)據(jù)庫表名轉(zhuǎn)換的方法(您可以在代碼注釋中可以得到更多詳細(xì)的內(nèi)容)。下面是第一次編碼完成后大致情形:
package com.ai92.cooljunit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 對名稱、地址等字符串格式的內(nèi)容進(jìn)行格式檢查
* 或者格式化的工具類
*
* @author Ai92
*/
public class WordDealUtil {
/**
* 將Java對象名稱(每個單詞的頭字母大寫)按照
* 數(shù)據(jù)庫命名的習(xí)慣進(jìn)行格式化
* 格式化后的數(shù)據(jù)為小寫字母,并且使用下劃線分割命名單詞
*
* 例如:employeeInfo 經(jīng)過格式化之后變?yōu)?employee_info
*
* @param name Java對象名稱
*/
public static String wordFormat4DB(String name){
Pattern p = Pattern.compile("[A-Z]");
Matcher m = p.matcher(name);
StringBuffer sb = new StringBuffer();
while(m.find()){
m.appendReplacement(sb, "_"+m.group());
}
return m.appendTail(sb).toString().toLowerCase();
}
}
|
它是否能按照預(yù)期的效果執(zhí)行呢?嘗試為它編寫 JUnit 單元測試代碼如下:
package com.ai92.cooljunit;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class TestWordDealUtil {
//測試wordFormat4DB正常運(yùn)行的情況
@Test public void wordFormat4DBNormal(){
String target = "employeeInfo";
String result = WordDealUtil.wordFormat4DB(target);
assertEquals("employee_info", result);
}
}
|
很普通的一個類嘛!測試類 TestWordDealUtil 之所以使用“Test”開頭,完全是為了更好的區(qū)分測試類與被測試類。測試方法 wordFormat4DBNormal 調(diào)用執(zhí)行被測試方法 WordDealUtil.wordFormat4DB,以判斷運(yùn)行結(jié)果是否達(dá)到設(shè)計(jì)預(yù)期的效果。需要注意的是,測試方法 wordFormat4DBNormal 需要按照一定的規(guī)范書寫:
- 測試方法必須使用注解 org.junit.Test 修飾。
- 測試方法必須使用 public void 修飾,而且不能帶有任何參數(shù)。
測試方法中要處理的字符串為“employeeInfo”,按照設(shè)計(jì)目的,處理后的結(jié)果應(yīng)該為“employee_info”。assertEquals 是由 JUnit 提供的一系列判斷測試結(jié)果是否正確的靜態(tài)斷言方法(位于類 org.junit.Assert 中)之一,我們使用它將執(zhí)行結(jié)果 result 和預(yù)期值“employee_info”進(jìn)行比較,來判斷測試是否成功。
看看運(yùn)行結(jié)果如何。在測試類上點(diǎn)擊右鍵,在彈出菜單中選擇 Run As JUnit Test。運(yùn)行結(jié)果如下圖所示:
圖3 JUnit 運(yùn)行成功界面
綠色的進(jìn)度條提示我們,測試運(yùn)行通過了。但現(xiàn)在就宣布代碼通過了單元測試還為時(shí)過早。記住:您的單元測試代碼不是用來證明您是對的,而是為了證明您沒有錯。因此單元測試的范圍要全面,比如對邊界值、正常值、錯誤值得測試;對代碼可能出現(xiàn)的問題要全面預(yù)測,而這也正是需求分析、詳細(xì)設(shè)計(jì)環(huán)節(jié)中要考慮的。顯然,我們的測試才剛剛開始,繼續(xù)補(bǔ)充一些對特殊情況的測試:
public class TestWordDealUtil {
……
//測試 null 時(shí)的處理情況
@Test public void wordFormat4DBNull(){
String target = null;
String result = WordDealUtil.wordFormat4DB(target);
assertNull(result);
}
//測試空字符串的處理情況
@Test public void wordFormat4DBEmpty(){
String target = "";
String result = WordDealUtil.wordFormat4DB(target);
assertEquals("", result);
}
//測試當(dāng)首字母大寫時(shí)的情況
@Test public void wordFormat4DBegin(){
String target = "EmployeeInfo";
String result = WordDealUtil.wordFormat4DB(target);
assertEquals("employee_info", result);
}
//測試當(dāng)尾字母為大寫時(shí)的情況
@Test public void wordFormat4DBEnd(){
String target = "employeeInfoA";
String result = WordDealUtil.wordFormat4DB(target);
assertEquals("employee_info_a", result);
}
//測試多個相連字母大寫時(shí)的情況
@Test public void wordFormat4DBTogether(){
String target = "employeeAInfo";
String result = WordDealUtil.wordFormat4DB(target);
assertEquals("employee_a_info", result);
}
}
|
再次運(yùn)行測試。很遺憾,JUnit 運(yùn)行界面提示我們有兩個測試情況未通過測試(圖4)——當(dāng)首字母大寫時(shí)得到的處理結(jié)果與預(yù)期的有偏差,造成測試失?。╢ailure);而當(dāng)測試對 null 的處理結(jié)果時(shí),則直接拋出了異常——測試錯誤(error)。顯然,被測試代碼中并沒有對首字母大寫和 null 這兩種特殊情況進(jìn)行處理,修改如下:
//修改后的方法wordFormat4DB
/**
* 將Java對象名稱(每個單詞的頭字母大寫)按照
* 數(shù)據(jù)庫命名的習(xí)慣進(jìn)行格式化
* 格式化后的數(shù)據(jù)為小寫字母,并且使用下劃線分割命名單詞
* 如果參數(shù)name為null,則返回null
*
* 例如:employeeInfo 經(jīng)過格式化之后變?yōu)?employee_info
*
* @param name Java對象名稱
*/
public static String wordFormat4DB(String name){
if(name == null){
return null;
}
Pattern p = Pattern.compile("[A-Z]");
Matcher m = p.matcher(name);
StringBuffer sb = new StringBuffer();
while(m.find()){
if(m.start() != 0)
m.appendReplacement(sb, ("_"+m.group()).toLowerCase());
}
return m.appendTail(sb).toString().toLowerCase();
}
|
圖4 JUnit 運(yùn)行失敗界面
JUnit 將測試失敗的情況分為兩種:failure 和 error。Failure 一般由單元測試使用的斷言方法判斷失敗引起,它表示在測試點(diǎn)發(fā)現(xiàn)了問題;而 error 則是由代碼異常引起,這是測試目的之外的發(fā)現(xiàn),它可能產(chǎn)生于測試代碼本身的錯誤(測試代碼也是代碼,同樣無法保證完全沒有缺陷),也可能是被測試代碼中的一個隱藏的bug。
 |
請牢記!
請牢記這一條 JUnit 最佳實(shí)踐:測試任何可能的錯誤。單元測試不是用來證明您是對的,而是為了證明您沒有錯。
|
|
啊哈,再次運(yùn)行測試,綠條又重現(xiàn)眼前。通過對 WordDealUtil.wordFormat4DB 比較全面的單元測試,現(xiàn)在的代碼已經(jīng)比較穩(wěn)定,可以作為 API 的一部分提供給其它模塊使用了。
不知不覺中我們已經(jīng)使用 JUnit 漂亮的完成了一次單元測試??梢泽w會到 JUnit 是多么輕量級,多么簡單,根本不需要花心思去研究,這就可以把更多的注意力放在更有意義的事情上——編寫完整全面的單元測試。
JUnit 深入
當(dāng)然,JUnit 提供的功能決不僅僅如此簡單,在接下來的內(nèi)容中,我們會看到 JUnit 中很多有用的特性,掌握它們對您靈活的編寫單元測試代碼非常有幫助。
Fixture
何謂 Fixture?它是指在執(zhí)行一個或者多個測試方法時(shí)需要的一系列公共資源或者數(shù)據(jù),例如測試環(huán)境,測試數(shù)據(jù)等等。在編寫單元測試的過程中,您會發(fā)現(xiàn)在大部分的測試方法在進(jìn)行真正的測試之前都需要做大量的鋪墊——為設(shè)計(jì)準(zhǔn)備 Fixture 而忙碌。這些鋪墊過程占據(jù)的代碼往往比真正測試的代碼多得多,而且這個比率隨著測試的復(fù)雜程度的增加而遞增。當(dāng)多個測試方法都需要做同樣的鋪墊時(shí),重復(fù)代碼的“壞味道”便在測試代碼中彌漫開來。這股“壞味道”會弄臟您的代碼,還會因?yàn)槭韬鲈斐慑e誤,應(yīng)該使用一些手段來根除它。
JUnit 專門提供了設(shè)置公共 Fixture 的方法,同一測試類中的所有測試方法都可以共用它來初始化 Fixture 和注銷 Fixture。和編寫 JUnit 測試方法一樣,公共 Fixture 的設(shè)置也很簡單,您只需要:
- 使用注解 org,junit.Before 修飾用于初始化 Fixture 的方法。
- 使用注解 org.junit.After 修飾用于注銷 Fixture 的方法。
- 保證這兩種方法都使用 public void 修飾,而且不能帶有任何參數(shù)。
遵循上面的三條原則,編寫出的代碼大體是這個樣子:
//初始化Fixture方法
@Before public void init(){……}
//注銷Fixture方法
@After public void destroy(){……}
|
這樣,在每一個測試方法執(zhí)行之前,JUnit 會保證 init 方法已經(jīng)提前初始化測試環(huán)境,而當(dāng)此測試方法執(zhí)行完畢之后,JUnit 又會調(diào)用 destroy 方法注銷測試環(huán)境。注意是每一個測試方法的執(zhí)行都會觸發(fā)對公共 Fixture 的設(shè)置,也就是說使用注解 Before 或者 After 修飾的公共 Fixture 設(shè)置方法是方法級別的(圖5)。這樣便可以保證各個獨(dú)立的測試之間互不干擾,以免其它測試代碼修改測試環(huán)境或者測試數(shù)據(jù)影響到其它測試代碼的準(zhǔn)確性。
圖5 方法級別 Fixture 執(zhí)行示意圖
可是,這種 Fixture 設(shè)置方式還是引來了批評,因?yàn)樗实拖?,特別是在設(shè)置 Fixture 非常耗時(shí)的情況下(例如設(shè)置數(shù)據(jù)庫鏈接)。而且對于不會發(fā)生變化的測試環(huán)境或者測試數(shù)據(jù)來說,是不會影響到測試方法的執(zhí)行結(jié)果的,也就沒有必要針對每一個測試方法重新設(shè)置一次 Fixture。因此在 JUnit 4 中引入了類級別的 Fixture 設(shè)置方法,編寫規(guī)范如下:
- 使用注解 org,junit.BeforeClass 修飾用于初始化 Fixture 的方法。
- 使用注解 org.junit.AfterClass 修飾用于注銷 Fixture 的方法。
- 保證這兩種方法都使用 public static void 修飾,而且不能帶有任何參數(shù)。
類級別的 Fixture 僅會在測試類中所有測試方法執(zhí)行之前執(zhí)行初始化,并在全部測試方法測試完畢之后執(zhí)行注銷方法(圖6)。代碼范本如下:
//類級別Fixture初始化方法
@BeforeClass public static void dbInit(){……}
//類級別Fixture注銷方法
@AfterClass public static void dbClose(){……}
|
圖6 類級別 Fixture 執(zhí)行示意圖
異常以及時(shí)間測試
注解 org.junit.Test 中有兩個非常有用的參數(shù):expected 和 timeout。參數(shù) expected 代表測試方法期望拋出指定的異常,如果運(yùn)行測試并沒有拋出這個異常,則 JUnit 會認(rèn)為這個測試沒有通過。這為驗(yàn)證被測試方法在錯誤的情況下是否會拋出預(yù)定的異常提供了便利。舉例來說,方法 supportDBChecker 用于檢查用戶使用的數(shù)據(jù)庫版本是否在系統(tǒng)的支持的范圍之內(nèi),如果用戶使用了不被支持的數(shù)據(jù)庫版本,則會拋出運(yùn)行時(shí)異常 UnsupportedDBVersionException。測試方法 supportDBChecker 在數(shù)據(jù)庫版本不支持時(shí)是否會拋出指定異常的單元測試方法大體如下:
@Test(expected=UnsupportedDBVersionException.class)
public void unsupportedDBCheck(){
……
}
|
注解 org.junit.Test 的另一個參數(shù) timeout,指定被測試方法被允許運(yùn)行的最長時(shí)間應(yīng)該是多少,如果測試方法運(yùn)行時(shí)間超過了指定的毫秒數(shù),則JUnit認(rèn)為測試失敗。這個參數(shù)對于性能測試有一定的幫助。例如,如果解析一份自定義的 XML 文檔花費(fèi)了多于 1 秒的時(shí)間,就需要重新考慮 XML 結(jié)構(gòu)的設(shè)計(jì),那單元測試方法可以這樣來寫:
@Test(timeout=1000)
public void selfXMLReader(){
……
}
|
忽略測試方法
JUnit 提供注解 org.junit.Ignore 用于暫時(shí)忽略某個測試方法,因?yàn)橛袝r(shí)候由于測試環(huán)境受限,并不能保證每一個測試方法都能正確運(yùn)行。例如下面的代碼便表示由于沒有了數(shù)據(jù)庫鏈接,提示 JUnit 忽略測試方法 unsupportedDBCheck:
@ Ignore(“db is down”)
@Test(expected=UnsupportedDBVersionException.class)
public void unsupportedDBCheck(){
……
}
|
但是一定要小心。注解 org.junit.Ignore 只能用于暫時(shí)的忽略測試,如果需要永遠(yuǎn)忽略這些測試,一定要確認(rèn)被測試代碼不再需要這些測試方法,以免忽略必要的測試點(diǎn)。
測試運(yùn)行器
又一個新概念出現(xiàn)了——測試運(yùn)行器,JUnit 中所有的測試方法都是由它負(fù)責(zé)執(zhí)行的。JUnit 為單元測試提供了默認(rèn)的測試運(yùn)行器,但 JUnit 并沒有限制您必須使用默認(rèn)的運(yùn)行器。相反,您不僅可以定制自己的運(yùn)行器(所有的運(yùn)行器都繼承自 org.junit.runner.Runner),而且還可以為每一個測試類指定使用某個具體的運(yùn)行器。指定方法也很簡單,使用注解 org.junit.runner.RunWith 在測試類上顯式的聲明要使用的運(yùn)行器即可:
@RunWith(CustomTestRunner.class)
public class TestWordDealUtil {
……
}
|
顯而易見,如果測試類沒有顯式的聲明使用哪一個測試運(yùn)行器,JUnit 會啟動默認(rèn)的測試運(yùn)行器執(zhí)行測試類(比如上面提及的單元測試代碼)。一般情況下,默認(rèn)測試運(yùn)行器可以應(yīng)對絕大多數(shù)的單元測試要求;當(dāng)使用 JUnit 提供的一些高級特性(例如即將介紹的兩個特性)或者針對特殊需求定制 JUnit 測試方式時(shí),顯式的聲明測試運(yùn)行器就必不可少了。
測試套件
在實(shí)際項(xiàng)目中,隨著項(xiàng)目進(jìn)度的開展,單元測試類會越來越多,可是直到現(xiàn)在我們還只會一個一個的單獨(dú)運(yùn)行測試類,這在實(shí)際項(xiàng)目實(shí)踐中肯定是不可行的。為了解決這個問題,JUnit 提供了一種批量運(yùn)行測試類的方法,叫做測試套件。這樣,每次需要驗(yàn)證系統(tǒng)功能正確性時(shí),只執(zhí)行一個或幾個測試套件便可以了。測試套件的寫法非常簡單,您只需要遵循以下規(guī)則:
- 創(chuàng)建一個空類作為測試套件的入口。
- 使用注解 org.junit.runner.RunWith 和 org.junit.runners.Suite.SuiteClasses 修飾這個空類。
- 將 org.junit.runners.Suite 作為參數(shù)傳入注解 RunWith,以提示 JUnit 為此類使用套件運(yùn)行器執(zhí)行。
- 將需要放入此測試套件的測試類組成數(shù)組作為注解 SuiteClasses 的參數(shù)。
- 保證這個空類使用 public 修飾,而且存在公開的不帶有任何參數(shù)的構(gòu)造函數(shù)。
package com.ai92.cooljunit;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
……
/**
* 批量測試 工具包 中測試類
* @author Ai92
*/
@RunWith(Suite.class)
@Suite.SuiteClasses({TestWordDealUtil.class})
public class RunAllUtilTestsSuite {
}
|
上例代碼中,我們將前文提到的測試類 TestWordDealUtil 放入了測試套件 RunAllUtilTestsSuite 中,在 Eclipse 中運(yùn)行測試套件,可以看到測試類 TestWordDealUtil 被調(diào)用執(zhí)行了。測試套件中不僅可以包含基本的測試類,而且可以包含其它的測試套件,這樣可以很方便的分層管理不同模塊的單元測試代碼。但是,您一定要保證測試套件之間沒有循環(huán)包含關(guān)系,否則無盡的循環(huán)就會出現(xiàn)在您的面前……。
參數(shù)化測試
回顧一下我們在小節(jié)“JUnit 初體驗(yàn)”中舉的實(shí)例。為了保證單元測試的嚴(yán)謹(jǐn)性,我們模擬了不同類型的字符串來測試方法的處理能力,為此我們編寫大量的單元測試方法??墒沁@些測試方法都是大同小異:代碼結(jié)構(gòu)都是相同的,不同的僅僅是測試數(shù)據(jù)和期望值。有沒有更好的方法將測試方法中相同的代碼結(jié)構(gòu)提取出來,提高代碼的重用度,減少復(fù)制粘貼代碼的煩惱?在以前的 JUnit 版本上,并沒有好的解決方法,而現(xiàn)在您可以使用 JUnit 提供的參數(shù)化測試方式應(yīng)對這個問題。
參數(shù)化測試的編寫稍微有點(diǎn)麻煩(當(dāng)然這是相對于 JUnit 中其它特性而言):
- 為準(zhǔn)備使用參數(shù)化測試的測試類指定特殊的運(yùn)行器 org.junit.runners.Parameterized。
- 為測試類聲明幾個變量,分別用于存放期望值和測試所用數(shù)據(jù)。
- 為測試類聲明一個使用注解 org.junit.runners.Parameterized.Parameters 修飾的,返回值為 java.util.Collection 的公共靜態(tài)方法,并在此方法中初始化所有需要測試的參數(shù)對。
- 為測試類聲明一個帶有參數(shù)的公共構(gòu)造函數(shù),并在其中為第二個環(huán)節(jié)中聲明的幾個變量賦值。
- 編寫測試方法,使用定義的變量作為參數(shù)進(jìn)行測試。
我們按照這個標(biāo)準(zhǔn),重新改造一番我們的單元測試代碼:
package com.ai92.cooljunit;
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
import java.util.Collection;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class TestWordDealUtilWithParam {
private String expected;
private String target;
@Parameters
public static Collection words(){
return Arrays.asList(new Object[][]{
{"employee_info", "employeeInfo"}, //測試一般的處理情況
{null, null}, //測試 null 時(shí)的處理情況
|-------10--------20--------30--------40--------50--------60--------70--------80--------9|
|-------- XML error: The previous line is longer than the max of 90 characters ---------|
{"", ""}, //測試空字符串時(shí)的處理情況
|-------10--------20--------30--------40--------50--------60--------70--------80--------9|
|-------- XML error: The previous line is longer than the max of 90 characters ---------|
{"employee_info", "EmployeeInfo"}, //測試當(dāng)首字母大寫時(shí)的情況
{"employee_info_a", "employeeInfoA"}, //測試當(dāng)尾字母為大寫時(shí)的情況
{"employee_a_info", "employeeAInfo"} //測試多個相連字母大寫時(shí)的情況
});
}
/**
* 參數(shù)化測試必須的構(gòu)造函數(shù)
* @param expected 期望的測試結(jié)果,對應(yīng)參數(shù)集中的第一個參數(shù)
* @param target 測試數(shù)據(jù),對應(yīng)參數(shù)集中的第二個參數(shù)
*/
public TestWordDealUtilWithParam(String expected , String target){
this.expected = expected;
this.target = target;
}
/**
* 測試將 Java 對象名稱到數(shù)據(jù)庫名稱的轉(zhuǎn)換
*/
@Test public void wordFormat4DB(){
assertEquals(expected, WordDealUtil.wordFormat4DB(target));
}
}
|
很明顯,代碼瘦身了。在靜態(tài)方法 words 中,我們使用二維數(shù)組來構(gòu)建測試所需要的參數(shù)列表,其中每個數(shù)組中的元素的放置順序并沒有什么要求,只要和構(gòu)造函數(shù)中的順序保持一致就可以了。現(xiàn)在如果再增加一種測試情況,只需要在靜態(tài)方法 words 中添加相應(yīng)的數(shù)組即可,不再需要復(fù)制粘貼出一個新的方法出來了。
JUnit 和 Ant
隨著項(xiàng)目的進(jìn)展,項(xiàng)目的規(guī)模在不斷的膨脹,為了保證項(xiàng)目的質(zhì)量,有計(jì)劃的執(zhí)行全面的單元測試是非常有必要的。但單靠JUnit提供的測試套件很難勝任這項(xiàng)工作,因?yàn)轫?xiàng)目中單元測試類的個數(shù)在不停的增加,測試套件卻無法動態(tài)的識別新加入的單元測試類,需要手動修改測試套件,這是一個很容易遺忘得步驟,稍有疏忽就會影響全面單元測試的覆蓋率。
當(dāng)然解決的方法有多種多樣,其中將 JUnit 與構(gòu)建利器 Ant 結(jié)合使用可以很簡單的解決這個問題。Ant —— 備受贊譽(yù)的 Java 構(gòu)建工具。它憑借出色的易用性、平臺無關(guān)性以及對項(xiàng)目自動測試和自動部署的支持,成為眾多項(xiàng)目構(gòu)建過程中不可或缺的獨(dú)立工具,并已經(jīng)成為事實(shí)上的標(biāo)準(zhǔn)。Ant 內(nèi)置了對 JUnit 的支持,它提供了兩個 Task:junit 和 junitreport,分別用于執(zhí)行 JUnit 單元測試和生成測試結(jié)果報(bào)告。使用這兩個 Task 編寫構(gòu)建腳本,可以很簡單的完成每次全面單元測試的任務(wù)。
不過,在使用 Ant 運(yùn)行 JUnit 之前,您需要稍作一些配置。打開 Eclipse 首選項(xiàng)界面,選擇 Ant -> Runtime 首選項(xiàng)(見圖7),將 JUnit 4.1 的 JAR 文件添加到 Classpath Tab 頁中的 Global Entries 設(shè)置項(xiàng)里。記得檢查一下 Ant Home Entries 設(shè)置項(xiàng)中的 Ant 版本是否在 1.7.0 之上,如果不是請?zhí)鎿Q為最新版本的 Ant JAR 文件。
圖7 Ant Runtime 首選項(xiàng)
剩下的工作就是要編寫 Ant 構(gòu)建腳本 build.xml。雖然這個過程稍嫌繁瑣,但這是一件一勞永逸的事情?,F(xiàn)在我們就把前面編寫的測試用例都放置到 Ant 構(gòu)建腳本中執(zhí)行,為項(xiàng)目 coolJUnit 的構(gòu)建腳本添加一下內(nèi)容:
<?xml version="1.0"?>
<!-- =============================================
auto unittest task
ai92
========================================== -->
<project name="auto unittest task" default="junit and report" basedir=".">
<property name="output folder" value="bin"/>
<property name="src folder" value="src"/>
<property name="test folder" value="testsrc"/>
<property name="report folder" value="report" />
<!-- - - - - - - - - - - - - - - - - -
target: test report folder init
- - - - - - - - - - - - - - - - - -->
<target name="test init">
<mkdir dir="${report folder}"/>
</target>
<!-- - - - - - - - - - - - - - - - - -
target: compile
- - - - - - - - - - - - - - - - - -->
<target name="compile">
<javac srcdir="${src folder}" destdir="${output folder}" />
<echo>compilation complete!</echo>
</target>
<!-- - - - - - - - - - - - - - - - - -
target: compile test cases
- - - - - - - - - - - - - - - - - -->
<target name="test compile" depends="test init">
<javac srcdir="${test folder}" destdir="${output folder}" />
<echo>test compilation complete!</echo>
</target>
<target name="all compile" depends="compile, test compile">
</target>
<!-- ========================================
target: auto test all test case and output report file
===================================== -->
<target name="junit and report" depends="all compile">
<junit printsummary="on" fork="true" showoutput="true">
<classpath>
<fileset dir="lib" includes="**/*.jar"/>
<pathelement path="${output folder}"/>
</classpath>
<formatter type="xml" />
<batchtest todir="${report folder}">
<fileset dir="${output folder}">
<include name="**/Test*.*" />
</fileset>
</batchtest>
</junit>
<junitreport todir="${report folder}">
<fileset dir="${report folder}">
<include name="TEST-*.xml" />
</fileset>
<report format="frames" todir="${report folder}" />
</junitreport>
</target>
</project>
|
Target junit report 是 Ant 構(gòu)建腳本中的核心內(nèi)容,其它 target 都是為它的執(zhí)行提供前期服務(wù)。Task junit 會尋找輸出目錄下所有命名以“Test”開頭的 class 文件,并執(zhí)行它們。緊接著 Task junitreport 會將執(zhí)行結(jié)果生成 HTML 格式的測試報(bào)告(圖8)放置在“report folder”下。
為整個項(xiàng)目的單元測試類確定一種命名風(fēng)格。不僅是出于區(qū)分類別的考慮,這為 Ant 批量執(zhí)行單元測試也非常有幫助,比如前面例子中的測試類都已“Test”打頭,而測試套件則以“Suite”結(jié)尾等等。
圖8 junitreport 生成的測試報(bào)告
現(xiàn)在執(zhí)行一次全面的單元測試變得非常簡單了,只需要運(yùn)行一下 Ant 構(gòu)建腳本,就可以走完所有流程,并能得到一份詳盡的測試報(bào)告。您可以在 Ant 在線手冊 中獲得上面提及的每一個 Ant 內(nèi)置 task 的使用細(xì)節(jié)。
總結(jié)
隨著越來越多的開發(fā)人員開始認(rèn)同并接受極限編程(XP)的思想,單元測試的作用在軟件工程中變得越來越重要。本文旨在將最新的單元測試工具 JUnit 4 介紹給您,以及如何結(jié)合 IDE Eclipse 和構(gòu)建工具 Ant 創(chuàng)建自動化單元測試方案。并且還期望您能夠通過本文“感染”一些好的單元測試意識,因?yàn)?JUnit 本身僅僅是一份工具而已,它的真正優(yōu)勢來自于它的思想和技術(shù)。
下載
描述 |
名字 |
大小 |
下載方法 |
本文示例代碼 |
coolJUnit.zip |
24 KB |
HTTP |
參考資料
學(xué)習(xí)
- JUnit 4 搶先看(Elliotte Rusty Harold, developerWorks, 2005 年 10 月):Elliotte Rusty Harold 為大家揭開了 JUnit 4 新框架的面紗。
- 追求代碼質(zhì)量: JUnit 4 與 TestNG 的對比(Andrew Glover, developerWorks, 2006 年 9 月):Andrew Glover 探討了這兩種框架各自的獨(dú)特之處,并闡述了 TestNG 獨(dú)有的三種高級測試特性。
- TestNG 使 Java 單元測試輕而易舉(Filippo Diotalevi, developerWorks, 2005 年 1 月):TestNG 不僅確實(shí)強(qiáng)大、創(chuàng)新、可擴(kuò)展、靈活,它還展示了 Java Annotations(JDK 5.0 中的重大新特性)的有趣應(yīng)用。
- Tiger 中的注釋,第 1 部分: 向 Java 代碼中添加元數(shù)據(jù)(Brett McLaughlin, developerWorks, 2004 年 9 月):本文解釋了元數(shù)據(jù)如此有用的原因,向您介紹了 Java 語言中的注釋,并研究了 Tiger 的內(nèi)置注釋。
- 利用 Ant 和 JUnit 進(jìn)行增量開發(fā)(Malcolm Davis, developerWorks, 2000 年 11 月):本文通過使用代碼樣本說明了單元測試的種種好處,特別是使用 Ant 和 JUnit 帶來的各種方便。
- 追求代碼質(zhì)量系列(Andrew Glover, developerWorks):在這個系列中,Andrew Glover 將重點(diǎn)闡述有關(guān)保證代碼質(zhì)量的一些有時(shí)看上去有點(diǎn)神秘的東西。
獲得產(chǎn)品和技術(shù)
關(guān)于作者