1. 單元測試入門——優(yōu)秀基因
單元測試最初興起于敏捷社區(qū)盒蟆。1997年趾娃,設(shè)計(jì)模式四巨頭之一Erich Gamma和極限編程發(fā)明人Kent Beck共同開發(fā)了JUnit庶弃,而JUnit框架在此之后又引領(lǐng)了xUnit家族的發(fā)展赞草,深刻的影響著單元測試在各種編程語言中的普及千劈。當(dāng)前,單元測試也成了敏捷開發(fā)流行以來的現(xiàn)代軟件開發(fā)中必不可少的工具之一辛孵。同時(shí)丛肮,越來越多的互聯(lián)網(wǎng)行業(yè)推崇自動(dòng)化測試的概念,作為自動(dòng)化測試的重要組成部分魄缚,單元測試是一種經(jīng)濟(jì)合理的回歸測試手段宝与,在當(dāng)前敏捷開發(fā)的迭代(Sprint)中非常流行和需要。
然而有些時(shí)候冶匹,這些單元測試并沒有有效的改善生產(chǎn)力伴鳖,甚至單元測試有時(shí)候變成一種負(fù)擔(dān)。人們盲目的追求測試覆蓋率徙硅,往往卻忽視了測試代碼本身的質(zhì)量,各種無效的單元測試反而帶來了沉重的維護(hù)負(fù)擔(dān)搞疗。
本篇講義將會(huì)集中的從單元測試的入門嗓蘑、優(yōu)秀單元測試的編寫以及單元測試的實(shí)踐等三個(gè)方面展開探討。
文中的相關(guān)約定:
文中的示例代碼塊均使用Java語言匿乃。
文中的粗體部分表示重點(diǎn)內(nèi)容和重點(diǎn)提示桩皿。
文中的引用框部分,一般是定義或者來源于其它地方幢炸。
文中標(biāo)題的【探討】泄隔,表示此部分講師與學(xué)員共同探討并由講師引導(dǎo),得到方案宛徊。
文中的代碼變量和說明
用方框圈起來的佛嬉,是相關(guān)代碼的變量、方法闸天、異常等暖呕。
1.1 單元測試的價(jià)值
-
什么是單元測試
在維基百科中,單元測試被定義為一段代碼調(diào)用另一段代碼苞氮,隨后檢驗(yàn)一些假設(shè)的正確性湾揽。
以上是對單元測試的傳統(tǒng)定義,盡管從技術(shù)上說是正確的,但是它很難使我們成為更加優(yōu)秀的程序員库物。這些定義在諸多討論單元測試的書籍和網(wǎng)站上霸旗,我們總能看到,可能你已經(jīng)厭倦戚揭,覺得是老生常談诱告。不過不必?fù)?dān)心,正是從這個(gè)我們熟悉的毫目,共同的出發(fā)點(diǎn)蔬啡,我們引申出單元測試的概念。
或許很多人將軟件測試行為與單元測試的概念混淆為一談镀虐。在正式開始考慮單元測試的定義之前箱蟆,請先思考下面的問題,回顧以前遇到的或者所寫的測試:
- 兩周或者兩個(gè)月刮便、甚至半年空猜、一年、兩年前寫的單元測試恨旱,現(xiàn)在還可以運(yùn)行并得到結(jié)果么辈毯?
- 兩個(gè)月前寫的單元測試,任何一個(gè)團(tuán)隊(duì)成員都可以運(yùn)行并且得到結(jié)果么搜贤?
- 是否可以在數(shù)分鐘以內(nèi)跑完所有的單元測試呢谆沃?
- 可以通過單擊一個(gè)按鈕就能運(yùn)行所寫的單元測試么?
- 能否在數(shù)分鐘內(nèi)寫一個(gè)基本的單元測試呢仪芒?
當(dāng)我們能夠?qū)ι鲜龅膯栴}唁影,全部回答“是”的時(shí)候,我們便可以定義單元測試的概念了掂名。優(yōu)秀的測試應(yīng)該以其本來的据沈、非手工的形式輕松執(zhí)行。同時(shí)饺蔑,這樣的測試應(yīng)該是任何人都可以使用锌介,任何人都可以運(yùn)行的。在這個(gè)前提下猾警,測試的運(yùn)行應(yīng)該能夠足夠快孔祸,運(yùn)行起來不費(fèi)力、不費(fèi)事肿嘲、不費(fèi)時(shí)融击,并且即便寫新的測試,也應(yīng)該能夠順利雳窟、不耗時(shí)的完成尊浪。如上便是我們需要的單元測試兆龙。
涵蓋上面描述的要求的情況下习瑰,我們可以提出比較徹底的單元測試的定義:
單元測試(Unit Test),是一段自動(dòng)化的代碼,用來調(diào)動(dòng)被測試的方法或類琴儿,而后驗(yàn)證基于該方法或類的邏輯行為的一些假設(shè)董虱。單元測試幾乎總是用單元測試框架來寫的捂敌。它寫起來很順手轰胁,運(yùn)行起來不費(fèi)時(shí)。它是全自動(dòng)的掉盅、可信賴的也拜、可讀性強(qiáng)的和可維護(hù)的。
接下來我們首先討論單元測試框架的概念:
框架是一個(gè)應(yīng)用程序的半成品趾痘÷框架提供了一個(gè)可復(fù)用的公共結(jié)構(gòu),程序員可以在多個(gè)應(yīng)用程序之間進(jìn)行共享該結(jié)構(gòu)永票,并且可以加以擴(kuò)展以便滿足它們的特定的要求卵贱。
單元測試檢查一個(gè)獨(dú)立工作單元的行為,在Java程序中侣集,一個(gè)獨(dú)立工作單元經(jīng)常是一個(gè)獨(dú)立的方法键俱,同時(shí)就是一項(xiàng)單一的任務(wù),不直接依賴于其它任何任務(wù)的完成世分。
所有的代碼都需要測試编振。于是在代碼中的滿足上述定義,并且對獨(dú)立的工作單元進(jìn)行測試的行為臭埋,就是我們討論的單元測試党觅。
?
-
優(yōu)秀單元測試的特性
單元測試是非常有威力的魔法,但是如果使用不當(dāng)也會(huì)浪費(fèi)你大量的時(shí)間斋泄,從而對項(xiàng)目造成巨大的不利影響。另一方面镐牺,如果沒有恰當(dāng)?shù)木帉懞蛯?shí)現(xiàn)單元測試炫掐,在維護(hù)和調(diào)用這些測試上面,也會(huì)很容易的浪費(fèi)很多時(shí)間睬涧,從而影響產(chǎn)品代碼和整個(gè)項(xiàng)目募胃。
我們不能讓這種情況出現(xiàn)。請切記畦浓,做單元測試的首要原因是為了工作更加輕松”允現(xiàn)在我們一起探討下如何編寫優(yōu)秀的單元測試,只有如此讶请,方可正確的開展單元測試祷嘶,提升項(xiàng)目的生產(chǎn)力屎媳。
根據(jù)上一小節(jié)的內(nèi)容,首先我們列出一些優(yōu)秀的單元測試大多具備的特點(diǎn):
- 自動(dòng)的论巍、可重復(fù)的執(zhí)行的測試
- 開發(fā)人員比較容易實(shí)現(xiàn)編寫的測試
- 一旦寫好烛谊,將來任何時(shí)間都依舊可以用
- 團(tuán)隊(duì)的任何人都可運(yùn)行的測試
- 一般情況下單擊一個(gè)按鈕就可以運(yùn)行
- 測試可以可以快速的運(yùn)行
- ……
或許還有更多的情形,我們可以再接再厲的思考出更多的場景嘉汰〉べ鳎總結(jié)這些,我們可以得到一些基本的應(yīng)該遵循的簡單原則鞋怀,它們能夠讓不好的單元測試遠(yuǎn)離你的項(xiàng)目双泪。這個(gè)原則定義了一個(gè)優(yōu)秀的測試應(yīng)該具備的品質(zhì),合稱為A-TRIP:
- 自動(dòng)化(Automatic)
- 徹底的(Thorough)
- 可重復(fù)(Repeatable)
- 獨(dú)立的(Independent)
- 專業(yè)的(Professional)
接下來密似,我們分別就每一個(gè)標(biāo)準(zhǔn)進(jìn)行分析和解釋焙矛,從而我們可以正確的理解這些。
-
A
-TRIP 自動(dòng)化(Automatic)單元測試需要能夠自動(dòng)的運(yùn)行辛友。這里包含了兩個(gè)層面:調(diào)用測試的自動(dòng)化以及結(jié)果檢查的自動(dòng)化薄扁。
- 調(diào)用測試的自動(dòng)化:代碼首先需要能夠正確的被調(diào)用,并且所有的測試可以有選擇的依次執(zhí)行废累。在一些時(shí)候邓梅,我們選擇IDE(Integration Development Environment,集成開發(fā)環(huán)境)可以幫助我們自動(dòng)的運(yùn)行我們指定的測試邑滨,當(dāng)然也可以考慮CI(Continuous Integration日缨,持續(xù)集成)的方式進(jìn)行自動(dòng)化執(zhí)行測試。
- 結(jié)果檢查的自動(dòng)化:測試結(jié)果必須在測試的執(zhí)行以后掖看,“自己”告訴“自己”并展示出來匣距。如果一個(gè)項(xiàng)目需要通過雇傭一個(gè)人來讀取測試的輸出,然后驗(yàn)證代碼是否能夠正常的工作哎壳,那么這是一種可能導(dǎo)致項(xiàng)目失敗的做法毅待。而且一致性回歸的一個(gè)重要特征就是能夠讓測試自己檢查自身是否通過了驗(yàn)證,人類對這些重復(fù)性的手工行為也是非常不擅長归榕。
-
A-
T
RIP 徹底的(Thorough)好的單元測試應(yīng)該是徹底的尸红,它們測試了所有可能會(huì)出現(xiàn)問題的情況。一個(gè)極端是每行代碼刹泄、代碼可能每一個(gè)分支外里、每一個(gè)可能拋出的異常等等,都作為測試對象特石。另一個(gè)極端是僅僅測試最可能的情形——邊界條件盅蝗、殘缺和畸形的數(shù)據(jù)等等。事實(shí)上這是一個(gè)項(xiàng)目層面的決策問題姆蘸。
另外請注意:Bug往往集中的出現(xiàn)在代碼的某塊區(qū)域中墩莫,而不是均勻的分布在代碼的每塊區(qū)域中的芙委。對于這種現(xiàn)象,業(yè)內(nèi)引出了一個(gè)著名的戰(zhàn)斗口號“不要修修補(bǔ)補(bǔ)贼穆,完全重寫题山!”。一般情況下故痊,完全拋棄一塊Bug很多的代碼塊顶瞳,并進(jìn)行重寫會(huì)令開銷更小,痛苦更少愕秫。
總之慨菱,單元測試越多,代碼問題越少戴甩。
-
A-T
R
IP 可重復(fù)(Repeatable)每一個(gè)測試必須可以重復(fù)的符喝,多次執(zhí)行,并且結(jié)果只能有一個(gè)甜孤。這樣說明协饲,測試的目標(biāo)只有一個(gè),就是測試應(yīng)該能夠以任意的的順序一次又一次的執(zhí)行缴川,并且產(chǎn)生相同的結(jié)果茉稠。意味著,測試不能依賴不受控制的任何外部因素把夸。這個(gè)話題引出了“測試替身”的概念而线,必要的時(shí)候,需要用測試替身來隔離所有的外界因素恋日。
如果每次測試執(zhí)行不能產(chǎn)生相同的結(jié)果膀篮,那么真相只有一個(gè):代碼中有真正的Bug。
-
A-TR
I
P 獨(dú)立的(Independent)測試應(yīng)該是簡潔而且精煉的岂膳,這意味著每個(gè)測試都應(yīng)該有強(qiáng)的針對性誓竿,并且獨(dú)立于其它測試和環(huán)境谈截。請記住,這些測試嫂丙,可能在同一時(shí)間點(diǎn)诽表,被多個(gè)開發(fā)人員運(yùn)行竿奏。那么在編寫測試的時(shí)候绿语,確保一次只測試了一樣?xùn)|西吕粹。
獨(dú)立的匹耕,意味著你可以在任何時(shí)間以任何順序運(yùn)行任何測試。每一個(gè)測試都應(yīng)該是一個(gè)孤島既鞠。
-
A-TRI
P
專業(yè)的(Professional)測試代碼需要是專業(yè)的损趋。意味著,在多次編寫測試的時(shí)候桐玻,需要注意抽取相同的代碼邏輯镊靴,進(jìn)行封裝設(shè)計(jì)。這樣的做法是可行的踊谋,而且需要得到鼓勵(lì)轿衔。
測試代碼害驹,是真實(shí)的代碼。在必要的時(shí)候摘刑,需要?jiǎng)?chuàng)建一個(gè)框架進(jìn)行測試枷恕。測試的代碼應(yīng)該和產(chǎn)品的代碼量大體相當(dāng)。所以測試代碼需要保持專業(yè)胡控,有良好的設(shè)計(jì)昼激。
?
-
生產(chǎn)力的因素
這里我們討論生產(chǎn)力的問題橙困。
當(dāng)單元測試越來越多的時(shí)候,團(tuán)隊(duì)的測試覆蓋率會(huì)快速的提高夏跷,不用再花費(fèi)時(shí)間修復(fù)過去的錯(cuò)誤,待修復(fù)缺陷的總數(shù)在下降猫态。測試開始清晰可見的影響團(tuán)隊(duì)工作的質(zhì)量。但是當(dāng)測試覆蓋率不斷提高的時(shí)候匆光,我們是否要追求100%的測試覆蓋率呢?
事實(shí)上周崭,那些確實(shí)的測試,不會(huì)給團(tuán)隊(duì)帶來更多價(jià)值摸航,花費(fèi)更多精力來編寫測試不會(huì)帶來額外的收益。很多測試未覆蓋到的代碼读串,在項(xiàng)目中事實(shí)上也沒有用到。何必測試那些空的方法呢胀茵?同時(shí),100%的覆蓋率并不能確保沒有缺陷——它只能保證你所有的代碼都執(zhí)行了脱拼,不論程序的行為是否滿足要求熄浓,與其追求代碼覆蓋率赌蔑,不如將重點(diǎn)關(guān)注在確保寫出有意義的測試跷乐。
當(dāng)團(tuán)隊(duì)已經(jīng)達(dá)到穩(wěn)定水平——曲線的平坦部分顯示出額外投資的收益遞減。測試越多浅侨,額外測試的價(jià)值越少。第一個(gè)測試最有可能是針對代碼最重要的區(qū)域证膨,因此帶來高價(jià)值與高風(fēng)險(xiǎn)如输。當(dāng)我們?yōu)閹缀跛惺虑榫帉憸y試后,那些仍然沒有測試覆蓋的地方椎例,很可能是最不重要和最不可能破壞的挨决。
接下來分析一個(gè)測試因素影響的圖:
編排.png事實(shí)上,大多數(shù)代碼將測試作為質(zhì)量工具订歪,沿著曲線停滯了盖高。從這里看,我們需要找出影響程序員生產(chǎn)力的因素。本質(zhì)上谦秧,測試代碼的重復(fù)和多余的復(fù)雜性會(huì)降低生產(chǎn)力,抵消測試帶來的正面影響焕窝。最直接的兩個(gè)影響生產(chǎn)力的因素:
反饋環(huán)長度
和調(diào)試
垃沦。這兩者是在鍵盤上消耗程序員時(shí)間的罪魁禍?zhǔn)住H绻阱e(cuò)誤發(fā)生后迅速學(xué)習(xí)卧惜,那么花在調(diào)試上的時(shí)間是可以大幅避免的返工——同時(shí),反饋環(huán)越長,花在調(diào)試上的時(shí)間越多退腥。等待對變更進(jìn)行確認(rèn)和驗(yàn)證澜术,在很大程度上牽扯到測試執(zhí)行的速度兰英,這個(gè)是上述強(qiáng)調(diào)的反饋環(huán)長度和調(diào)試時(shí)間的根本原因之一寨闹。另外三個(gè)根本原因會(huì)影響程序員的調(diào)試量净赴。
- 測試的可讀性:缺乏可讀性自然降低分析的熟讀割以,并且鼓勵(lì)程序員打開調(diào)試器严沥,因?yàn)殚喿x代碼不會(huì)讓你明白嗜桌。同時(shí)因?yàn)楹茈y看出錯(cuò)誤的所在方灾,還會(huì)引入更多的缺陷坪郭。
- 測試結(jié)果的準(zhǔn)確度:準(zhǔn)確度是一個(gè)基本要求个从。
- 可依賴性和可靠性:可靠并且重復(fù)的方式運(yùn)行測試,提供結(jié)果是另一個(gè)基本要求歪沃。
?
-
設(shè)計(jì)潛力的曲線
假設(shè)先寫了最重要的測試——針對最常見和基本的場景嗦锐,以及軟件架構(gòu)中的關(guān)鍵部位。那么測試質(zhì)量很高沪曙,我們可以講重復(fù)的代碼都重構(gòu)掉奕污,并且保持測試精益和可維護(hù)。那么我們想象一下液走,積累了如此高的測試覆蓋率以后碳默,唯一沒測試到的地方,只能是那些最不重要和最不可能破壞的缘眶,項(xiàng)目沒有運(yùn)行到的地方了嘱根。平心而論,那么地方也是沒有什么價(jià)值的地方巷懈,那么该抒,之前的做法傾向于收益遞減——已經(jīng)不能再從編寫測試這樣的事情中獲取價(jià)值了。
這是由于不做的事情而造成的質(zhì)量穩(wěn)態(tài)顶燕。之所以這么說凑保,是因?yàn)橄胍竭_(dá)更高的生產(chǎn)力,我們需要換個(gè)思路去考慮測試涌攻。為了找回丟掉的潛力欧引,我們需要從編寫測試中找到完全不同的價(jià)值——價(jià)值來自于創(chuàng)新及設(shè)計(jì)導(dǎo)向,而并非防止回歸缺陷的保護(hù)及驗(yàn)證導(dǎo)向恳谎。
總而言之维咸,為了充分和完全的發(fā)揮測試的潛力,我們需要:
- 像生產(chǎn)代碼一樣對待你測試代碼——大膽重構(gòu)惠爽、創(chuàng)建和維護(hù)高質(zhì)量測試
- 開始將測試作為一種設(shè)計(jì)工具癌蓖,指導(dǎo)代碼針對實(shí)際用途進(jìn)行設(shè)計(jì)。
第一種方法婚肆,是我們在這篇講義中討論的重點(diǎn)租副。多數(shù)程序員在編寫測試的時(shí)候會(huì)不知所措,無法顧及高質(zhì)量较性,或者降低編寫用僧、維護(hù)结胀、運(yùn)行測試的成本。
第二種方法责循,是討論利用測試作為設(shè)計(jì)的方面糟港,我們的目的是對這種動(dòng)態(tài)和工作方式有個(gè)全面的了解,在接下來的[探討]中我們繼續(xù)分析這個(gè)話題院仿。
?
1.2 [探討]正確地認(rèn)識單元測試
-
練習(xí):一個(gè)簡單的單元測試示例
我們從一個(gè)簡單的例子開始設(shè)計(jì)測試秸抚,它是一個(gè)獨(dú)立的方法,用來查找list中的最大值歹垫。
int getLargestElement(int[] list){ // TODO: find largest element from list and return it. }
比如剥汤,給定一個(gè)數(shù)組 { 1, 50排惨, 81吭敢, 100 },這個(gè)方法應(yīng)該返回100暮芭,這樣就構(gòu)成了一個(gè)很合理測試鹿驼。那么,我們還能想出一些別的測試么辕宏?就這樣的方法畜晰,在繼續(xù)閱讀之前,請認(rèn)真的思考一分鐘匾效,記下來所有能想到的測試。
在繼續(xù)閱讀之前恤磷,請靜靜的思考一會(huì)兒……
想到了多少測試呢面哼?請將想到的測試都在紙上寫出來。格式如下:
- 50, 60, 7, 58, 98 --> 98
- 100, 90, 25 --> 100
- ……
然后我們編寫一個(gè)基本的符合要求的函數(shù)扫步,來繼續(xù)進(jìn)行測試魔策。
public int getLargestElement(int[] list) { int temp = Integer.MIN_VALUE; for (int i = 0; i < list.length; i++) { if (temp < list[i]) { temp = list[i]; } } return temp; }
然后請考慮上述代碼是否有問題,可以用什么樣的例子來進(jìn)行測試河胎。
?
-
分析:為什么不寫單元測試
請思考當(dāng)前在組織或者項(xiàng)目中闯袒,如何寫單元測試,是否有不寫單元測試的習(xí)慣和借口游岳,這些分別是什么政敢?
?
-
分析:單元測試的結(jié)構(gòu)與內(nèi)容
當(dāng)我們確定要寫單元測試的時(shí)候,請認(rèn)真分析胚迫,一個(gè)單元測試包含什么樣的內(nèi)容喷户,為什么?
?
-
分析:單元測試的必要性
請分析單元測試必要性访锻,嘗試得出單元測試所帶來的好處褪尝。
單元測試的主要目的闹获,就是驗(yàn)證應(yīng)用程序是否可以按照預(yù)期的方式正常運(yùn)行,以及盡早的發(fā)現(xiàn)錯(cuò)誤河哑。盡管功能測試也可以做到這一點(diǎn)避诽,但是單元測試更加強(qiáng)大,并且用戶更加豐富璃谨,它能做的不僅僅是驗(yàn)證應(yīng)用程序的正常運(yùn)行沙庐,單元測試還可以做到更多。
-
帶來更高的測試覆蓋率
功能測試大約可以覆蓋到70%的應(yīng)用程序代碼睬罗,如果希望進(jìn)行的更加深入一點(diǎn)轨功,提供更高的測試覆蓋率,那么我們需要編寫單元測試了容达。單元測試可以很容易的模擬錯(cuò)誤條件古涧,這一點(diǎn)在功能測試中卻很難辦到,有些情況下甚至是不可能辦到的花盐。單元測試不僅提供了測試羡滑,還提供了更多的其它用途,在最后一部分我們將會(huì)繼續(xù)介紹算芯。
-
提高團(tuán)隊(duì)效率
在一個(gè)項(xiàng)目中柒昏,經(jīng)過單元測試通過的代碼,可以稱為高質(zhì)量的代碼熙揍。這些代碼無需等待到其它所有的組件都完成以后再提交职祷,而是可以隨時(shí)提交,提高的團(tuán)隊(duì)的效率届囚。如果不進(jìn)行單元測試有梆,那么測試行為大多數(shù)要等到所有的組件都完成以后,整個(gè)應(yīng)用程序可以運(yùn)行以后意系,才能進(jìn)行泥耀,嚴(yán)重影響了團(tuán)隊(duì)效率。
-
自信的重構(gòu)和改進(jìn)實(shí)現(xiàn)
在沒有進(jìn)行單元測試的代碼中蛔添,重構(gòu)是有著巨大風(fēng)險(xiǎn)的行為痰催。因?yàn)槟憧偸强赡軙?huì)損壞一些東西。而單元測試提供了一個(gè)安全網(wǎng)迎瞧,可以為重構(gòu)的行為提供信心夸溶。同時(shí)在良好的單元測試基礎(chǔ)上,對代碼進(jìn)行改進(jìn)實(shí)現(xiàn)凶硅,對一些修改代碼蜘醋,增加新的特性或者功能的行為,有單元測試作為保障咏尝,可以防止在改進(jìn)的基礎(chǔ)上压语,引入新的Bug啸罢。
-
將預(yù)期的行為文檔化
在一些代碼的文檔中,示例的威力是眾所周知的胎食。當(dāng)完成一個(gè)生產(chǎn)代碼的時(shí)候扰才,往往要生成或者編寫對應(yīng)的API文檔。而如果在這些代碼中進(jìn)行了完整的單元測試厕怜,則這些單元測試就是最好的實(shí)例衩匣。它們展示了如何使用這些API,也正是因?yàn)槿绱酥嗪剑鼈兙褪峭昝赖拈_發(fā)者文檔琅捏,同時(shí)因?yàn)閱卧獪y試必須與工作代碼保持同步,所以比起其它形式的文檔递雀,單元測試必須始終是最新的柄延,最有效的。
?
-
1.3 用 JUnit 進(jìn)行單元測試
JUnit誕生于1997年缀程,Erich Gamma 和 Kent Beck 針對 Java 創(chuàng)建了一個(gè)簡單但是有效的單元測試框架搜吧,隨后迅速的成為 Java 中開發(fā)單元測試的事實(shí)上的標(biāo)準(zhǔn)框架,被稱為 xUnit 的相關(guān)測試框架杨凑,正在逐漸成為任何語言的標(biāo)準(zhǔn)框架滤奈。
以我們的角度,JUnit用來“確保方法接受預(yù)期范圍內(nèi)的輸入撩满,并且為每一次測試輸入返回預(yù)期的值”蜒程。在這一節(jié)里,我們從零開始介紹如何為一個(gè)簡單的類創(chuàng)建單元測試伺帘。我們首先編寫一個(gè)測試昭躺,以及運(yùn)行該測試的最小框架,以便能夠理解單元測試是如何處理的曼追。然后我們在通過 JUnit 展示正確的工具可以如何使生活變得更加簡單窍仰。
本文中使用 JUnit 4 最新版進(jìn)行單元測試的示例與講解汉规。
JUnit 4 用到了許多 Java 5 中的特性礼殊,如注解。JUnit 4 需要使用 Java 5 或者更高的版本针史。
-
用 JUnit 構(gòu)建單元測試
這里我們開始構(gòu)建單元測試晶伦。
首先我們使用之前一節(jié)的【探討】中使用過的類,作為被測試的對象啄枕。創(chuàng)建一個(gè)類婚陪,叫做
HelloWorld
,該類中有一個(gè)方法频祝,可以從輸入的一個(gè)整型數(shù)組中泌参,找到最大的值脆淹,并且返回該值。代碼如下:
public class HelloWorld { public int getLargestElement(int[] list) { int temp = Integer.MIN_VALUE; for (int i = 0; i < list.length; i++) { if (temp < list[i]) { temp = list[i]; } } return temp; } }
雖然我們針對該類沽一,沒有列出文檔盖溺,但是 HelloWorld 中的 int getLargestElement(int[])方法的意圖顯然是接受一個(gè)整型的數(shù)組,并且以 int 的類型铣缠,返回該數(shù)組中最大的值烘嘱。編譯器能夠告訴我們,它通過了編譯蝗蛙,但是我們也應(yīng)該確保它在運(yùn)行期間可以正常的工作蝇庭。
單元測試的核心原則是“任何沒有經(jīng)過自動(dòng)測試的程序功能都可以當(dāng)做它不存在”。getLargestElement 方法代表了 HelloWorld 類的一個(gè)核心功能捡硅,我們擁有了一些實(shí)現(xiàn)該功能的代碼哮内,現(xiàn)在缺少的只是一個(gè)證明實(shí)現(xiàn)能夠正常工作的自動(dòng)測試。
這個(gè)時(shí)候病曾,進(jìn)行任何測試看起來都會(huì)有些困難牍蜂,畢竟我們甚至沒有可以輸入一個(gè)數(shù)組的值的用戶界面。除非我們使用在【探討】中使用的類進(jìn)行測試泰涂。
示例代碼:
public class HelloWorldTest { public static void main(String[] args) { HelloWorld hello = new HelloWorld(); int[] listToTest = {-10, -20, -100, -90}; int result = hello.getLargestElement(listToTest); if (result != -10) { System.out.println("獲取最大值錯(cuò)誤鲫竞,期望的結(jié)果是 100;實(shí)際錯(cuò)誤的結(jié)果: " + result); } else { System.out.println("獲取最大值正確逼蒙,通過測試从绘。"); } } }
輸出結(jié)果如下:
獲取最大值正確,通過測試是牢。 Process finished with exit code 0
第一個(gè)
HelloWorldTest
類非常簡單僵井。它創(chuàng)建了 HelloWorld 的一個(gè)實(shí)例,傳遞給它一個(gè)數(shù)組驳棱,并且檢查運(yùn)行的結(jié)果批什。如果運(yùn)行結(jié)果與我們預(yù)期的不一致,那么我們就在標(biāo)準(zhǔn)輸出設(shè)備上輸出一條消息社搅。現(xiàn)在我們編譯并且運(yùn)行這個(gè)程序驻债,那么測試將會(huì)正常通過,同時(shí)一切看上去都非常順利形葬『夏牛可是事實(shí)上并非都是如此圓滿,如果我們修改部分測試笙以,再次運(yùn)行淌实,可能會(huì)遇到不通過測試的情況,甚至代碼異常。
接下來我們修改代碼如下:
public class HelloWorldTest { public static void main(String[] args) { HelloWorld hello = new HelloWorld(); int[] listToTest = null; int result = hello.getLargestElement(listToTest); if (result != -10) { System.out.println("獲取最大值錯(cuò)誤拆祈,期望的結(jié)果是 100恨闪;實(shí)際錯(cuò)誤的結(jié)果: " + result); } else { System.out.println("獲取最大值正確,通過測試放坏。"); } } }
當(dāng)我們再次執(zhí)行代碼的時(shí)候凛剥,代碼運(yùn)行就會(huì)報(bào)錯(cuò)。運(yùn)行結(jié)果如下:
Exception in thread "main" java.lang.NullPointerException at HelloWorld.getLargestElement(HelloWorld.java:11) at HelloWorldTest.main(HelloWorldTest.java:13) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144) Process finished with exit code 1
按照第一節(jié)中的描述的優(yōu)秀的單元測試轻姿,上述代碼毫無疑問犁珠,稱不上優(yōu)秀的單元測試,因?yàn)闇y試連運(yùn)行都無法運(yùn)行互亮。令人高興的是犁享,JUnit 團(tuán)隊(duì)解決了上述麻煩。JUnit 框架支持自我檢測豹休,并逐個(gè)報(bào)告每個(gè)測試的所有錯(cuò)誤和結(jié)果炊昆。接下來我們來進(jìn)一步了解 JUnit 。
JUnit 是一個(gè)單元測試框架威根,在設(shè)計(jì)之初凤巨,JUnit 團(tuán)隊(duì)已經(jīng)為框架定義了3個(gè)不相關(guān)的目標(biāo):
- 框架必須幫助我們編寫有用的測試
- 框架必須幫助我們創(chuàng)建具有長久價(jià)值的測試
- 框架必須幫助我們通過復(fù)用代碼來降低編寫測試的成本
首先安裝 JUnit 。這里我們使用原始的方式添加 JAR 文件到 ClassPath 中洛搀。
下載地址:https://github.com/junit-team/junit4/wiki/Download-and-Install敢茁,下載如下兩個(gè) JAR 包,放到項(xiàng)目的依賴的路徑中留美。
junit.jar
hamcrest-core.jar
在 IDEA 的項(xiàng)目中彰檬,添加一個(gè)文件夾 lib,將上述兩個(gè)文件添加到 lib 中谎砾。
然后 File | Project Structure | Modules逢倍,打開 Modules 對話框,選擇右邊的 Dependencies 的選項(xiàng)卡景图,點(diǎn)擊右邊的 + 號较雕,選擇 “1 JARs or directories”并找到剛剛添加的兩個(gè) JRA 文件,并確定挚币。
然后新建 Java Class亮蒋,代碼如下:
public class HelloWorldTests { @Test public void test01GetLargestElement(){ HelloWorld hello = new HelloWorld(); int[] listToTest = {10, 20, 100, 90}; int result = hello.getLargestElement(listToTest); Assert.assertEquals("獲取最大值錯(cuò)誤! ", 100, result); } @Test public void test02GetLargestElement(){ HelloWorld hello = new HelloWorld(); int[] listToTest = {-10, 20, -100, 90}; int result = hello.getLargestElement(listToTest); Assert.assertEquals("獲取最大值錯(cuò)誤! ", 90, result); } }
如上的操作,我們便定義了一個(gè)單元測試忘晤,使用 JUnit 編寫了測試宛蚓。主要的要點(diǎn)如下:
- 針對每個(gè)測試的對象類激捏,單獨(dú)編寫測試類设塔,測試方法,避免副作用
- 定義一個(gè)測試類
- 使用 JUnit 的注解方式提供的方法: @Test
- 使用 JUnit 提供的方法進(jìn)行斷言:Assert.assertEquals(String msg, long expected, long actual)
- 創(chuàng)建一個(gè)測試方法的要求:該方法必須是公共的,不帶任何參數(shù)闰蛔,返回值類型為void痕钢,同時(shí)必須使用@Test注解
-
JUnit 的各種斷言
為了進(jìn)行驗(yàn)測試驗(yàn)證,我們使用了由 JUnit 的
Assert
類提供的assert
方法序六。正如我們在上面的例子中使用的那樣任连,我們在測試類中靜態(tài)的導(dǎo)入這些方法瘟檩,同時(shí)還有更多的方法以供我們使用萎河,如下我們列出一些流行的 assert 方法。方法 Method 檢查條件 assertEquals(msg, a, b)
a == b荸型,msg可選繁涂,用來解釋失敗的原因 assertNotEquals(msg, a, b)
a != b拱她,msg可選,用來解釋失敗的原因 assertTrue(msg, x )
x 是真扔罪,msg可選秉沼,用來解釋失敗的原因 assertFalse(msg, x)
x 是假,msg可選矿酵,用來解釋失敗的原因 assertSame(msg, a, b)
a 不是 b唬复,msg可選,用來解釋失敗的原因 assertNull(msg, x)
x 是null全肮,msg可選敞咧,用來解釋失敗的原因 assertNotNull(msg, x)
x 不是null,msg可選辜腺,用來解釋失敗的原因 assertThat(msg, actual, matcher)
用匹配器進(jìn)行斷言妄均,高級應(yīng)用*,不再此文檔討論 一般來說哪自,一個(gè)測試方法包括了多個(gè)斷言丰包。當(dāng)其中一個(gè)斷言失敗的時(shí)候,整個(gè)測試方法將會(huì)被終止——從而導(dǎo)致該方法中剩下的斷言將會(huì)無法執(zhí)行了壤巷。此時(shí)邑彪,不能有別的想法,只能先修復(fù)當(dāng)前失敗的斷言胧华,以此類推寄症,不斷地修復(fù)當(dāng)前失敗的斷言,通過一個(gè)個(gè)測試矩动,慢慢前行有巧。
-
JUnit 的框架
到目前為止,我們只是介紹了斷言本身悲没,很顯然我們不能只是簡單的把斷言方法寫完篮迎,就希望測試可以運(yùn)行起來。我們需要一個(gè)框架來輔助完成這些,那么我們就要做多一些工作了甜橱。很幸運(yùn)的是逊笆,我們不用多做太多。
在 JUnit 4 提供了
@Before
和@After
岂傲,在每個(gè)測試函數(shù)調(diào)用之前/后都會(huì)調(diào)用难裆。-
@Before
: Method annotated with@Before
executes before every test. 每個(gè)測試方法開始前執(zhí)行的方法 -
@After
: Method annotated with@After
executes after every test. 每個(gè)測試方法執(zhí)行后再執(zhí)行的方法
如果在測試之前有些工作我們只想做一次,用不著每個(gè)函數(shù)之前都做一次镊掖。比如讀一個(gè)很大的文件乃戈。那就用下面兩個(gè)來標(biāo)注:
@BeforeClass
: 測試類初始化的時(shí)候,執(zhí)行的方法
@AfterClass
: 測試類銷毀的時(shí)候亩进,執(zhí)行的方法注意:
-
@Before
/@After
可以執(zhí)行多次;@BeforeClass
/@AfterClass
只能執(zhí)行一次 - 如果我們預(yù)計(jì)有Exception偏化,那就給@Test加參數(shù):
@Test(expected = XXXException.class)
- 如果出現(xiàn)死循環(huán)怎么辦?這時(shí)timeout參數(shù)就有用了:
@Test(timeout = 1000)
- 如果我們暫時(shí)不用測試一個(gè)用例镐侯,我們不需要?jiǎng)h除或都注釋掉侦讨。只要改成:
@Ignore
,你也可以說明一下原因@Ignore("something happens")
示例代碼:下面的代碼代表了單元測試用例的基本框架
public class JUnitDemoTest { @Before public void setUp(){ //TODO: 測試預(yù)置條件苟翻,測試安裝 } @After public void tearDown(){ //TODO: 測試清理韵卤,測試卸載 } @Test public void test01(){ //TODO: test01 腳本 } @Test public void test02(){ //TODO: test02 腳本 } @Test public void test03(){ //TODO: test03 腳本 } }
單元測試框架的過程如下:
測試過程.pngJUnit 需要注意的事項(xiàng):
- 每個(gè)
@Test
都是一個(gè)測試用例,一個(gè)類可以寫多個(gè)@Test
- 每個(gè)
@Test
執(zhí)行之前 都會(huì)執(zhí)行 @Before崇猫,執(zhí)行之后都會(huì)運(yùn)行@After
- 每個(gè)
@Test
沈条,@After
,@Before
都必須是public void
, 參數(shù)為空 -
@After
/@Before
也可以是多個(gè)诅炉,并且有執(zhí)行順序蜡歹。在每個(gè)@Test
前后執(zhí)行多次。-
@Before
多個(gè)名字長度一致涕烧,z -> a
, 長度不一致月而,會(huì)先執(zhí)行名字短的。 -
@After
/@Test
多個(gè)名字長度一致议纯,a -> z
, 長度不一致父款,會(huì)后執(zhí)行名字短的。
-
-
@AfterClass
/@BeforeClass
也可以是多個(gè)瞻凤,并且有執(zhí)行順序憨攒。只會(huì)在測試類的實(shí)例化前后各執(zhí)行一次。-
@BeforeClass
多個(gè)名字長度一致阀参,z -> a
, 長度不一致肝集,會(huì)先執(zhí)行名字短的。 -
@AfterClass
多個(gè)名字長度一致蛛壳,a -> z
, 長度不一致杏瞻,會(huì)后執(zhí)行名字短的所刀。
-
-
@AfterClass
/@BeforeClass
都必須是public static void,
參數(shù)為空 - 測試結(jié)果有 通過、不通過和錯(cuò)誤 三種伐憾。
-
-
JUnit 的測試運(yùn)行
這一小節(jié),我們來介紹一下 JUnit 4 中的新的測試運(yùn)行器(Test Runner)赫模。如果我們剛開始編寫測試树肃,那么我們需要盡可能快捷的運(yùn)行這些測試,這樣我們才能夠?qū)y試融合到開發(fā)循環(huán)中去瀑罗。
編碼 → 運(yùn)行 → 測試 → 編碼……
其中胸嘴,JUnit 就可以讓我們構(gòu)建和運(yùn)行測試。我們可以按照組合測試Suite 以及參數(shù)化測試分別來運(yùn)行測試斩祭。
-
組合測試Suite
測試集 (Suite 或者 test suite)一組測試劣像。測試集是一種把多個(gè)相關(guān)測試歸入一組的便捷測試方式〈菝担可以在一個(gè)測試集中耳奕,定義需要打包測試的類,并一次性運(yùn)行所有包含的測試诬像;也可以分別定義多個(gè)測試集屋群,然后在一個(gè)主測試集中運(yùn)行多個(gè)相關(guān)的測試集,打包相關(guān)的測試的類坏挠,并一次性運(yùn)行所有包含的測試芍躏。
示例代碼如下:
@RunWith(value = Suite.class) @Suite.SuiteClasses(value = HelloWorldTests.class) public class HelloWorldTestRunner { }
?
-
參數(shù)化測試
參數(shù)化測試(Parameterized)是測試運(yùn)行器允許使用不同的參數(shù)多次運(yùn)行同一個(gè)測試。參數(shù)化測試的代碼如下:
@RunWith(value = Parameterized.class) public class ParameterizedHelloWorldTests { @Parameterized.Parameters public static Collection getTestParameters() { int[] listToTest1 = {10, 80, 100, -96}; int[] listToTest2 = {-10, -80, -100, -6}; int[] listToTest3 = {10, -80, -100, -96}; int[] listToTest4 = {10, -80, 100, -96}; int[] listToTest5 = {10, 80, -100, -96}; return Arrays.asList(new Object[][]{ {100, listToTest1}, {-6, listToTest2}, {10, listToTest3}, {100, listToTest4}, {80, listToTest5}}); } @Parameterized.Parameter public int expected; @Parameterized.Parameter(value = 1) public int[] listToTest; @Test public void testGetLargestElementByParameters() { Assert.assertEquals("獲取最大元素錯(cuò)誤降狠!", expected, new HelloWorld().getLargestElement(listToTest)); } }
對于參數(shù)化測試的運(yùn)行器來運(yùn)行測試類对竣,那么必須滿足以下要求:
- 測試類必須使用
@RunWith(value = Parameterized.class)
注解 - 必須聲明測試中所使用的實(shí)例變量
- 提供一個(gè)用
@Parameterized.Parameters
的注解方法,這里用的是getTestParameters()
榜配,同時(shí)此方法的簽名必須是public static Collection
- 為測試指定構(gòu)造方法否纬,或者一個(gè)個(gè)全局變量的成員進(jìn)行賦值
- 所有的測試方法以
@Test
注解,實(shí)例化被測試的程序蛋褥,同時(shí)在斷言中使用我們提供的全局變量作為參數(shù)
?
- 測試類必須使用
-
1.4 [探討]按業(yè)務(wù)價(jià)值導(dǎo)向進(jìn)行單元測試設(shè)計(jì)
-
練習(xí):測試的結(jié)果是否正確
如果測試代碼能夠運(yùn)行正確烦味,我們要怎么才能知道它是正確的呢?
如何應(yīng)對測試數(shù)據(jù)量比較大的時(shí)候壁拉,我們的測試代碼如何編寫谬俄?
?
-
練習(xí):測試的邊界條件
尋找邊界條件是單元測試中最有價(jià)值的工作之一,一般來說Bug出現(xiàn)在邊界上的概率比較大弃理。那么我們都需要考慮什么樣的邊界條件呢溃论?
?
-
練習(xí):強(qiáng)制產(chǎn)生錯(cuò)誤條件
關(guān)于產(chǎn)生錯(cuò)誤的條件,請列出一個(gè)詳細(xì)的清單來痘昌。
?
-
分析:測試作為設(shè)計(jì)工具
第一節(jié)【專題】中钥勋,我們有討論設(shè)計(jì)潛力的曲線炬转,其中第二條方案強(qiáng)調(diào)了測試作為設(shè)計(jì)的工具。那么我們想就兩個(gè)方面來討論這個(gè)測試設(shè)計(jì)的問題算灸。
- TDD扼劈,測試驅(qū)動(dòng)開發(fā)
- BDD,行為驅(qū)動(dòng)開發(fā)
?