(轉(zhuǎn)貼)用 Jester 對(duì)測(cè)試進(jìn)行測(cè)試
Posted on 2006-07-19 15:33 eddy liao 閱讀(388) 評(píng)論(0) 編輯 收藏 所屬分類: 軟件質(zhì)量
用 Jester 對(duì)測(cè)試進(jìn)行測(cè)試測(cè)試套件有缺陷,這不是玩笑 ![]() |
![]() |
![]() |
|
級(jí)別: 初級(jí)
Elliotte Rusty Harold
, 副教授, Polytechnic University
2005 年 6 月 02 日
全面的單元測(cè)試套件對(duì)健壯的程序是必不可少的。但是如何才能保證測(cè)試套件測(cè)試了應(yīng)當(dāng)測(cè)試的每件事呢?Ivan Moore 的 JUnit 測(cè)試的測(cè)試器 Jester,擅長(zhǎng)發(fā)現(xiàn)測(cè)試套件的問(wèn)題,并提供對(duì)代碼基本結(jié)構(gòu)的深入觀察。Elliotte Rusty Harold 介紹了 Jester 并展示如何使用它才能得到最佳結(jié)果。
測(cè)試先行的開發(fā)是極限編程(XP)中爭(zhēng)議最少、采用最廣泛的部分。到目前為止,大多數(shù)專業(yè) Java 程序員都可能捕捉過(guò)測(cè)試 bug。(請(qǐng)參閱 參考資料 獲得有關(guān)“被測(cè)試傳染”的更多信息。) JUnit 是 Java 社區(qū)事實(shí)上的標(biāo)準(zhǔn)測(cè)試框架,沒(méi)有經(jīng)過(guò)全面的 JUnit 測(cè)試套件測(cè)試過(guò)的系統(tǒng)是不完整的。如果您的項(xiàng)目有全面的測(cè)試套件,那么恭喜您:您將制作出質(zhì)量良好的、有利于工作的軟件。但是大多數(shù)代碼基礎(chǔ)相當(dāng)復(fù)雜。您能確定每個(gè)方法都被測(cè)試到、每個(gè)分支都進(jìn)入過(guò)么?如果不能,那么當(dāng)這些方法和分支在生產(chǎn)中執(zhí)行的時(shí)候,應(yīng)用程序會(huì)如何表現(xiàn)呢?
對(duì)代碼進(jìn)行測(cè)試的下一步是用 代碼覆蓋 工具對(duì)測(cè)試進(jìn)行度量。代碼覆蓋是一種查看一套測(cè)試覆蓋了多少代碼的方法。信心的獲得,不僅需要知道測(cè)試了程序整體,還要知道每個(gè)方法在全部可能情況下都得到測(cè)試。傳統(tǒng)情況下,這類度量的執(zhí)行方法是在測(cè)試執(zhí)行時(shí)對(duì)測(cè)試進(jìn)行監(jiān)視,可以通過(guò) Java 虛擬機(jī)調(diào)試接口(JVMDI)或 Java 虛擬機(jī)工具接口 (JVMTI)進(jìn)行,或者直接處理字節(jié)碼。一次都沒(méi)有執(zhí)行過(guò)的語(yǔ)句是測(cè)試不到的。
Clover 和 EMMA(參閱 參考資料) 這類工具采用的這種方法對(duì)于發(fā)現(xiàn)測(cè)試不到的語(yǔ)句很有價(jià)值 —— 但是還不夠。知道測(cè)試套件沒(méi)有執(zhí)行某個(gè)語(yǔ)句,可以證明該語(yǔ)句沒(méi)測(cè)試到。但是,反過(guò)來(lái)不成立。如果執(zhí)行了某一行代碼,并不一定代表它得到測(cè)試。完全有可能存在這樣的情況:測(cè)試并沒(méi)有檢查代碼行是否生成正確結(jié)果。
當(dāng)然,沒(méi)有人會(huì)編寫測(cè)試套件對(duì)每個(gè)語(yǔ)句的結(jié)果都進(jìn)行驗(yàn)證。在眾多的問(wèn)題當(dāng)中,這個(gè)問(wèn)題可能會(huì)破壞封裝。您可能認(rèn)為,針對(duì)特定輸入,只有方法中的每一行都操作正確,方法才會(huì)生成預(yù)期結(jié)果。但是這個(gè)假設(shè)并不合理。例如,如果沒(méi)有測(cè)試到所有可能輸入,也就沒(méi)有測(cè)試到為處理邊際情況而設(shè)計(jì)的代碼,這時(shí)會(huì)如何呢?有可能還會(huì)測(cè)試到每行代碼,但有可能遺漏真正的 bug。
![]() |
|
![]() ![]() |
![]()
|
這正是 Jester 發(fā)揮作用的地方。與 Clover 這類傳統(tǒng)的代碼覆蓋工具不同,Jester 不去查看報(bào)告了哪行代碼。相反,Jester 會(huì)修改源代碼、重新編譯源代碼,然后運(yùn)行測(cè)試套件,查看是否有什么事出錯(cuò)。例如,它會(huì)把 1 改成 2,或者把 if (x > y)
改成 if (false)
。如果測(cè)試套件的關(guān)注不夠,沒(méi)有注意到修改,那么就說(shuō)明遺漏了某項(xiàng)測(cè)試。
我將通過(guò)在開源的 Jaxen XPath 工具(參閱 參考資料)上應(yīng)用 Jester 而對(duì)它進(jìn)行演示。Jaxen 有一個(gè)基于 JUnit 的測(cè)試套件,而且這個(gè)套件的代碼覆蓋并不完善。
在運(yùn)行 Jester 之前,所有對(duì)沒(méi)有修改的源代碼的單元測(cè)試都必須測(cè)試通過(guò)。如果不是這樣,那么 Jester 就無(wú)法知道是不是它的修改造成了破壞。(為了演示,我不得不修復(fù)一個(gè) bug,我過(guò)去為它編寫了測(cè)試用例,但是沒(méi)有跟蹤修復(fù)它。)
Jester 與 IDE 的集成不是特別好(或者根本不好),所以要讓測(cè)試通過(guò),重要的是正確設(shè)置 CLASSPATH
和目錄。運(yùn)行測(cè)試套件所需要的命令行對(duì)于每個(gè)項(xiàng)目都是不同的。因?yàn)?Jaxen 測(cè)試使用指向特定測(cè)試文件的相對(duì) URL ,所以它的測(cè)試必須在 jaxen 目錄中運(yùn)行。下面是我最后運(yùn)行 Jaxen 測(cè)試的方式:
|
在運(yùn)行 Jester 之前,還需要清楚針對(duì)測(cè)試套件的一項(xiàng)附加限制。除非測(cè)試失敗,否則不能打印有關(guān) System.err
的任何內(nèi)容。Jester 要通過(guò)檢查打印的內(nèi)容來(lái)判斷測(cè)試是否成功,所以對(duì) System.err
的程序輸出會(huì)把 Jester 弄混。
測(cè)試套件運(yùn)行無(wú)誤之后,請(qǐng)做一份源代碼樹的拷貝。記住,Jester 要向代碼故意加入 bug,所以您可不要冒險(xiǎn)在出現(xiàn)問(wèn)題的情況下遺漏一個(gè) bug。(如果您在使用源代碼控制,那么這不會(huì)是個(gè)大問(wèn)題。如果沒(méi)有,請(qǐng)暫停閱讀本文,立即把代碼簽入 CVS 或 Subversion 倉(cāng)庫(kù)。)
要運(yùn)行 Jester,在路徑中必須同時(shí)擁有 jester.jar 和 junit.jar(JUnit 沒(méi)有和 Jester 綁在一起。需要分別下載)。Jester 在類路徑中查找它的配置文件,所以必須還要把 Jester 的主目錄放在類路徑中。當(dāng)然,還需要添加所測(cè)試的應(yīng)用程序需要的其他 JAR。主類是 jester.TestTester
。傳遞給這個(gè)程序的參數(shù)是測(cè)試應(yīng)用程序的測(cè)試套件名稱。(我不得不為 Jaxen 編寫一個(gè)主類,因?yàn)樗鼪](méi)有包含一個(gè)可以運(yùn)行它的全部測(cè)試的類。)如果把全部必要的 JAR 文件和目錄都添加到 CLASSPATH
環(huán)境變量,而不是把它們添加到 jre/lib/ext 或者用 -classpath
引用它們,那么 Jester 工作起來(lái)會(huì)更加穩(wěn)定。下面是我針對(duì) Jaxen 運(yùn)行初始測(cè)試的方式:
|
Jester 運(yùn)行很慢,即使檢測(cè)一個(gè)文件也是如此。它顯示一個(gè)進(jìn)度對(duì)話框,如圖 1 所示,并在 System.out
上打印輸出,讓您知道它在做的工作,并向您保證它并沒(méi)有完全掛起。
圖 1. Jester 進(jìn)度

如果在第一次運(yùn)行若干分鐘(或者時(shí)間足夠運(yùn)行完整的測(cè)試套件,甚至更長(zhǎng))之后,什么輸出也沒(méi)有看到,那么 Jester 可能 確實(shí) 掛起了,這很可能是因?yàn)轭惵窂降膯?wèn)題。如果每件事都進(jìn)行順利,那么應(yīng)當(dāng)看到像清單 1 所示的輸出:
清單 1. Jester 輸出
|
從清單 1 中可以看到,BaseXPath
沒(méi)有得到很好的測(cè)試。Jester 對(duì)類進(jìn)行了 11 項(xiàng)修改,而只有一項(xiàng)造成測(cè)試失敗。有些修改是假陽(yáng)性,但是 11 處修改肯定不應(yīng)當(dāng)只報(bào)告 1 處。
下一步是在不破壞測(cè)試套件的情況下查看 Jester 改變的代碼,看看是否需要為它編寫測(cè)試。Jester 在 GUI 中顯示它進(jìn)行的修改,如 圖 1 所示(它不能在無(wú)人控制的情況下運(yùn)行,這有點(diǎn)煩人),在控制臺(tái)上打印輸出,如 清單 1 所示,并生成 XML 文件,文件中是沒(méi)有產(chǎn)生影響的修改列表,如清單 2 所示:
清單 2. Jester XML 輸出
|
Jester 的行號(hào)報(bào)告通常不是個(gè)好方法,所以最好是在控制臺(tái)輸出中查找修改的代碼。下面是 清單 1 的報(bào)告中的修改:
|
在這個(gè)方法中,這個(gè)修改是在類的結(jié)束處:
|
對(duì)測(cè)試套件迅速查找之后發(fā)現(xiàn),實(shí)際上沒(méi)有測(cè)試調(diào)用 selectSingleNodeForContext
。所以下一步就是為這個(gè)方法編寫一個(gè)測(cè)試。這個(gè)方法是 protected 的方法,所以測(cè)試不能直接調(diào)用它。有時(shí)需要編寫一個(gè)子類(通常作為內(nèi)部類)來(lái)測(cè)試 protected 的方法。但是在這個(gè)例子中,稍做一點(diǎn)檢查就很快發(fā)現(xiàn)這個(gè)方法由同一個(gè)類中的兩個(gè) public 方法(stringValue
和 numberValue
)直接調(diào)用。所以也可以用這兩個(gè)方法來(lái)測(cè)試它:
|
最后一步是運(yùn)行測(cè)試用例,確定它通過(guò)。下面是結(jié)果:
|
Jester 捕捉到一個(gè) bug!方法沒(méi)有像預(yù)期的那樣工作。更有趣的是,對(duì) bug 的調(diào)查揭示出潛在的設(shè)計(jì)缺陷。BaseXPath
類可能更適合作為抽象類而不是具體類。我發(fā)誓,我并不是特意挑選這個(gè)示例來(lái)公開這個(gè) bug。我從 BaseXPath
開始只是因?yàn)樗琼敿?jí) org.jaxen 包的第一個(gè)類,而且我選擇 selectSingleNodeForContext
作為所測(cè)試的方法也只是因?yàn)樗?Jester 報(bào)告的最后一個(gè)錯(cuò)誤。我真的認(rèn)為這個(gè)方法沒(méi)有什么問(wèn)題,但是我錯(cuò)了。如果某些事沒(méi)有經(jīng)過(guò)測(cè)試,那么就應(yīng)當(dāng)假設(shè)它是有問(wèn)題的。Jester 會(huì)告訴您出了什么問(wèn)題。
下一步顯而易見(jiàn):修復(fù) bug。(請(qǐng)確保同時(shí)對(duì) Jester 正在處理的源樹拷貝和實(shí)際樹中的 bug 進(jìn)行了修復(fù)。)然后,迭代 —— 針對(duì)這個(gè)類重新運(yùn)行 Jester,直到任何修改都不能通過(guò),或者可以通過(guò)的修改都是不相關(guān)的。在我為這個(gè) bug 添加測(cè)試(并修復(fù))之后,Jester 就報(bào)告 11 個(gè)修改中只有 8 個(gè)沒(méi)有檢測(cè)到,如 清單 2 所示。這在調(diào)試中是經(jīng)常出現(xiàn)的事:修復(fù)了一個(gè)問(wèn)題就修復(fù)(或者暴露了)另外幾個(gè)。
![]() ![]() |
![]()
|
因?yàn)?Jester 重新編譯代碼基,而且要為自己做的每個(gè)修改都重新運(yùn)行測(cè)試套件,所以它的運(yùn)行要比 Clover 這樣的傳統(tǒng)工具慢得多。因此,對(duì)性能加以關(guān)注是很重要的。可以用許多技術(shù)加快 Jester 的運(yùn)行。
首先,如果編譯在 Jester 執(zhí)行時(shí)間中占了顯著部分,那么請(qǐng)嘗試使用一個(gè)更快的編譯器。許多用戶都報(bào)告采用 Jikes 代替 Javac 后速度有顯著提高(參閱 參考資料)。可以在 Jester 主目錄中的 jester.cfg 文件中修改 Jester 使用的編譯命令。
第二,剖析和優(yōu)化測(cè)試套件。一般情況下,人們對(duì)單元測(cè)試運(yùn)行的速度沒(méi)太注意,但是如果乘上 Jester 上千次執(zhí)行測(cè)試套件的次數(shù),那么任何節(jié)約都會(huì)非常顯著。具體來(lái)說(shuō),要在測(cè)試套件中查找在正常代碼中不會(huì)出現(xiàn)的問(wèn)題。JUnit 會(huì)重新初始化每個(gè)執(zhí)行方法的全部字段,所以如果不是測(cè)試類的每個(gè)方法都用的字段,那么把測(cè)試數(shù)據(jù)從字段中拿出來(lái)放在本地變量中,可以顯著提高速度。如果形成的代碼副本不合您的風(fēng)格,請(qǐng)嘗試把測(cè)試套件分成更小、更模塊化的類,以便所有的初始數(shù)據(jù)可以在全部測(cè)試方法之間共享。
第三,重新組織測(cè)試套件的 suite
方法,以便最脆弱的測(cè)試(修改之后最有可能出錯(cuò)的)在不太脆弱的測(cè)試之前運(yùn)行。只要 Jester 發(fā)現(xiàn)一個(gè)測(cè)試失敗,就會(huì)終止運(yùn)行,所以盡早失敗可以短路大量耗時(shí)的額外測(cè)試。
第四,出于相似的原因,當(dāng)測(cè)試失敗的機(jī)會(huì)差不多時(shí),把最快的測(cè)試放在第一位。按照大概的執(zhí)行時(shí)間給測(cè)試排序。只在內(nèi)存中執(zhí)行的測(cè)試在訪問(wèn)磁盤的測(cè)試之前,訪問(wèn)磁盤的測(cè)試在訪問(wèn) LAN 的測(cè)試之前,訪問(wèn) LAN 的測(cè)試在訪問(wèn) Internet 的測(cè)試之前。如果有些測(cè)試特別慢,試試去掉它們,即便這會(huì)增加假陽(yáng)性的數(shù)量。在 XOM (一個(gè)用 Java 語(yǔ)言處理 XML 的 API)的測(cè)試套件中,在 50 個(gè)測(cè)試類中,只有很少的幾個(gè)就占據(jù)了 90% 以上的執(zhí)行時(shí)間。在測(cè)試的時(shí)候清除這些類可以帶來(lái) 10 倍的性能提升。
最后,也是最重要的,就是不要一次測(cè)試整個(gè)代碼基。每次把測(cè)試限制在一個(gè)類上,而且只運(yùn)行能夠暴露這個(gè)類的覆蓋不足的測(cè)試。可能需要更長(zhǎng)時(shí)間來(lái)測(cè)試每個(gè)類,但是用這種方法,幾乎可以立即填補(bǔ)不足、修復(fù) bug,而不必為 Jester 的一次運(yùn)行完成等上好幾天。
![]() ![]() |
![]()
|
Jester 是聰明的程序員的工具包中一個(gè)重要的附加。它可以發(fā)現(xiàn)其他工具不能發(fā)現(xiàn)的代碼覆蓋不足,這會(huì)直接變成發(fā)現(xiàn)和修復(fù) bug。使用 Jester 對(duì)代碼基進(jìn)行測(cè)試,可以制造出更強(qiáng)壯的軟件。
![]() ![]() |
![]()
|
- 您可以參閱本文在 developerWorks 全球站點(diǎn)上的 英文原文。
- 從 SourceForge 下載 Jester。
- 得到 test infected。
-
JUnit
是 Java 代碼事實(shí)上的標(biāo)準(zhǔn)單元測(cè)試框架,也是 Jester 依賴的框架。
- 在本文中作為示例的 Jaxen 項(xiàng)目是一個(gè)用于 Java 語(yǔ)言的開源 XPath 引擎,適用于許多不同的對(duì)象模型。
-
Clover
是一個(gè)更傳統(tǒng)的測(cè)試覆蓋工具,是 Jester 的有益補(bǔ)充。它比 Jester 更容易使用,也快得多;但是它只能測(cè)試在測(cè)試期間執(zhí)行的代碼,而不是它真正要測(cè)試的代碼。
-
EMMA
是一個(gè)免費(fèi)、開源的代碼覆蓋工具。請(qǐng)學(xué)習(xí)面向 Java 程序員的各種不同的 開源單元測(cè)試工具 和 開源代碼覆蓋工具 的更多內(nèi)容。
- 請(qǐng)閱讀 Dave Thomas 和 Andy Hunt 的 Pragmatic Unit Testing in Java With JUnit(Pragmatic Bookshelf, 2003)。
- Dennis M. Sosnoski 通過(guò)介紹開源的 Hansel 和 Gretel 代碼覆蓋工具,開始 了 Classworking 工具箱 系列。
- David Carew 和 Sandeep Desai 撰寫的“Keeping critters out of your code: How to use WebSphere and JUnit to prevent programming bugs”(developerWorks, 2003 年 6 月),介紹了采用 XP 方法進(jìn)行測(cè)試。
- Malcolm Davis 撰寫的“利用 Ant 和 JUnit 進(jìn)行增量開發(fā)”(developerWorks,2000 年 11 月),解釋了如何把 JUnit 集成到自己的項(xiàng)目中。
- Eric Allen 和 Roy Miller 在他們各自的專欄 診斷 Java 代碼 和 揭開極端編程的神秘面紗 中頻繁地涉及到單元測(cè)試。
- Erik Hatcher 撰寫的“讓編譯和測(cè)試過(guò)程自動(dòng)化”(developerWorks,2001 年 8 月),介紹了如何把增量測(cè)試和持續(xù)構(gòu)建組合到一個(gè)自動(dòng)的過(guò)程中。
-
Testdriven.com
是一個(gè)關(guān)于測(cè)試驅(qū)動(dòng)開發(fā)的文章和資源的全面集合。
-
Jikes
是一個(gè)用 C 編寫的開源的 Java 編譯器,運(yùn)行起來(lái)比 javac 快得多。
- 在 developerWorks Java 技術(shù)專區(qū) 可以找到 Java 編程各方面的文章。
- 請(qǐng)?jiān)L問(wèn) Developer Bookstore,獲得技術(shù)書籍的完整列表,其中包括數(shù)百本 Java 相關(guān)主題 的圖書。
- 通過(guò)參與 developerWorks blogs 加入 developerWorks 社區(qū)。
![]() ![]() |
![]()
|
![]() |
||
|
![]() |
Elliotte Rusty Harold 來(lái)自新奧爾良,現(xiàn)在他還定期返回新奧爾良研究一盆秋葵。但是,他和妻子 Beth 及他們的貓咪 Charm(以夸克命名)和 Marjorie(以他的岳母為名),定居在布魯克林附近的 Prospect Heights。他是 Polytechnic 大學(xué)計(jì)算機(jī)科學(xué)的副教授,他在該校講授 Java 和面向?qū)ο缶幊獭K?Web 站點(diǎn) Cafe au Lait 已經(jīng)成為 Internet 上最流行的獨(dú)立 Java 站點(diǎn)之一,他的分站點(diǎn) Cafe con Leche 已經(jīng)成為最流行的 XML 站點(diǎn)之一。他的書包括 Effective XML、Processing XML with Java、Java Network Programming和 The XML 1.1 Bible。他目前在開發(fā)處理 XML 的 XOM API 和 XQuisitor GUI 查詢工具。 |