1. 單元測試入門——優(yōu)秀基因
單元測試最初興起于敏捷社區(qū)。1997年,設(shè)計模式四巨頭之一Erich Gamma和極限編程發(fā)明人Kent Beck共同開發(fā)了JUnit,而JUnit框架在此之后又引領(lǐng)了xUnit家族的發(fā)展,深刻的影響著單元測試在各種編程語言中的普及。當(dāng)前,單元測試也成了敏捷開發(fā)流行以來的現(xiàn)代軟件開發(fā)中必不可少的工具之一。同時,越來越多的互聯(lián)網(wǎng)行業(yè)推崇自動化測試的概念,作為自動化測試的重要組成部分,單元測試是一種經(jīng)濟合理的回歸測試手段,在當(dāng)前敏捷開發(fā)的迭代(Sprint)中非常流行和需要。
然而有些時候,這些單元測試并沒有有效的改善生產(chǎn)力,甚至單元測試有時候變成一種負擔(dān)。人們盲目的追求測試覆蓋率,往往卻忽視了測試代碼本身的質(zhì)量,各種無效的單元測試反而帶來了沉重的維護負擔(dān)。
本篇講義將會集中的從單元測試的入門、優(yōu)秀單元測試的編寫以及單元測試的實踐等三個方面展開探討。
文中的相關(guān)約定:
文中的示例代碼塊均使用Java語言。
文中的粗體部分表示重點內(nèi)容和重點提示。
文中的引用框部分,一般是定義或者來源于其它地方。
文中標(biāo)題的【探討】,表示此部分講師與學(xué)員共同探討并由講師引導(dǎo),得到方案。
文中的代碼變量和說明
用方框圈起來的,是相關(guān)代碼的變量、方法、異常等。
1.1 單元測試的價值
-
什么是單元測試
在維基百科中,單元測試被定義為一段代碼調(diào)用另一段代碼,隨后檢驗一些假設(shè)的正確性。
以上是對單元測試的傳統(tǒng)定義,盡管從技術(shù)上說是正確的,但是它很難使我們成為更加優(yōu)秀的程序員。這些定義在諸多討論單元測試的書籍和網(wǎng)站上,我們總能看到,可能你已經(jīng)厭倦,覺得是老生常談。不過不必擔(dān)心,正是從這個我們熟悉的,共同的出發(fā)點,我們引申出單元測試的概念。
或許很多人將軟件測試行為與單元測試的概念混淆為一談。在正式開始考慮單元測試的定義之前,請先思考下面的問題,回顧以前遇到的或者所寫的測試:
- 兩周或者兩個月、甚至半年、一年、兩年前寫的單元測試,現(xiàn)在還可以運行并得到結(jié)果么?
- 兩個月前寫的單元測試,任何一個團隊成員都可以運行并且得到結(jié)果么?
- 是否可以在數(shù)分鐘以內(nèi)跑完所有的單元測試呢?
- 可以通過單擊一個按鈕就能運行所寫的單元測試么?
- 能否在數(shù)分鐘內(nèi)寫一個基本的單元測試呢?
當(dāng)我們能夠?qū)ι鲜龅膯栴},全部回答“是”的時候,我們便可以定義單元測試的概念了。優(yōu)秀的測試應(yīng)該以其本來的、非手工的形式輕松執(zhí)行。同時,這樣的測試應(yīng)該是任何人都可以使用,任何人都可以運行的。在這個前提下,測試的運行應(yīng)該能夠足夠快,運行起來不費力、不費事、不費時,并且即便寫新的測試,也應(yīng)該能夠順利、不耗時的完成。如上便是我們需要的單元測試。
涵蓋上面描述的要求的情況下,我們可以提出比較徹底的單元測試的定義:
單元測試(Unit Test),是一段自動化的代碼,用來調(diào)動被測試的方法或類,而后驗證基于該方法或類的邏輯行為的一些假設(shè)。單元測試幾乎總是用單元測試框架來寫的。它寫起來很順手,運行起來不費時。它是全自動的、可信賴的、可讀性強的和可維護的。
接下來我們首先討論單元測試框架的概念:
框架是一個應(yīng)用程序的半成品。框架提供了一個可復(fù)用的公共結(jié)構(gòu),程序員可以在多個應(yīng)用程序之間進行共享該結(jié)構(gòu),并且可以加以擴展以便滿足它們的特定的要求。
單元測試檢查一個獨立工作單元的行為,在Java程序中,一個獨立工作單元經(jīng)常是一個獨立的方法,同時就是一項單一的任務(wù),不直接依賴于其它任何任務(wù)的完成。
所有的代碼都需要測試。于是在代碼中的滿足上述定義,并且對獨立的工作單元進行測試的行為,就是我們討論的單元測試。
?
-
優(yōu)秀單元測試的特性
單元測試是非常有威力的魔法,但是如果使用不當(dāng)也會浪費你大量的時間,從而對項目造成巨大的不利影響。另一方面,如果沒有恰當(dāng)?shù)木帉懞蛯崿F(xiàn)單元測試,在維護和調(diào)用這些測試上面,也會很容易的浪費很多時間,從而影響產(chǎn)品代碼和整個項目。
我們不能讓這種情況出現(xiàn)。請切記,做單元測試的首要原因是為了工作更加輕松。現(xiàn)在我們一起探討下如何編寫優(yōu)秀的單元測試,只有如此,方可正確的開展單元測試,提升項目的生產(chǎn)力。
根據(jù)上一小節(jié)的內(nèi)容,首先我們列出一些優(yōu)秀的單元測試大多具備的特點:
- 自動的、可重復(fù)的執(zhí)行的測試
- 開發(fā)人員比較容易實現(xiàn)編寫的測試
- 一旦寫好,將來任何時間都依舊可以用
- 團隊的任何人都可運行的測試
- 一般情況下單擊一個按鈕就可以運行
- 測試可以可以快速的運行
- ……
或許還有更多的情形,我們可以再接再厲的思考出更多的場景。總結(jié)這些,我們可以得到一些基本的應(yīng)該遵循的簡單原則,它們能夠讓不好的單元測試遠離你的項目。這個原則定義了一個優(yōu)秀的測試應(yīng)該具備的品質(zhì),合稱為A-TRIP:
- 自動化(Automatic)
- 徹底的(Thorough)
- 可重復(fù)(Repeatable)
- 獨立的(Independent)
- 專業(yè)的(Professional)
接下來,我們分別就每一個標(biāo)準(zhǔn)進行分析和解釋,從而我們可以正確的理解這些。
-
A
-TRIP 自動化(Automatic)單元測試需要能夠自動的運行。這里包含了兩個層面:調(diào)用測試的自動化以及結(jié)果檢查的自動化。
- 調(diào)用測試的自動化:代碼首先需要能夠正確的被調(diào)用,并且所有的測試可以有選擇的依次執(zhí)行。在一些時候,我們選擇IDE(Integration Development Environment,集成開發(fā)環(huán)境)可以幫助我們自動的運行我們指定的測試,當(dāng)然也可以考慮CI(Continuous Integration,持續(xù)集成)的方式進行自動化執(zhí)行測試。
- 結(jié)果檢查的自動化:測試結(jié)果必須在測試的執(zhí)行以后,“自己”告訴“自己”并展示出來。如果一個項目需要通過雇傭一個人來讀取測試的輸出,然后驗證代碼是否能夠正常的工作,那么這是一種可能導(dǎo)致項目失敗的做法。而且一致性回歸的一個重要特征就是能夠讓測試自己檢查自身是否通過了驗證,人類對這些重復(fù)性的手工行為也是非常不擅長。
-
A-
T
RIP 徹底的(Thorough)好的單元測試應(yīng)該是徹底的,它們測試了所有可能會出現(xiàn)問題的情況。一個極端是每行代碼、代碼可能每一個分支、每一個可能拋出的異常等等,都作為測試對象。另一個極端是僅僅測試最可能的情形——邊界條件、殘缺和畸形的數(shù)據(jù)等等。事實上這是一個項目層面的決策問題。
另外請注意:Bug往往集中的出現(xiàn)在代碼的某塊區(qū)域中,而不是均勻的分布在代碼的每塊區(qū)域中的。對于這種現(xiàn)象,業(yè)內(nèi)引出了一個著名的戰(zhàn)斗口號“不要修修補補,完全重寫!”。一般情況下,完全拋棄一塊Bug很多的代碼塊,并進行重寫會令開銷更小,痛苦更少。
總之,單元測試越多,代碼問題越少。
-
A-T
R
IP 可重復(fù)(Repeatable)每一個測試必須可以重復(fù)的,多次執(zhí)行,并且結(jié)果只能有一個。這樣說明,測試的目標(biāo)只有一個,就是測試應(yīng)該能夠以任意的的順序一次又一次的執(zhí)行,并且產(chǎn)生相同的結(jié)果。意味著,測試不能依賴不受控制的任何外部因素。這個話題引出了“測試替身”的概念,必要的時候,需要用測試替身來隔離所有的外界因素。
如果每次測試執(zhí)行不能產(chǎn)生相同的結(jié)果,那么真相只有一個:代碼中有真正的Bug。
-
A-TR
I
P 獨立的(Independent)測試應(yīng)該是簡潔而且精煉的,這意味著每個測試都應(yīng)該有強的針對性,并且獨立于其它測試和環(huán)境。請記住,這些測試,可能在同一時間點,被多個開發(fā)人員運行。那么在編寫測試的時候,確保一次只測試了一樣?xùn)|西。
獨立的,意味著你可以在任何時間以任何順序運行任何測試。每一個測試都應(yīng)該是一個孤島。
-
A-TRI
P
專業(yè)的(Professional)測試代碼需要是專業(yè)的。意味著,在多次編寫測試的時候,需要注意抽取相同的代碼邏輯,進行封裝設(shè)計。這樣的做法是可行的,而且需要得到鼓勵。
測試代碼,是真實的代碼。在必要的時候,需要創(chuàng)建一個框架進行測試。測試的代碼應(yīng)該和產(chǎn)品的代碼量大體相當(dāng)。所以測試代碼需要保持專業(yè),有良好的設(shè)計。
?
-
生產(chǎn)力的因素
這里我們討論生產(chǎn)力的問題。
當(dāng)單元測試越來越多的時候,團隊的測試覆蓋率會快速的提高,不用再花費時間修復(fù)過去的錯誤,待修復(fù)缺陷的總數(shù)在下降。測試開始清晰可見的影響團隊工作的質(zhì)量。但是當(dāng)測試覆蓋率不斷提高的時候,我們是否要追求100%的測試覆蓋率呢?
事實上,那些確實的測試,不會給團隊帶來更多價值,花費更多精力來編寫測試不會帶來額外的收益。很多測試未覆蓋到的代碼,在項目中事實上也沒有用到。何必測試那些空的方法呢?同時,100%的覆蓋率并不能確保沒有缺陷——它只能保證你所有的代碼都執(zhí)行了,不論程序的行為是否滿足要求,與其追求代碼覆蓋率,不如將重點關(guān)注在確保寫出有意義的測試。
當(dāng)團隊已經(jīng)達到穩(wěn)定水平——曲線的平坦部分顯示出額外投資的收益遞減。測試越多,額外測試的價值越少。第一個測試最有可能是針對代碼最重要的區(qū)域,因此帶來高價值與高風(fēng)險。當(dāng)我們?yōu)閹缀跛惺虑榫帉憸y試后,那些仍然沒有測試覆蓋的地方,很可能是最不重要和最不可能破壞的。
接下來分析一個測試因素影響的圖:
編排.png事實上,大多數(shù)代碼將測試作為質(zhì)量工具,沿著曲線停滯了。從這里看,我們需要找出影響程序員生產(chǎn)力的因素。本質(zhì)上,測試代碼的重復(fù)和多余的復(fù)雜性會降低生產(chǎn)力,抵消測試帶來的正面影響。最直接的兩個影響生產(chǎn)力的因素:
反饋環(huán)長度
和調(diào)試
。這兩者是在鍵盤上消耗程序員時間的罪魁禍?zhǔn)住H绻阱e誤發(fā)生后迅速學(xué)習(xí),那么花在調(diào)試上的時間是可以大幅避免的返工——同時,反饋環(huán)越長,花在調(diào)試上的時間越多。等待對變更進行確認和驗證,在很大程度上牽扯到測試執(zhí)行的速度,這個是上述強調(diào)的反饋環(huán)長度和調(diào)試時間的根本原因之一。另外三個根本原因會影響程序員的調(diào)試量。
- 測試的可讀性:缺乏可讀性自然降低分析的熟讀,并且鼓勵程序員打開調(diào)試器,因為閱讀代碼不會讓你明白。同時因為很難看出錯誤的所在,還會引入更多的缺陷。
- 測試結(jié)果的準(zhǔn)確度:準(zhǔn)確度是一個基本要求。
- 可依賴性和可靠性:可靠并且重復(fù)的方式運行測試,提供結(jié)果是另一個基本要求。
?
-
設(shè)計潛力的曲線
假設(shè)先寫了最重要的測試——針對最常見和基本的場景,以及軟件架構(gòu)中的關(guān)鍵部位。那么測試質(zhì)量很高,我們可以講重復(fù)的代碼都重構(gòu)掉,并且保持測試精益和可維護。那么我們想象一下,積累了如此高的測試覆蓋率以后,唯一沒測試到的地方,只能是那些最不重要和最不可能破壞的,項目沒有運行到的地方了。平心而論,那么地方也是沒有什么價值的地方,那么,之前的做法傾向于收益遞減——已經(jīng)不能再從編寫測試這樣的事情中獲取價值了。
這是由于不做的事情而造成的質(zhì)量穩(wěn)態(tài)。之所以這么說,是因為想要到達更高的生產(chǎn)力,我們需要換個思路去考慮測試。為了找回丟掉的潛力,我們需要從編寫測試中找到完全不同的價值——價值來自于創(chuàng)新及設(shè)計導(dǎo)向,而并非防止回歸缺陷的保護及驗證導(dǎo)向。
總而言之,為了充分和完全的發(fā)揮測試的潛力,我們需要:
- 像生產(chǎn)代碼一樣對待你測試代碼——大膽重構(gòu)、創(chuàng)建和維護高質(zhì)量測試
- 開始將測試作為一種設(shè)計工具,指導(dǎo)代碼針對實際用途進行設(shè)計。
第一種方法,是我們在這篇講義中討論的重點。多數(shù)程序員在編寫測試的時候會不知所措,無法顧及高質(zhì)量,或者降低編寫、維護、運行測試的成本。
第二種方法,是討論利用測試作為設(shè)計的方面,我們的目的是對這種動態(tài)和工作方式有個全面的了解,在接下來的[探討]中我們繼續(xù)分析這個話題。
?
1.2 [探討]正確地認識單元測試
-
練習(xí):一個簡單的單元測試示例
我們從一個簡單的例子開始設(shè)計測試,它是一個獨立的方法,用來查找list中的最大值。
int getLargestElement(int[] list){ // TODO: find largest element from list and return it. }
比如,給定一個數(shù)組 { 1, 50, 81, 100 },這個方法應(yīng)該返回100,這樣就構(gòu)成了一個很合理測試。那么,我們還能想出一些別的測試么?就這樣的方法,在繼續(xù)閱讀之前,請認真的思考一分鐘,記下來所有能想到的測試。
在繼續(xù)閱讀之前,請靜靜的思考一會兒……
想到了多少測試呢?請將想到的測試都在紙上寫出來。格式如下:
- 50, 60, 7, 58, 98 --> 98
- 100, 90, 25 --> 100
- ……
然后我們編寫一個基本的符合要求的函數(shù),來繼續(xù)進行測試。
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; }
然后請考慮上述代碼是否有問題,可以用什么樣的例子來進行測試。
?
-
分析:為什么不寫單元測試
請思考當(dāng)前在組織或者項目中,如何寫單元測試,是否有不寫單元測試的習(xí)慣和借口,這些分別是什么?
?
-
分析:單元測試的結(jié)構(gòu)與內(nèi)容
當(dāng)我們確定要寫單元測試的時候,請認真分析,一個單元測試包含什么樣的內(nèi)容,為什么?
?
-
分析:單元測試的必要性
請分析單元測試必要性,嘗試得出單元測試所帶來的好處。
單元測試的主要目的,就是驗證應(yīng)用程序是否可以按照預(yù)期的方式正常運行,以及盡早的發(fā)現(xiàn)錯誤。盡管功能測試也可以做到這一點,但是單元測試更加強大,并且用戶更加豐富,它能做的不僅僅是驗證應(yīng)用程序的正常運行,單元測試還可以做到更多。
-
帶來更高的測試覆蓋率
功能測試大約可以覆蓋到70%的應(yīng)用程序代碼,如果希望進行的更加深入一點,提供更高的測試覆蓋率,那么我們需要編寫單元測試了。單元測試可以很容易的模擬錯誤條件,這一點在功能測試中卻很難辦到,有些情況下甚至是不可能辦到的。單元測試不僅提供了測試,還提供了更多的其它用途,在最后一部分我們將會繼續(xù)介紹。
-
提高團隊效率
在一個項目中,經(jīng)過單元測試通過的代碼,可以稱為高質(zhì)量的代碼。這些代碼無需等待到其它所有的組件都完成以后再提交,而是可以隨時提交,提高的團隊的效率。如果不進行單元測試,那么測試行為大多數(shù)要等到所有的組件都完成以后,整個應(yīng)用程序可以運行以后,才能進行,嚴(yán)重影響了團隊效率。
-
自信的重構(gòu)和改進實現(xiàn)
在沒有進行單元測試的代碼中,重構(gòu)是有著巨大風(fēng)險的行為。因為你總是可能會損壞一些東西。而單元測試提供了一個安全網(wǎng),可以為重構(gòu)的行為提供信心。同時在良好的單元測試基礎(chǔ)上,對代碼進行改進實現(xiàn),對一些修改代碼,增加新的特性或者功能的行為,有單元測試作為保障,可以防止在改進的基礎(chǔ)上,引入新的Bug。
-
將預(yù)期的行為文檔化
在一些代碼的文檔中,示例的威力是眾所周知的。當(dāng)完成一個生產(chǎn)代碼的時候,往往要生成或者編寫對應(yīng)的API文檔。而如果在這些代碼中進行了完整的單元測試,則這些單元測試就是最好的實例。它們展示了如何使用這些API,也正是因為如此,它們就是完美的開發(fā)者文檔,同時因為單元測試必須與工作代碼保持同步,所以比起其它形式的文檔,單元測試必須始終是最新的,最有效的。
?
-
1.3 用 JUnit 進行單元測試
JUnit誕生于1997年,Erich Gamma 和 Kent Beck 針對 Java 創(chuàng)建了一個簡單但是有效的單元測試框架,隨后迅速的成為 Java 中開發(fā)單元測試的事實上的標(biāo)準(zhǔn)框架,被稱為 xUnit 的相關(guān)測試框架,正在逐漸成為任何語言的標(biāo)準(zhǔn)框架。
以我們的角度,JUnit用來“確保方法接受預(yù)期范圍內(nèi)的輸入,并且為每一次測試輸入返回預(yù)期的值”。在這一節(jié)里,我們從零開始介紹如何為一個簡單的類創(chuàng)建單元測試。我們首先編寫一個測試,以及運行該測試的最小框架,以便能夠理解單元測試是如何處理的。然后我們在通過 JUnit 展示正確的工具可以如何使生活變得更加簡單。
本文中使用 JUnit 4 最新版進行單元測試的示例與講解。
JUnit 4 用到了許多 Java 5 中的特性,如注解。JUnit 4 需要使用 Java 5 或者更高的版本。
-
用 JUnit 構(gòu)建單元測試
這里我們開始構(gòu)建單元測試。
首先我們使用之前一節(jié)的【探討】中使用過的類,作為被測試的對象。創(chuàng)建一個類,叫做
HelloWorld
,該類中有一個方法,可以從輸入的一個整型數(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[])方法的意圖顯然是接受一個整型的數(shù)組,并且以 int 的類型,返回該數(shù)組中最大的值。編譯器能夠告訴我們,它通過了編譯,但是我們也應(yīng)該確保它在運行期間可以正常的工作。
單元測試的核心原則是“任何沒有經(jīng)過自動測試的程序功能都可以當(dāng)做它不存在”。getLargestElement 方法代表了 HelloWorld 類的一個核心功能,我們擁有了一些實現(xiàn)該功能的代碼,現(xiàn)在缺少的只是一個證明實現(xiàn)能夠正常工作的自動測試。
這個時候,進行任何測試看起來都會有些困難,畢竟我們甚至沒有可以輸入一個數(shù)組的值的用戶界面。除非我們使用在【探討】中使用的類進行測試。
示例代碼:
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("獲取最大值錯誤,期望的結(jié)果是 100;實際錯誤的結(jié)果: " + result); } else { System.out.println("獲取最大值正確,通過測試。"); } } }
輸出結(jié)果如下:
獲取最大值正確,通過測試。 Process finished with exit code 0
第一個
HelloWorldTest
類非常簡單。它創(chuàng)建了 HelloWorld 的一個實例,傳遞給它一個數(shù)組,并且檢查運行的結(jié)果。如果運行結(jié)果與我們預(yù)期的不一致,那么我們就在標(biāo)準(zhǔn)輸出設(shè)備上輸出一條消息。現(xiàn)在我們編譯并且運行這個程序,那么測試將會正常通過,同時一切看上去都非常順利。可是事實上并非都是如此圓滿,如果我們修改部分測試,再次運行,可能會遇到不通過測試的情況,甚至代碼異常。
接下來我們修改代碼如下:
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("獲取最大值錯誤,期望的結(jié)果是 100;實際錯誤的結(jié)果: " + result); } else { System.out.println("獲取最大值正確,通過測試。"); } } }
當(dāng)我們再次執(zhí)行代碼的時候,代碼運行就會報錯。運行結(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)秀的單元測試,因為測試連運行都無法運行。令人高興的是,JUnit 團隊解決了上述麻煩。JUnit 框架支持自我檢測,并逐個報告每個測試的所有錯誤和結(jié)果。接下來我們來進一步了解 JUnit 。
JUnit 是一個單元測試框架,在設(shè)計之初,JUnit 團隊已經(jīng)為框架定義了3個不相關(guān)的目標(biāo):
- 框架必須幫助我們編寫有用的測試
- 框架必須幫助我們創(chuàng)建具有長久價值的測試
- 框架必須幫助我們通過復(fù)用代碼來降低編寫測試的成本
首先安裝 JUnit 。這里我們使用原始的方式添加 JAR 文件到 ClassPath 中。
下載地址:https://github.com/junit-team/junit4/wiki/Download-and-Install,下載如下兩個 JAR 包,放到項目的依賴的路徑中。
junit.jar
hamcrest-core.jar
在 IDEA 的項目中,添加一個文件夾 lib,將上述兩個文件添加到 lib 中。
然后 File | Project Structure | Modules,打開 Modules 對話框,選擇右邊的 Dependencies 的選項卡,點擊右邊的 + 號,選擇 “1 JARs or directories”并找到剛剛添加的兩個 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("獲取最大值錯誤! ", 100, result); } @Test public void test02GetLargestElement(){ HelloWorld hello = new HelloWorld(); int[] listToTest = {-10, 20, -100, 90}; int result = hello.getLargestElement(listToTest); Assert.assertEquals("獲取最大值錯誤! ", 90, result); } }
如上的操作,我們便定義了一個單元測試,使用 JUnit 編寫了測試。主要的要點如下:
- 針對每個測試的對象類,單獨編寫測試類,測試方法,避免副作用
- 定義一個測試類
- 使用 JUnit 的注解方式提供的方法: @Test
- 使用 JUnit 提供的方法進行斷言:Assert.assertEquals(String msg, long expected, long actual)
- 創(chuàng)建一個測試方法的要求:該方法必須是公共的,不帶任何參數(shù),返回值類型為void,同時必須使用@Test注解
-
JUnit 的各種斷言
為了進行驗測試驗證,我們使用了由 JUnit 的
Assert
類提供的assert
方法。正如我們在上面的例子中使用的那樣,我們在測試類中靜態(tài)的導(dǎo)入這些方法,同時還有更多的方法以供我們使用,如下我們列出一些流行的 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)
用匹配器進行斷言,高級應(yīng)用*,不再此文檔討論 一般來說,一個測試方法包括了多個斷言。當(dāng)其中一個斷言失敗的時候,整個測試方法將會被終止——從而導(dǎo)致該方法中剩下的斷言將會無法執(zhí)行了。此時,不能有別的想法,只能先修復(fù)當(dāng)前失敗的斷言,以此類推,不斷地修復(fù)當(dāng)前失敗的斷言,通過一個個測試,慢慢前行。
-
JUnit 的框架
到目前為止,我們只是介紹了斷言本身,很顯然我們不能只是簡單的把斷言方法寫完,就希望測試可以運行起來。我們需要一個框架來輔助完成這些,那么我們就要做多一些工作了。很幸運的是,我們不用多做太多。
在 JUnit 4 提供了
@Before
和@After
,在每個測試函數(shù)調(diào)用之前/后都會調(diào)用。-
@Before
: Method annotated with@Before
executes before every test. 每個測試方法開始前執(zhí)行的方法 -
@After
: Method annotated with@After
executes after every test. 每個測試方法執(zhí)行后再執(zhí)行的方法
如果在測試之前有些工作我們只想做一次,用不著每個函數(shù)之前都做一次。比如讀一個很大的文件。那就用下面兩個來標(biāo)注:
@BeforeClass
: 測試類初始化的時候,執(zhí)行的方法
@AfterClass
: 測試類銷毀的時候,執(zhí)行的方法注意:
-
@Before
/@After
可以執(zhí)行多次;@BeforeClass
/@AfterClass
只能執(zhí)行一次 - 如果我們預(yù)計有Exception,那就給@Test加參數(shù):
@Test(expected = XXXException.class)
- 如果出現(xiàn)死循環(huán)怎么辦?這時timeout參數(shù)就有用了:
@Test(timeout = 1000)
- 如果我們暫時不用測試一個用例,我們不需要刪除或都注釋掉。只要改成:
@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 需要注意的事項:
- 每個
@Test
都是一個測試用例,一個類可以寫多個@Test
- 每個
@Test
執(zhí)行之前 都會執(zhí)行 @Before,執(zhí)行之后都會運行@After
- 每個
@Test
,@After
,@Before
都必須是public void
, 參數(shù)為空 -
@After
/@Before
也可以是多個,并且有執(zhí)行順序。在每個@Test
前后執(zhí)行多次。-
@Before
多個名字長度一致,z -> a
, 長度不一致,會先執(zhí)行名字短的。 -
@After
/@Test
多個名字長度一致,a -> z
, 長度不一致,會后執(zhí)行名字短的。
-
-
@AfterClass
/@BeforeClass
也可以是多個,并且有執(zhí)行順序。只會在測試類的實例化前后各執(zhí)行一次。-
@BeforeClass
多個名字長度一致,z -> a
, 長度不一致,會先執(zhí)行名字短的。 -
@AfterClass
多個名字長度一致,a -> z
, 長度不一致,會后執(zhí)行名字短的。
-
-
@AfterClass
/@BeforeClass
都必須是public static void,
參數(shù)為空 - 測試結(jié)果有 通過、不通過和錯誤 三種。
-
-
JUnit 的測試運行
這一小節(jié),我們來介紹一下 JUnit 4 中的新的測試運行器(Test Runner)。如果我們剛開始編寫測試,那么我們需要盡可能快捷的運行這些測試,這樣我們才能夠?qū)y試融合到開發(fā)循環(huán)中去。
編碼 → 運行 → 測試 → 編碼……
其中,JUnit 就可以讓我們構(gòu)建和運行測試。我們可以按照組合測試Suite 以及參數(shù)化測試分別來運行測試。
-
組合測試Suite
測試集 (Suite 或者 test suite)一組測試。測試集是一種把多個相關(guān)測試歸入一組的便捷測試方式。可以在一個測試集中,定義需要打包測試的類,并一次性運行所有包含的測試;也可以分別定義多個測試集,然后在一個主測試集中運行多個相關(guān)的測試集,打包相關(guān)的測試的類,并一次性運行所有包含的測試。
示例代碼如下:
@RunWith(value = Suite.class) @Suite.SuiteClasses(value = HelloWorldTests.class) public class HelloWorldTestRunner { }
?
-
參數(shù)化測試
參數(shù)化測試(Parameterized)是測試運行器允許使用不同的參數(shù)多次運行同一個測試。參數(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("獲取最大元素錯誤!", expected, new HelloWorld().getLargestElement(listToTest)); } }
對于參數(shù)化測試的運行器來運行測試類,那么必須滿足以下要求:
- 測試類必須使用
@RunWith(value = Parameterized.class)
注解 - 必須聲明測試中所使用的實例變量
- 提供一個用
@Parameterized.Parameters
的注解方法,這里用的是getTestParameters()
,同時此方法的簽名必須是public static Collection
- 為測試指定構(gòu)造方法,或者一個個全局變量的成員進行賦值
- 所有的測試方法以
@Test
注解,實例化被測試的程序,同時在斷言中使用我們提供的全局變量作為參數(shù)
?
- 測試類必須使用
-
1.4 [探討]按業(yè)務(wù)價值導(dǎo)向進行單元測試設(shè)計
-
練習(xí):測試的結(jié)果是否正確
如果測試代碼能夠運行正確,我們要怎么才能知道它是正確的呢?
如何應(yīng)對測試數(shù)據(jù)量比較大的時候,我們的測試代碼如何編寫?
?
-
練習(xí):測試的邊界條件
尋找邊界條件是單元測試中最有價值的工作之一,一般來說Bug出現(xiàn)在邊界上的概率比較大。那么我們都需要考慮什么樣的邊界條件呢?
?
-
練習(xí):強制產(chǎn)生錯誤條件
關(guān)于產(chǎn)生錯誤的條件,請列出一個詳細的清單來。
?
-
分析:測試作為設(shè)計工具
第一節(jié)【專題】中,我們有討論設(shè)計潛力的曲線,其中第二條方案強調(diào)了測試作為設(shè)計的工具。那么我們想就兩個方面來討論這個測試設(shè)計的問題。
- TDD,測試驅(qū)動開發(fā)
- BDD,行為驅(qū)動開發(fā)
?