本文翻譯自IBM DeveloperWorks上的一篇文章,該文講述了測試分類(test categorization)的概念,本身這個概念很簡單,但是卻實際的解決我們常見的問題,在我們的測試龐大到一定地步的時候,測試的運行時間過長,維護成本很高,我們?nèi)绾文軌虮WC持續(xù)集成(CI)的正常運行?那就是通過測試分類。所以我翻譯了這片文章,希望對大家有所幫助。
原文:In pursuit of code quality: Use test categorization for agile builds
原文作者:Andrew Glover is president of Stelligent Incorporated, which helps companies address software quality with effective developer testing strategies and continuous integration techniques that enable teams to monitor code quality early and often. Check out Andy's blog for a list of his publications.
大家都同意開發(fā)人員的測試很重要,但是為什么要花這么長的時間運行測試呢?這個月,Andrew Glover將給我們講述對于系統(tǒng)來說需要保證運行的三類測試,并且告訴你如何根據(jù)分類整理和運行測試。結(jié)果將會奇跡般的減少build的時間,即使是面對當(dāng)今龐大的測試集。
如果不太難過的話,假想一下你是一個2002年初剛剛建立的公司的開發(fā)人員。在淘金熱潮中,你和你的同事已經(jīng)決定使用最流行最強大的Java API來開發(fā)一個龐大的數(shù)據(jù)驅(qū)動的Web應(yīng)用程序。你可你的管理團隊堅定的信仰敏捷過程。從第一天開始,就使用JUnit編寫測試,并且通過Ant build腳本盡可能頻繁的運行它們。最后,你們還會使用cron(*nix下的一個定時運行腳本的任務(wù))來進行nightly build。再然后,某些人可能會下在CruiseControl然后把測試寫成套件,然后在每次check-in時執(zhí)行(持續(xù)集成)。
現(xiàn)在回到今天。
經(jīng)過了前幾年的磨練,你的公司已經(jīng)開發(fā)了數(shù)量巨大的代碼,當(dāng)然也有同樣龐大的JUnit測試。一年前所有的事情都運轉(zhuǎn)良好,當(dāng)你的測試套件有超過2000個測試,人們開始注意到build過程可能要執(zhí)行三個小時以上。幾個月以前,你停止通過代碼提交來處罰持續(xù)集成(CI)運行單元測試,因為CI服務(wù)器會因此過渡繁忙。你改為進行nightly測試(每日測試),第二天早上開發(fā)人員可能會頭疼測試為何失敗。
最近,測試套件似乎很難在晚上運行一次以上了——這是為什么呢?它們永遠(yuǎn)運行不完!沒有人會用幾個小時的時間來等待確認(rèn)代碼運行是正常的(或不正常)。所以,整個的測試會在晚上運行,對么?
因為你如此頻繁的運行測試,他們總是充滿了問題。(譯者注:你會開始懷疑是不是測試也出了問題,是否想測試你的測試?)從而,你和你的團隊開始懷疑單元測試的價值:如果代碼質(zhì)量并不那么重要,為什么我們要承受這種痛苦?假如你可以用敏捷的方法運行它們的話,你們完全同意這是單元測試的基本價值。
嘗試測試分類(test categorization)
你需要的是一個讓你的build轉(zhuǎn)變到更敏捷狀態(tài)的策略。你需要一種解決方案來允許你在一天內(nèi)多次運行測試,讓那些已經(jīng)需要三個小時完成的測試回到原先的狀態(tài)。
在你嘗試使用這個策略讓你的測試套件恢復(fù)原形之前,思考一下“單元測試”的基本概念可能會有所幫助。“我家有一只動物”和“我喜歡汽車”這樣的陳述不是非常明確,所以,不幸的是,“我們編寫單元測試”也不明確。現(xiàn)在,但愿測試泛指一切。
思考前面的兩個關(guān)于動物和汽車的陳述:它們產(chǎn)生了很多疑問。例如,你家里有什么動物?是貓、蜥蜴還是熊?“我家有一只熊”與“我家有一只貓”完全不同。同樣的,“我喜歡汽車”對于與汽車銷售商交談時沒有幫助。你喜歡哪種車:運動車、卡車或者大貨車?不同的答案會將你引入不同的路徑。
同樣,對于開發(fā)人員進行測試,根絕測試類型分類是有所幫助的。這樣做更加精確,能夠允許你的團隊以不同的頻度運行不同類型的測試。分類是避免惱人的運行所有“單元測試”的三小時build的關(guān)鍵方法。
三種分類
形象的將你的測試套件整理為三層,每一層代表開發(fā)人員進行的不同類型的測試,它們是根據(jù)運行時間的長短劃分的。如圖1所示,每一層將花費更多的總build時間,無論是運行時間還是編寫它們所需的時間。
圖1 測試分類的三層

最下面一層測試運行時間最短,如你所想,他們也是最容易寫的。他們也覆蓋最少量的代碼。頂層是有高層次的測試組成,它們檢測應(yīng)用程序的很大一部分。這些測試相對難寫,同時也需要更多時間來執(zhí)行。中間一層測試介于兩個極端之間。
這三個分類如下:
讓我們分別的考察它們。
1、單元測試
單元測試隔離的確認(rèn)一個或者多個對象。單元測試不處理數(shù)據(jù)庫、文件系統(tǒng)或者任何可能帶來測試不能保證長期可運行的因素;順序上,測試可以從(項目)第一天就開始寫。事實上,這就是JUnit的設(shè)計目標(biāo)。單元測試的隔離概念是在很多mock對象庫隔離特定對象的外在依賴的基礎(chǔ)上的。進一步說,單元測試可以在實際代碼編寫前就開始寫——也就是測試先行開發(fā)TDD的概念。
單元測試一般容易編寫,因為他們不依靠于系統(tǒng)依賴,并且他們運行迅速。不好的方面是,單獨的單元測試只能提供有限的代碼覆蓋度。單元測試的價值在于允許開發(fā)者在最低的依賴程度下保證對象的質(zhì)量。
因為單元測試運行迅速容易編寫,一個代碼庫應(yīng)該有很多單元測試且盡量頻繁的運行它們。你應(yīng)該在每次build的時候運行它們,不管是在你的機器或者一個CI環(huán)境(以為這你應(yīng)該在每次向SCM系統(tǒng)chech in之前運行它們)。
2、組件測試
組件測試保證多個對象的交互,但是它們突破了代碼隔離的概念。因為組件測試處理多層架構(gòu),他們經(jīng)常要處理數(shù)據(jù)庫、文件系統(tǒng)、網(wǎng)絡(luò)元素等。而且組件測試一般很難在(項目)前編寫,所以將它們加入到一個實際的測試先行/測試驅(qū)動的場景中是個很大的挑戰(zhàn)。
組件測試編寫要花多一些時間,因為他們比單元測試要棘手。從另一個方面來看,他們能夠提供比單元測試更高的代碼覆蓋率因為它們的寬工作范圍。它們運行耗時更多,所以它們會極大地拖長你們的總測試耗時。
一個宿主框架可能減少測試龐大架構(gòu)組建的挑戰(zhàn)難度。DbUnit就是一個這種框架的完美例子。DbUnit是編寫依賴于數(shù)據(jù)庫的測試容易,它能夠處理復(fù)雜的數(shù)據(jù)庫狀態(tài)準(zhǔn)備工作。
當(dāng)測試引起build時間延長,你基本上可以確定那就是大組的組件測試造成的。因為這些測試比單元測試運行時間更長,你可能發(fā)現(xiàn)你不能總是運行它們。因此,它讓CI環(huán)境至少以小時為間隔執(zhí)行它們。你一應(yīng)該要求每個開發(fā)者在check in前在本機環(huán)境運行這些代碼。
3、系統(tǒng)測試
系統(tǒng)測試從端到端保證軟件應(yīng)用。因此,他們提出了高度的架構(gòu)復(fù)雜性:整個應(yīng)用必須在進行系統(tǒng)測試時運行。如果是一個Web應(yīng)用程序,你需要訪問數(shù)據(jù)庫,從Web服務(wù)器、(應(yīng)用程序)容器、任何相關(guān)的配置都要配合系統(tǒng)測試的運行。系統(tǒng)測試總是在軟件開發(fā)周期的最后階段撰寫的。
系統(tǒng)測試對于編寫人員是個挑戰(zhàn),并且實際往往花費比較長的時間。另一方面,他們提供更好的催款理由,也就是說,他們提供了系統(tǒng)架構(gòu)級的代碼覆蓋率。
系統(tǒng)測試與功能測試非常相近。區(qū)別在于它們不是一個假扮用戶,用戶是虛擬的。就像組件測試一樣,很多框架都是來幫助這類測試的。例如,jWebUnit通過模擬一個瀏覽器提供了測試Web應(yīng)用程序的基礎(chǔ)設(shè)施。
什么是接受測試?
接受測試與功能測試類似,不同點在于,理想情況下,客戶或者最終用戶來編寫接受測試。與功能測試類似,接受測試按照最終用戶的行為測試。一個備受關(guān)注的接受測試框架是Selenium,它使用瀏覽器來測試Web應(yīng)用程序。Selenium可以在build過程中自動運行,就像JUnit測試一樣。但是Selenium是一個新的平臺:他不一定使用JUnit,方式也不太一樣。(Selenium RC就沒有這個問題了)
我應(yīng)該使用jWebUnit或者Selenium?
jWebUnit是一個JUnit擴展框架,設(shè)計用來進行系統(tǒng)測試;所以,它需要你自己寫這些測試。Selenium是一個優(yōu)秀的接受測試和功能測試工具,不同于jWebUnit,它允許非程序員編寫測試。理想狀態(tài)下,你的團隊可以同時使用兩種工具來確認(rèn)應(yīng)用程序的功能。
使用TestNG進行測試分類
使用TestNG實現(xiàn)測試分類非常容易。使用TestNG的group注釋,邏輯上將測試分類就是進行合適的group注釋,這非常簡單。運行某一分類的測試只需要將group名稱傳給test runner就可以了,例如通過Ant。
實現(xiàn)測試分類
所以,你的單元測試套件實際上是單元測試、組件測試和系統(tǒng)測試的套件。甚至,在你檢查所有的測試后發(fā)現(xiàn)build需要這么長時間是因為大部分測試都是組件測試。下一個問題是,如何通過JUnit實現(xiàn)測試分類?
你有很多選擇,但是讓我們先試驗一下最簡單的兩個:
- 根據(jù)需要的分類創(chuàng)建不同的JUnit套件(suite)文件
- 對于不同類型的測試創(chuàng)建不同的目錄
創(chuàng)建不同的套件
你可以使用JUnit的TestSuite類(它也是一種Test)定義一組同類測試的集合。你要創(chuàng)建一個TestSuite的實例并添加相關(guān)的測試類到test方法中。你可以在TestSuite實例中通過定義一個叫做suite()的public static方法告訴JUnit這個套件包括哪些測試。所有包括的測試將會一次全部執(zhí)行。因此你可以通過創(chuàng)建TestSuite來實現(xiàn)測試分類,一個單元測試的TestSuite、一個組件測試的TestSuite,有一個系統(tǒng)測試的TestSuite。
例如清單1的類中的suite()方法創(chuàng)建了一個包含所有組建測試的TestSuite。注意這個類不是非常符合JUnit規(guī)范。他既沒有繼承TestCase,也沒有任何測試的定義。但是JUnit會自動發(fā)現(xiàn)suite()方法并且運行它返回的所有測試類。
清單1 單元測試的TestSuite
package?test.org.acme.widget;
import?junit.framework.Test;
import?junit.framework.TestSuite;
import?test.org.acme.widget.*;
public?class?ComponentTestSuite?{
?public?static?void?main(String[]?args)?{
??junit.textui.TestRunner.run(ComponentTestSuite.suite());
?}
?public?static?Test?suite(){
??TestSuite?suite?=?new?TestSuite();
??suite.addTestSuite(DefaultSpringWidgetDAOImplTest.class);
??suite.addTestSuite(WidgetDAOImplLoadTest.class);
??
??suite.addTestSuite(WidgetReportTest.class);
??return?suite;
?}
}
定義TestSuite的過程需要你察看你當(dāng)前的所有測試并將它們加入到相應(yīng)的類里面(例如,所有的單元測試加入到UnitTestSuite)。這也就意味著你在相應(yīng)的分類里面創(chuàng)建了新的測試,你必須編程式的將它們添加到合適的TestSuite中,當(dāng)然還需要重新編譯它們。
運行單獨的TestSuite需要單獨的Ant任務(wù)來運行正確的測試組。你可以定義一個component-test任務(wù)來執(zhí)行ComponentTtestSuite,就像清單2中的樣子:
清單2 運行組建測試的一個Ant任務(wù)
<
target?
name
="component-test"
?
???????????if
="Junit.present"
?
???????????depends
="junit-present,compile-tests"
>
?
<
mkdir?
dir
="${testreportdir}"
/>
???
?
<
junit?
dir
="./"
?failureproperty
="test.failure"
?
?????????????printSummary
="yes"
?
?????????????fork
="true"
?haltonerror
="true"
>
???
<
sysproperty?
key
="basedir"
?value
="."
/>
?????
???
<
formatter?
type
="xml"
/>
??????
???
<
formatter?
usefile
="false"
?type
="plain"
/>
?????
???
<
classpath
>
????
<
path?
refid
="build.classpath"
/>
???????
????
<
pathelement?
path
="${testclassesdir}"
/>
????????
????
<
pathelement?
path
="${classesdir}"
/>
??????
???
</
classpath
>
???
<
batchtest?
todir
="${testreportdir}"
>
????
<
fileset?
dir
="test"
>
?????
<
include?
name
="**/ComponentTestSuite.java"
/>
?????????????????
????
</
fileset
>
???
</
batchtest
>
?
</
junit
>
</
target
>
理想情況下,你還需要一個觸發(fā)單元測試的任務(wù)和系統(tǒng)測試的任務(wù)。最后,還有希望運行所有測試的情況,你需要創(chuàng)建第四個任務(wù)來運行其它三個任務(wù),就像清單3里面那樣:
清單3 運行所有測試的任務(wù)
<
target?
name
="test-all"
?depends
="unit-test,component-test,system-test"
/>
創(chuàng)建單獨的TestSuite是一個迅速實現(xiàn)測試分類的解決方案。缺點是這個方法需要你創(chuàng)建新的測試,你必須編成式的將它們添加到合適的TestSuite里面,這可能有點痛苦。給每個測試類型創(chuàng)建單獨的目錄可能是一種更加有彈性的方法,它允許你添加新的測試分類但無需重新編譯。
創(chuàng)建單獨的目錄
我發(fā)現(xiàn)最簡單的通過JUnit實現(xiàn)測試分類的方法是邏輯上將不同類型的測試放到不同的目錄中。使用這個方法,所有的單元測試都放在unit目錄,所有的組建測試都放在component目錄,等等。
例如,在test目錄中保存著所有未分類的測試,你可以創(chuàng)建三個新的子目錄,就像清單4中那樣:
清單4 實現(xiàn)測試分類的目錄結(jié)構(gòu)
acme-proj/
???????test/
??????????unit/
??????????component/
??????????system/?
??????????conf/
運行這些測試,你需要定義至少四個Ant任務(wù):一個給單元測試,另外的給組建測試,還有系統(tǒng)測試。第四個任務(wù)是一個方便運行其它三個測試類型的任務(wù)(就像清單3種展示的那種方式)。
JUnit任務(wù)就像清單2中的形式。區(qū)別在哪里呢,只是在任務(wù)的batchtest這個地方。這次,fileset指向的是一個指定的目錄,就像清單5種的樣子,他指向了unit目錄:
清單5 JUnit任務(wù)中的batchtest方面,用來運行所有單元測試
<
batchtest?
todir
="${testreportdir}"
>
?
<
fileset?
dir
="test/unit"
>
?
??
<
include?
name
="**/**Test.java"
/>
???????
?
</
fileset
>
</
batchtest
>
注意這個任務(wù)運行test/unit目錄下的所有測試,當(dāng)創(chuàng)建了新的單元測試(或者其它分類的其它測試),你只需要把它們放到這個目錄里面就可以了!這比添加一行到TestSuite中并重新編譯它要方便多了。
問題解決了!
回到最初的場景,我認(rèn)為你和你的團隊會決定使用單獨的目錄這種彈性的解決方案來解決你們的build時間過長的問題。這個任務(wù)最難的一個方面是檢查和分清測試類型。你重構(gòu)你的Ant build文件創(chuàng)建四個新的任務(wù)(三個單獨的測試分類還有一個運行它們?nèi)齻€)。甚至,你修改CuiresControl只在check-in的時候運行單元測試,而組建測試按小時運行。更進一步的檢查后,系統(tǒng)測試也可以幾個小時運行一次,也許你會創(chuàng)建一個新的任務(wù)來同時運行組建測試和系統(tǒng)測試。
最后的結(jié)果是每天測試運行很多次,你的團隊可以快速的發(fā)現(xiàn)集成錯誤——一般在幾個小時內(nèi)。
創(chuàng)建敏捷構(gòu)建不是為了趕時髦,它實際上是保證代碼質(zhì)量的重要因素。測試運行的更加頻繁,開發(fā)人員的測試的價值就能直接轉(zhuǎn)化為錢。并且,希望你們的公司能夠在2006取得廣泛的成功!
資源
Learn
-
"
Automate acceptance tests with Selenium
" (Christian Hellsten, developerWorks, December 2005): Architects, developers, and testers learn how to use the Selenium testing tools to automate acceptance tests.
-
"
Effective Unit Testing with DbUnit
" (Andrew Glover, OnJava, January 2004): Introduces database-dependent testing with DbUnit.
-
"
An early look at JUnit 4
" (Elliotte Harold, developerWorks, September 2005): Obsessive code tester Elliotte Harold takes JUnit 4 out for a spin.
-
"
Repeatable system tests
" (Andrew Glover, developerWorks, September 2006): Andrew Glover introduces Cargo, an open source framework that automates container management in a generic fashion.
-
"
Create test cases for Web applications
" (Amit Tuli, developerWorks, May 2005): Software engineer Amit Tuli introduces jWebUnit.
-
"
In pursuit of code quality: JUnit 4 vs. TestNG
" (Andrew Glover, developerWorks, April 2006): Has JUnit 4 rendered TestNG obsolete? Find out why not.
-
In pursuit of code quality series
(Andrew Glover, developerWorks): Learn more about code metrics, test frameworks, and writing quality-focused code.
-
developerWorks
: Hundreds of articles about every aspect of Java programming.
Get products and technologies
Discuss