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