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

最下面一層測(cè)試運(yùn)行時(shí)間最短,如你所想,他們也是最容易寫的。他們也覆蓋最少量的代碼。頂層是有高層次的測(cè)試組成,它們檢測(cè)應(yīng)用程序的很大一部分。這些測(cè)試相對(duì)難寫,同時(shí)也需要更多時(shí)間來(lái)執(zhí)行。中間一層測(cè)試介于兩個(gè)極端之間。
這三個(gè)分類如下:
-
單元測(cè)試
-
組件測(cè)試
-
系統(tǒng)測(cè)試
讓我們分別的考察它們。
1、單元測(cè)試
單元測(cè)試隔離的確認(rèn)一個(gè)或者多個(gè)對(duì)象。單元測(cè)試不處理數(shù)據(jù)庫(kù)、文件系統(tǒng)或者任何可能帶來(lái)測(cè)試不能保證長(zhǎng)期可運(yùn)行的因素;順序上,測(cè)試可以從(項(xiàng)目)第一天就開(kāi)始寫。事實(shí)上,這就是JUnit的設(shè)計(jì)目標(biāo)。單元測(cè)試的隔離概念是在很多mock對(duì)象庫(kù)隔離特定對(duì)象的外在依賴的基礎(chǔ)上的。進(jìn)一步說(shuō),單元測(cè)試可以在實(shí)際代碼編寫前就開(kāi)始寫——也就是測(cè)試先行開(kāi)發(fā)TDD的概念。
單元測(cè)試一般容易編寫,因?yàn)樗麄儾灰揽坑谙到y(tǒng)依賴,并且他們運(yùn)行迅速。不好的方面是,單獨(dú)的單元測(cè)試只能提供有限的代碼覆蓋度。單元測(cè)試的價(jià)值在于允許開(kāi)發(fā)者在最低的依賴程度下保證對(duì)象的質(zhì)量。
因?yàn)閱卧獪y(cè)試運(yùn)行迅速容易編寫,一個(gè)代碼庫(kù)應(yīng)該有很多單元測(cè)試且盡量頻繁的運(yùn)行它們。你應(yīng)該在每次build的時(shí)候運(yùn)行它們,不管是在你的機(jī)器或者一個(gè)CI環(huán)境(以為這你應(yīng)該在每次向SCM系統(tǒng)chech in之前運(yùn)行它們)。
2、組件測(cè)試
組件測(cè)試保證多個(gè)對(duì)象的交互,但是它們突破了代碼隔離的概念。因?yàn)榻M件測(cè)試處理多層架構(gòu),他們經(jīng)常要處理數(shù)據(jù)庫(kù)、文件系統(tǒng)、網(wǎng)絡(luò)元素等。而且組件測(cè)試一般很難在(項(xiàng)目)前編寫,所以將它們加入到一個(gè)實(shí)際的測(cè)試先行/測(cè)試驅(qū)動(dòng)的場(chǎng)景中是個(gè)很大的挑戰(zhàn)。
組件測(cè)試編寫要花多一些時(shí)間,因?yàn)樗麄儽葐卧獪y(cè)試要棘手。從另一個(gè)方面來(lái)看,他們能夠提供比單元測(cè)試更高的代碼覆蓋率因?yàn)樗鼈兊膶捁ぷ鞣秶K鼈冞\(yùn)行耗時(shí)更多,所以它們會(huì)極大地拖長(zhǎng)你們的總測(cè)試耗時(shí)。
一個(gè)宿主框架可能減少測(cè)試龐大架構(gòu)組建的挑戰(zhàn)難度。DbUnit就是一個(gè)這種框架的完美例子。DbUnit是編寫依賴于數(shù)據(jù)庫(kù)的測(cè)試容易,它能夠處理復(fù)雜的數(shù)據(jù)庫(kù)狀態(tài)準(zhǔn)備工作。
當(dāng)測(cè)試引起build時(shí)間延長(zhǎng),你基本上可以確定那就是大組的組件測(cè)試造成的。因?yàn)檫@些測(cè)試比單元測(cè)試運(yùn)行時(shí)間更長(zhǎng),你可能發(fā)現(xiàn)你不能總是運(yùn)行它們。因此,它讓CI環(huán)境至少以小時(shí)為間隔執(zhí)行它們。你一應(yīng)該要求每個(gè)開(kāi)發(fā)者在check in前在本機(jī)環(huán)境運(yùn)行這些代碼。
3、系統(tǒng)測(cè)試
系統(tǒng)測(cè)試從端到端保證軟件應(yīng)用。因此,他們提出了高度的架構(gòu)復(fù)雜性:整個(gè)應(yīng)用必須在進(jìn)行系統(tǒng)測(cè)試時(shí)運(yùn)行。如果是一個(gè)Web應(yīng)用程序,你需要訪問(wèn)數(shù)據(jù)庫(kù),從Web服務(wù)器、(應(yīng)用程序)容器、任何相關(guān)的配置都要配合系統(tǒng)測(cè)試的運(yùn)行。系統(tǒng)測(cè)試總是在軟件開(kāi)發(fā)周期的最后階段撰寫的。
系統(tǒng)測(cè)試對(duì)于編寫人員是個(gè)挑戰(zhàn),并且實(shí)際往往花費(fèi)比較長(zhǎng)的時(shí)間。另一方面,他們提供更好的催款理由,也就是說(shuō),他們提供了系統(tǒng)架構(gòu)級(jí)的代碼覆蓋率。
系統(tǒng)測(cè)試與功能測(cè)試非常相近。區(qū)別在于它們不是一個(gè)假扮用戶,用戶是虛擬的。就像組件測(cè)試一樣,很多框架都是來(lái)幫助這類測(cè)試的。例如,jWebUnit通過(guò)模擬一個(gè)瀏覽器提供了測(cè)試Web應(yīng)用程序的基礎(chǔ)設(shè)施。
什么是接受測(cè)試?
接受測(cè)試與功能測(cè)試類似,不同點(diǎn)在于,理想情況下,客戶或者最終用戶來(lái)編寫接受測(cè)試。與功能測(cè)試類似,接受測(cè)試按照最終用戶的行為測(cè)試。一個(gè)備受關(guān)注的接受測(cè)試框架是Selenium,它使用瀏覽器來(lái)測(cè)試Web應(yīng)用程序。Selenium可以在build過(guò)程中自動(dòng)運(yùn)行,就像JUnit測(cè)試一樣。但是Selenium是一個(gè)新的平臺(tái):他不一定使用JUnit,方式也不太一樣。(Selenium RC就沒(méi)有這個(gè)問(wèn)題了)
我應(yīng)該使用jWebUnit或者Selenium?
jWebUnit是一個(gè)JUnit擴(kuò)展框架,設(shè)計(jì)用來(lái)進(jìn)行系統(tǒng)測(cè)試;所以,它需要你自己寫這些測(cè)試。Selenium是一個(gè)優(yōu)秀的接受測(cè)試和功能測(cè)試工具,不同于jWebUnit,它允許非程序員編寫測(cè)試。理想狀態(tài)下,你的團(tuán)隊(duì)可以同時(shí)使用兩種工具來(lái)確認(rèn)應(yīng)用程序的功能。
使用TestNG進(jìn)行測(cè)試分類
使用TestNG實(shí)現(xiàn)測(cè)試分類非常容易。使用TestNG的group注釋,邏輯上將測(cè)試分類就是進(jìn)行合適的group注釋,這非常簡(jiǎn)單。運(yùn)行某一分類的測(cè)試只需要將group名稱傳給test runner就可以了,例如通過(guò)Ant。
實(shí)現(xiàn)測(cè)試分類
所以,你的單元測(cè)試套件實(shí)際上是單元測(cè)試、組件測(cè)試和系統(tǒng)測(cè)試的套件。甚至,在你檢查所有的測(cè)試后發(fā)現(xiàn)build需要這么長(zhǎng)時(shí)間是因?yàn)榇蟛糠譁y(cè)試都是組件測(cè)試。下一個(gè)問(wèn)題是,如何通過(guò)JUnit實(shí)現(xiàn)測(cè)試分類?
你有很多選擇,但是讓我們先試驗(yàn)一下最簡(jiǎn)單的兩個(gè):
- 根據(jù)需要的分類創(chuàng)建不同的JUnit套件(suite)文件
- 對(duì)于不同類型的測(cè)試創(chuàng)建不同的目錄
創(chuàng)建不同的套件
你可以使用JUnit的TestSuite類(它也是一種Test)定義一組同類測(cè)試的集合。你要?jiǎng)?chuàng)建一個(gè)TestSuite的實(shí)例并添加相關(guān)的測(cè)試類到test方法中。你可以在TestSuite實(shí)例中通過(guò)定義一個(gè)叫做suite()的public static方法告訴JUnit這個(gè)套件包括哪些測(cè)試。所有包括的測(cè)試將會(huì)一次全部執(zhí)行。因此你可以通過(guò)創(chuàng)建TestSuite來(lái)實(shí)現(xiàn)測(cè)試分類,一個(gè)單元測(cè)試的TestSuite、一個(gè)組件測(cè)試的TestSuite,有一個(gè)系統(tǒng)測(cè)試的TestSuite。
例如清單1的類中的suite()方法創(chuàng)建了一個(gè)包含所有組建測(cè)試的TestSuite。注意這個(gè)類不是非常符合JUnit規(guī)范。他既沒(méi)有繼承TestCase,也沒(méi)有任何測(cè)試的定義。但是JUnit會(huì)自動(dòng)發(fā)現(xiàn)suite()方法并且運(yùn)行它返回的所有測(cè)試類。
清單1 單元測(cè)試的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的過(guò)程需要你察看你當(dāng)前的所有測(cè)試并將它們加入到相應(yīng)的類里面(例如,所有的單元測(cè)試加入到UnitTestSuite)。這也就意味著你在相應(yīng)的分類里面創(chuàng)建了新的測(cè)試,你必須編程式的將它們添加到合適的TestSuite中,當(dāng)然還需要重新編譯它們。
運(yùn)行單獨(dú)的TestSuite需要單獨(dú)的Ant任務(wù)來(lái)運(yùn)行正確的測(cè)試組。你可以定義一個(gè)component-test任務(wù)來(lái)執(zhí)行ComponentTtestSuite,就像清單2中的樣子:
清單2 運(yùn)行組建測(cè)試的一個(gè)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
>
理想情況下,你還需要一個(gè)觸發(fā)單元測(cè)試的任務(wù)和系統(tǒng)測(cè)試的任務(wù)。最后,還有希望運(yùn)行所有測(cè)試的情況,你需要?jiǎng)?chuàng)建第四個(gè)任務(wù)來(lái)運(yùn)行其它三個(gè)任務(wù),就像清單3里面那樣:
清單3 運(yùn)行所有測(cè)試的任務(wù)
<
target?
name
="test-all"
?depends
="unit-test,component-test,system-test"
/>
創(chuàng)建單獨(dú)的TestSuite是一個(gè)迅速實(shí)現(xiàn)測(cè)試分類的解決方案。缺點(diǎn)是這個(gè)方法需要你創(chuàng)建新的測(cè)試,你必須編成式的將它們添加到合適的TestSuite里面,這可能有點(diǎn)痛苦。給每個(gè)測(cè)試類型創(chuàng)建單獨(dú)的目錄可能是一種更加有彈性的方法,它允許你添加新的測(cè)試分類但無(wú)需重新編譯。
創(chuàng)建單獨(dú)的目錄
我發(fā)現(xiàn)最簡(jiǎn)單的通過(guò)JUnit實(shí)現(xiàn)測(cè)試分類的方法是邏輯上將不同類型的測(cè)試放到不同的目錄中。使用這個(gè)方法,所有的單元測(cè)試都放在unit目錄,所有的組建測(cè)試都放在component目錄,等等。
例如,在test目錄中保存著所有未分類的測(cè)試,你可以創(chuàng)建三個(gè)新的子目錄,就像清單4中那樣:
清單4 實(shí)現(xiàn)測(cè)試分類的目錄結(jié)構(gòu)
acme-proj/
???????test/
??????????unit/
??????????component/
??????????system/?
??????????conf/
運(yùn)行這些測(cè)試,你需要定義至少四個(gè)Ant任務(wù):一個(gè)給單元測(cè)試,另外的給組建測(cè)試,還有系統(tǒng)測(cè)試。第四個(gè)任務(wù)是一個(gè)方便運(yùn)行其它三個(gè)測(cè)試類型的任務(wù)(就像清單3種展示的那種方式)。
JUnit任務(wù)就像清單2中的形式。區(qū)別在哪里呢,只是在任務(wù)的batchtest這個(gè)地方。這次,fileset指向的是一個(gè)指定的目錄,就像清單5種的樣子,他指向了unit目錄:
清單5 JUnit任務(wù)中的batchtest方面,用來(lái)運(yùn)行所有單元測(cè)試
<
batchtest?
todir
="${testreportdir}"
>
?
<
fileset?
dir
="test/unit"
>
?
??
<
include?
name
="**/**Test.java"
/>
???????
?
</
fileset
>
</
batchtest
>
注意這個(gè)任務(wù)運(yùn)行test/unit目錄下的所有測(cè)試,當(dāng)創(chuàng)建了新的單元測(cè)試(或者其它分類的其它測(cè)試),你只需要把它們放到這個(gè)目錄里面就可以了!這比添加一行到TestSuite中并重新編譯它要方便多了。
問(wèn)題解決了!
回到最初的場(chǎng)景,我認(rèn)為你和你的團(tuán)隊(duì)會(huì)決定使用單獨(dú)的目錄這種彈性的解決方案來(lái)解決你們的build時(shí)間過(guò)長(zhǎng)的問(wèn)題。這個(gè)任務(wù)最難的一個(gè)方面是檢查和分清測(cè)試類型。你重構(gòu)你的Ant build文件創(chuàng)建四個(gè)新的任務(wù)(三個(gè)單獨(dú)的測(cè)試分類還有一個(gè)運(yùn)行它們?nèi)齻€(gè))。甚至,你修改CuiresControl只在check-in的時(shí)候運(yùn)行單元測(cè)試,而組建測(cè)試按小時(shí)運(yùn)行。更進(jìn)一步的檢查后,系統(tǒng)測(cè)試也可以幾個(gè)小時(shí)運(yùn)行一次,也許你會(huì)創(chuàng)建一個(gè)新的任務(wù)來(lái)同時(shí)運(yùn)行組建測(cè)試和系統(tǒng)測(cè)試。
最后的結(jié)果是每天測(cè)試運(yùn)行很多次,你的團(tuán)隊(duì)可以快速的發(fā)現(xiàn)集成錯(cuò)誤——一般在幾個(gè)小時(shí)內(nèi)。
創(chuàng)建敏捷構(gòu)建不是為了趕時(shí)髦,它實(shí)際上是保證代碼質(zhì)量的重要因素。測(cè)試運(yùn)行的更加頻繁,開(kāi)發(fā)人員的測(cè)試的價(jià)值就能直接轉(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