單元測試之旅:預見優秀之①——優秀基因

單元測試之旅:預見優秀之①——優秀基因

來自專欄軟體技術預見6 人贊了文章

1. 單元測試入門——優秀基因

單元測試最初興起于敏捷社區。1997年,設計模式四巨頭之一Erich Gamma和極限編程發明人Kent Beck共同開發了JUnit,而JUnit框架在此之後又引領了xUnit家族的發展,深刻的影響著單元測試在各種編程語言中的普及。當前,單元測試也成了敏捷開發流行以來的現代軟體開發中必不可少的工具之一。同時,越來越多的互聯網行業推崇自動化測試的概念,作為自動化測試的重要組成部分,單元測試是一種經濟合理的回歸測試手段,在當前敏捷開發的迭代(Sprint)中非常流行和需要。

然而有些時候,這些單元測試並沒有有效的改善生產力,甚至單元測試有時候變成一種負擔。人們盲目的追求測試覆蓋率,往往卻忽視了測試代碼本身的質量,各種無效的單元測試反而帶來了沉重的維護負擔。

本篇講義將會集中的從單元測試的入門、優秀單元測試的編寫以及單元測試的實踐等三個方面展開探討。

文中的相關約定:

文中的示例代碼塊均使用Java語言。

文中的粗體部分表示重點內容和重點提示。

文中的引用框部分,一般是定義或者來源於其它地方。

文中標題的【探討】,表示此部分講師與學員共同探討並由講師引導,得到方案。

文中的代碼變數和說明用方框圈起來的,是相關代碼的變數、方法、異常等。

1.1 單元測試的價值

  • 什麼是單元測試

    在維基百科中,單元測試被定義為一段代碼調用另一段代碼,隨後檢驗一些假設的正確性。

    以上是對單元測試的傳統定義,儘管從技術上說是正確的,但是它很難使我們成為更加優秀的程序員。這些定義在諸多討論單元測試的書籍和網站上,我們總能看到,可能你已經厭倦,覺得是老生常談。不過不必擔心,正是從這個我們熟悉的,共同的出發點,我們引申出單元測試的概念。

    或許很多人將軟體測試行為與單元測試的概念混淆為一談。在正式開始考慮單元測試的定義之前,請先思考下面的問題,回顧以前遇到的或者所寫的測試:

    • 兩周或者兩個月、甚至半年、一年、兩年前寫的單元測試,現在還可以運行並得到結果么?
    • 兩個月前寫的單元測試,任何一個團隊成員都可以運行並且得到結果么?
    • 是否可以在數分鐘以內跑完所有的單元測試呢?
    • 可以通過單擊一個按鈕就能運行所寫的單元測試么?
    • 能否在數分鐘內寫一個基本的單元測試呢?

當我們能夠對上述的問題,全部回答「是」的時候,我們便可以定義單元測試的概念了。優秀的測試應該以其本來的、非手工的形式輕鬆執行。同時,這樣的測試應該是任何人都可以使用,任何人都可以運行的。在這個前提下,測試的運行應該能夠足夠快,運行起來不費力、不費事、不費時,並且即便寫新的測試,也應該能夠順利、不耗時的完成。如上便是我們需要的單元測試。

涵蓋上面描述的要求的情況下,我們可以提出比較徹底的單元測試的定義:

單元測試(Unit Test),是一段自動化的代碼,用來調動被測試的方法,而後驗證基於該方法或類的邏輯行為的一些假設。單元測試幾乎總是用單元測試框架來寫的。它寫起來很順手,運行起來不費時。它是全自動的、可信賴的、可讀性強的和可維護的。

接下來我們首先討論單元測試框架的概念:

框架是一個應用程序的半成品。框架提供了一個可復用的公共結構,程序員可以在多個應用程序之間進行共享該結構,並且可以加以擴展以便滿足它們的特定的要求。

單元測試檢查一個獨立工作單元的行為,在Java程序中,一個獨立工作單元經常是一個獨立的方法,同時就是一項單一的任務,不直接依賴於其它任何任務的完成。

所有的代碼都需要測試。於是在代碼中的滿足上述定義,並且對獨立的工作單元進行測試的行為,就是我們討論的單元測試。

?

  • 優秀單元測試的特性

    單元測試是非常有威力的魔法,但是如果使用不當也會浪費你大量的時間,從而對項目造成巨大的不利影響。另一方面,如果沒有恰當的編寫和實現單元測試,在維護和調用這些測試上面,也會很容易的浪費很多時間,從而影響產品代碼和整個項目。

    我們不能讓這種情況出現。請切記,做單元測試的首要原因是為了工作更加輕鬆。現在我們一起探討下如何編寫優秀的單元測試,只有如此,方可正確的開展單元測試,提升項目的生產力。

    根據上一小節的內容,首先我們列出一些優秀的單元測試大多具備的特點:

自動的、可重複的執行的測試

開發人員比較容易實現編寫的測試

一旦寫好,將來任何時間都依舊可以用

團隊的任何人都可運行的測試

一般情況下單擊一個按鈕就可以運行

測試可以可以快速的運行

……

或許還有更多的情形,我們可以再接再厲的思考出更多的場景。總結這些,我們可以得到一些基本的應該遵循的簡單原則,它們能夠讓不好的單元測試遠離你的項目。這個原則定義了一個優秀的測試應該具備的品質,合稱為A-TRIP

自動化(Automatic)

徹底的(Thorough)

可重複(Repeatable)

獨立的(Independent)

專業的(Professional)

接下來,我們分別就每一個標準進行分析和解釋,從而我們可以正確的理解這些。

    • A-TRIP 自動化(Automatic)

      單元測試需要能夠自動的運行。這裡包含了兩個層面:調用測試的自動化以及結果檢查的自動化。

  1. 調用測試的自動化:代碼首先需要能夠正確的被調用,並且所有的測試可以有選擇的依次執行。在一些時候,我們選擇IDE(Integration Development Environment,集成開發環境)可以幫助我們自動的運行我們指定的測試,當然也可以考慮CI(Continuous Integration,持續集成)的方式進行自動化執行測試。
  2. 結果檢查的自動化:測試結果必須在測試的執行以後,「自己」告訴「自己」並展示出來。如果一個項目需要通過僱傭一個人來讀取測試的輸出,然後驗證代碼是否能夠正常的工作,那麼這是一種可能導致項目失敗的做法。而且一致性回歸的一個重要特徵就是能夠讓測試自己檢查自身是否通過了驗證,人類對這些重複性的手工行為也是非常不擅長。
    • A-TRIP 徹底的(Thorough)

      好的單元測試應該是徹底的,它們測試了所有可能會出現問題的情況。一個極端是每行代碼、代碼可能每一個分支、每一個可能拋出的異常等等,都作為測試對象。另一個極端是僅僅測試最可能的情形——邊界條件、殘缺和畸形的數據等等。事實上這是一個項目層面的決策問題。

      另外請注意:Bug往往集中的出現在代碼的某塊區域中,而不是均勻的分布在代碼的每塊區域中的。對於這種現象,業內引出了一個著名的戰鬥口號「不要修修補補,完全重寫!」。一般情況下,完全拋棄一塊Bug很多的代碼塊,並進行重寫會令開銷更小,痛苦更少。

      總之,單元測試越多,代碼問題越少。
    • A-TRIP 可重複(Repeatable)

      每一個測試必須可以重複的,多次執行,並且結果只能有一個。這樣說明,測試的目標只有一個,就是測試應該能夠以任意的的順序一次又一次的執行,並且產生相同的結果。意味著,測試不能依賴不受控制的任何外部因素。這個話題引出了「測試替身」的概念,必要的時候,需要用測試替身來隔離所有的外界因素。

      如果每次測試執行不能產生相同的結果,那麼真相只有一個:代碼中有真正的Bug。
    • A-TRIP 獨立的(Independent)

      測試應該是簡潔而且精鍊的,這意味著每個測試都應該有強的針對性,並且獨立於其它測試和環境。請記住,這些測試,可能在同一時間點,被多個開發人員運行。那麼在編寫測試的時候,確保一次只測試了一樣東西。

      獨立的,意味著你可以在任何時間以任何順序運行任何測試。每一個測試都應該是一個孤島。
    • A-TRIP 專業的(Professional)

      測試代碼需要是專業的。意味著,在多次編寫測試的時候,需要注意抽取相同的代碼邏輯,進行封裝設計。這樣的做法是可行的,而且需要得到鼓勵。

      測試代碼,是真實的代碼。在必要的時候,需要創建一個框架進行測試。測試的代碼應該和產品的代碼量大體相當。所以測試代碼需要保持專業,有良好的設計。

?

  • 生產力的因素

    這裡我們討論生產力的問題。

    當單元測試越來越多的時候,團隊的測試覆蓋率會快速的提高,不用再花費時間修復過去的錯誤,待修復缺陷的總數在下降。測試開始清晰可見的影響團隊工作的質量。但是當測試覆蓋率不斷提高的時候,我們是否要追求100%的測試覆蓋率呢?

    事實上,那些確實的測試,不會給團隊帶來更多價值,花費更多精力來編寫測試不會帶來額外的收益。很多測試未覆蓋到的代碼,在項目中事實上也沒有用到。何必測試那些空的方法呢?同時,100%的覆蓋率並不能確保沒有缺陷——它只能保證你所有的代碼都執行了,不論程序的行為是否滿足要求,與其追求代碼覆蓋率,不如將重點關注在確保寫出有意義的測試。

    當團隊已經達到穩定水平——曲線的平坦部分顯示出額外投資的收益遞減。測試越多,額外測試的價值越少。第一個測試最有可能是針對代碼最重要的區域,因此帶來高價值與高風險。當我們為幾乎所有事情編寫測試後,那些仍然沒有測試覆蓋的地方,很可能是最不重要和最不可能破壞的。

    接下來分析一個測試因素影響的圖:

測試因素影響

事實上,大多數代碼將測試作為質量工具,沿著曲線停滯了。從這裡看,我們需要找出影響程序員生產力的因素。本質上,測試代碼的重複和多餘的複雜性會降低生產力,抵消測試帶來的正面影響。最直接的兩個影響生產力的因素:反饋環長度調試。這兩者是在鍵盤上消耗程序員時間的罪魁禍首。如果在錯誤發生後迅速學習,那麼花在調試上的時間是可以大幅避免的返工——同時,反饋環越長,花在調試上的時間越多。

等待對變更進行確認和驗證,在很大程度上牽扯到測試執行的速度,這個是上述強調的反饋環長度和調試時間的根本原因之一。另外三個根本原因會影響程序員的調試量。

    1. 測試的可讀性:缺乏可讀性自然降低分析的熟讀,並且鼓勵程序員打開調試器,因為閱讀代碼不會讓你明白。同時因為很難看出錯誤的所在,還會引入更多的缺陷。
    2. 測試結果的準確度:準確度是一個基本要求。
    3. 可依賴性和可靠性:可靠並且重複的方式運行測試,提供結果是另一個基本要求。

?

  • 設計潛力的曲線

    假設先寫了最重要的測試——針對最常見和基本的場景,以及軟體架構中的關鍵部位。那麼測試質量很高,我們可以講重複的代碼都重構掉,並且保持測試精益和可維護。那麼我們想像一下,積累了如此高的測試覆蓋率以後,唯一沒測試到的地方,只能是那些最不重要和最不可能破壞的,項目沒有運行到的地方了。平心而論,那麼地方也是沒有什麼價值的地方,那麼,之前的做法傾向於收益遞減——已經不能再從編寫測試這樣的事情中獲取價值了。

    這是由於不做的事情而造成的質量穩態。之所以這麼說,是因為想要到達更高的生產力,我們需要換個思路去考慮測試。為了找回丟掉的潛力,我們需要從編寫測試中找到完全不同的價值——價值來自於創新及設計導向,而並非防止回歸缺陷的保護及驗證導向。

    總而言之,為了充分和完全的發揮測試的潛力,我們需要:

  1. 像生產代碼一樣對待你測試代碼——大膽重構、創建和維護高質量測試
  2. 開始將測試作為一種設計工具,指導代碼針對實際用途進行設計。

第一種方法,是我們在這篇講義中討論的重點。多數程序員在編寫測試的時候會不知所措,無法顧及高質量,或者降低編寫、維護、運行測試的成本。

第二種方法,是討論利用測試作為設計的方面,我們的目的是對這種動態和工作方式有個全面的了解,在接下來的[探討]中我們繼續分析這個話題。

?

1.2 [探討]正確地認識單元測試

  • 練習:一個簡單的單元測試示例

    我們從一個簡單的例子開始設計測試,它是一個獨立的方法,用來查找list中的最大值。

int getLargestElement(int[] list){ // TODO: find largest element from list and return it. }

    • 比如,給定一個數組 {1, 50, 81, 100},這個方法應該返回100,這樣就構成了一個很合理測試。那麼,我們還能想出一些別的測試么?就這樣的方法,在繼續閱讀之前,請認真的思考一分鐘,記下來所有能想到的測試。 在繼續閱讀之前,請靜靜的思考一會兒…… 想到了多少測試呢?請將想到的測試都在紙上寫出來。格式如下:

50, 60, 7, 58, 98 --> 98

100, 90, 25 --> 100

……

然後我們編寫一個基本的符合要求的函數,來繼續進行測試。

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;}

然後請考慮上述代碼是否有問題,可以用什麼樣的例子來進行測試。

?

  • 分析:為什麼不寫單元測試

    請思考當前在組織或者項目中,如何寫單元測試,是否有不寫單元測試的習慣和借口,這些分別是什麼?

    ?
  • 分析:單元測試的結構與內容

    當我們確定要寫單元測試的時候,請認真分析,一個單元測試包含什麼樣的內容,為什麼?

    ?
  • 分析:單元測試的必要性

    請分析單元測試必要性,嘗試得出單元測試所帶來的好處。

    單元測試的主要目的,就是驗證應用程序是否可以按照預期的方式正常運行,以及儘早的發現錯誤。儘管功能測試也可以做到這一點,但是單元測試更加強大,並且用戶更加豐富,它能做的不僅僅是驗證應用程序的正常運行,單元測試還可以做到更多。

    • 帶來更高的測試覆蓋率

      功能測試大約可以覆蓋到70%的應用程序代碼,如果希望進行的更加深入一點,提供更高的測試覆蓋率,那麼我們需要編寫單元測試了。單元測試可以很容易的模擬錯誤條件,這一點在功能測試中卻很難辦到,有些情況下甚至是不可能辦到的。單元測試不僅提供了測試,還提供了更多的其它用途,在最後一部分我們將會繼續介紹。
    • 提高團隊效率

      在一個項目中,經過單元測試通過的代碼,可以稱為高質量的代碼。這些代碼無需等待到其它所有的組件都完成以後再提交,而是可以隨時提交,提高的團隊的效率。如果不進行單元測試,那麼測試行為大多數要等到所有的組件都完成以後,整個應用程序可以運行以後,才能進行,嚴重影響了團隊效率。
    • 自信的重構和改進實現

      在沒有進行單元測試的代碼中,重構是有著巨大風險的行為。因為你總是可能會損壞一些東西。而單元測試提供了一個安全網,可以為重構的行為提供信心。同時在良好的單元測試基礎上,對代碼進行改進實現,對一些修改代碼,增加新的特性或者功能的行為,有單元測試作為保障,可以防止在改進的基礎上,引入新的Bug。
    • 將預期的行為文檔化

      在一些代碼的文檔中,示例的威力是眾所周知的。當完成一個生產代碼的時候,往往要生成或者編寫對應的API文檔。而如果在這些代碼中進行了完整的單元測試,則這些單元測試就是最好的實例。它們展示了如何使用這些API,也正是因為如此,它們就是完美的開發者文檔,同時因為單元測試必須與工作代碼保持同步,所以比起其它形式的文檔,單元測試必須始終是最新的,最有效的。

?

1.3 用 JUnit 進行單元測試

JUnit誕生於1997年,Erich Gamma 和 Kent Beck 針對 Java 創建了一個簡單但是有效的單元測試框架,隨後迅速的成為 Java 中開發單元測試的事實上的標準框架,被稱為 xUnit 的相關測試框架,正在逐漸成為任何語言的標準框架。

以我們的角度,JUnit用來「確保方法接受預期範圍內的輸入,並且為每一次測試輸入返回預期的值」。在這一節里,我們從零開始介紹如何為一個簡單的類創建單元測試。我們首先編寫一個測試,以及運行該測試的最小框架,以便能夠理解單元測試是如何處理的。然後我們在通過 JUnit 展示正確的工具可以如何使生活變得更加簡單。

本文中使用 JUnit 4 最新版進行單元測試的示例與講解。

JUnit 4 用到了許多 Java 5 中的特性,如註解。JUnit 4 需要使用 Java 5 或者更高的版本。

  • 用 JUnit 構建單元測試

    這裡我們開始構建單元測試。

    首先我們使用之前一節的【探討】中使用過的類,作為被測試的對象。創建一個類,叫做HelloWorld,該類中有一個方法,可以從輸入的一個整型數組中,找到最大的值,並且返回該值。

    代碼如下:

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[])方法的意圖顯然是接受一個整型的數組,並且以 int 的類型,返回該數組中最大的值。編譯器能夠告訴我們,它通過了編譯,但是我們也應該確保它在運行期間可以正常的工作。

單元測試的核心原則是「任何沒有經過自動測試的程序功能都可以當做它不存在」。getLargestElement 方法代表了 HelloWorld 類的一個核心功能,我們擁有了一些實現該功能的代碼,現在缺少的只是一個證明實現能夠正常工作的自動測試。

這個時候,進行任何測試看起來都會有些困難,畢竟我們甚至沒有可以輸入一個數組的值的用戶界面。除非我們使用在【探討】中使用的類進行測試。

示例代碼:

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("獲取最大值錯誤,期望的結果是 100;實際錯誤的結果: " + result); } else { System.out.println("獲取最大值正確,通過測試。"); } }}

輸出結果如下:

獲取最大值正確,通過測試。Process finished with exit code 0

第一個 HelloWorldTest 類非常簡單。它創建了 HelloWorld 的一個實例,傳遞給它一個數組,並且檢查運行的結果。如果運行結果與我們預期的不一致,那麼我們就在標準輸出設備上輸出一條消息。

現在我們編譯並且運行這個程序,那麼測試將會正常通過,同時一切看上去都非常順利。可是事實上並非都是如此圓滿,如果我們修改部分測試,再次運行,可能會遇到不通過測試的情況,甚至代碼異常。

接下來我們修改代碼如下:

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("獲取最大值錯誤,期望的結果是 100;實際錯誤的結果: " + result); } else { System.out.println("獲取最大值正確,通過測試。"); } }}

當我們再次執行代碼的時候,代碼運行就會報錯。運行結果如下:

Exception in thread "main" java.lang.NullPointerExceptionat 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

按照第一節中的描述的優秀的單元測試,上述代碼毫無疑問,稱不上優秀的單元測試,因為測試連運行都無法運行。令人高興的是,JUnit 團隊解決了上述麻煩。JUnit 框架支持自我檢測,並逐個報告每個測試的所有錯誤和結果。接下來我們來進一步了解 JUnit 。

JUnit 是一個單元測試框架,在設計之初,JUnit 團隊已經為框架定義了3個不相關的目標:

    • 框架必須幫助我們編寫有用的測試
    • 框架必須幫助我們創建具有長久價值的測試
    • 框架必須幫助我們通過復用代碼來降低編寫測試的成本

首先安裝 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 編寫了測試。主要的要點如下:

    1. 針對每個測試的對象類,單獨編寫測試類,測試方法,避免副作用
    2. 定義一個測試類
    3. 使用 JUnit 的註解方式提供的方法: @Test
    4. 使用 JUnit 提供的方法進行斷言:Assert.assertEquals(String msg, long expected, long actual)
    5. 創建一個測試方法的要求:該方法必須是公共的,不帶任何參數,返回值類型為void,同時必須使用@Test註解
  • JUnit 的各種斷言

    為了進行驗測試驗證,我們使用了由 JUnit 的 Assert 類提供的 assert 方法。正如我們在上面的例子中使用的那樣,我們在測試類中靜態的導入這些方法,同時還有更多的方法以供我們使用,如下我們列出一些流行的 assert 方法。

一般來說,一個測試方法包括了多個斷言。當其中一個斷言失敗的時候,整個測試方法將會被終止——從而導致該方法中剩下的斷言將會無法執行了。此時,不能有別的想法,只能先修復當前失敗的斷言,以此類推,不斷地修復當前失敗的斷言,通過一個個測試,慢慢前行。

  • JUnit 的框架

    到目前為止,我們只是介紹了斷言本身,很顯然我們不能只是簡單的把斷言方法寫完,就希望測試可以運行起來。我們需要一個框架來輔助完成這些,那麼我們就要做多一些工作了。很幸運的是,我們不用多做太多。

    在 JUnit 4 提供了@Before@After,在每個測試函數調用之前/後都會調用。

    • @Before: Method annotated with @Before executes before every test. 每個測試方法開始前執行的方法
    • @After: Method annotated with @After executes after every test. 每個測試方法執行後再執行的方法

如果在測試之前有些工作我們只想做一次,用不著每個函數之前都做一次。比如讀一個很大的文件。那就用下面兩個來標註:

@BeforeClass: 測試類初始化的時候,執行的方法

@AfterClass: 測試類銷毀的時候,執行的方法

注意:

    1. @Before/@After 可以執行多次; @BeforeClass/@AfterClass 只能執行一次
    2. 如果我們預計有Exception,那就給@Test加參數:@Test(expected = XXXException.class)
    3. 如果出現死循環怎麼辦?這時timeout參數就有用了:@Test(timeout = 1000)
    4. 如果我們暫時不用測試一個用例,我們不需要刪除或都注釋掉。只要改成:@Ignore ,你也可以說明一下原因@Ignore("something happens")

示例代碼:下面的代碼代表了單元測試用例的基本框架

public class JUnitDemoTest { @Before public void setUp(){ //TODO: 測試預置條件,測試安裝 } @After public void tearDown(){ //TODO: 測試清理,測試卸載 } @Test public void test01(){ //TODO: test01 腳本 } @Test public void test02(){ //TODO: test02 腳本 } @Test public void test03(){ //TODO: test03 腳本 }}

單元測試框架的過程如下:

單元測試框架的過程

JUnit 需要注意的事項:

    1. 每個 @Test 都是一個測試用例,一個類可以寫多個 @Test
    2. 每個 @Test 執行之前 都會執行 @Before,執行之後都會運行 @After
    3. 每個 @Test@After@Before 都必須是 public void, 參數為空
    4. @After / @Before 也可以是多個,並且有執行順序。在每個 @Test 前後執行多次。
  • @Before 多個名字長度一致,z -> a, 長度不一致,會先執行名字短的。
  • @After / @Test 多個名字長度一致,a -> z, 長度不一致,會後執行名字短的。
    1. @AfterClass / @BeforeClass 也可以是多個,並且有執行順序。只會在測試類的實例化前後各執行一次。
  • @BeforeClass 多個名字長度一致,z -> a, 長度不一致,會先執行名字短的。
  • @AfterClass 多個名字長度一致,a -> z, 長度不一致,會後執行名字短的。
    1. @AfterClass / @BeforeClass 都必須是 public static void, 參數為空
    2. 測試結果有 通過、不通過和錯誤 三種。
  • JUnit 的測試運行

    這一小節,我們來介紹一下 JUnit 4 中的新的測試運行器(Test Runner)。如果我們剛開始編寫測試,那麼我們需要儘可能快捷的運行這些測試,這樣我們才能夠將測試融合到開發循環中去。

    編碼 → 運行 → 測試 → 編碼……

    其中,JUnit 就可以讓我們構建和運行測試。我們可以按照組合測試Suite 以及參數化測試分別來運行測試。

    • 組合測試Suite

      測試集 (Suite 或者 test suite)一組測試。測試集是一種把多個相關測試歸入一組的便捷測試方式。可以在一個測試集中,定義需要打包測試的類,並一次性運行所有包含的測試;也可以分別定義多個測試集,然後在一個主測試集中運行多個相關的測試集,打包相關的測試的類,並一次性運行所有包含的測試。

      示例代碼如下:

@RunWith(value = Suite.class)@Suite.SuiteClasses(value = HelloWorldTests.class)public class HelloWorldTestRunner {}?

    • 參數化測試

      參數化測試(Parameterized)是測試運行器允許使用不同的參數多次運行同一個測試。參數化測試的代碼如下:

@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)); }}

    • 對於參數化測試的運行器來運行測試類,那麼必須滿足以下要求:

  1. 測試類必須使用@RunWith(value = Parameterized.class)註解
  2. 必須聲明測試中所使用的實例變數
  3. 提供一個用@Parameterized.Parameters的註解方法,這裡用的是getTestParameters(),同時此方法的簽名必須是public static Collection
  4. 為測試指定構造方法,或者一個個全局變數的成員進行賦值
  5. 所有的測試方法以@Test註解,實例化被測試的程序,同時在斷言中使用我們提供的全局變數作為參數

?

1.4 [探討]按業務價值導向進行單元測試設計

  • 練習:測試的結果是否正確

    如果測試代碼能夠運行正確,我們要怎麼才能知道它是正確的呢?

    如何應對測試數據量比較大的時候,我們的測試代碼如何編寫?

    ?
  • 練習:測試的邊界條件

    尋找邊界條件是單元測試中最有價值的工作之一,一般來說Bug出現在邊界上的概率比較大。那麼我們都需要考慮什麼樣的邊界條件呢?

    ?
  • 練習:強制產生錯誤條件

    關於產生錯誤的條件,請列出一個詳細的清單來。

    ?
  • 分析:測試作為設計工具

    第一節【專題】中,我們有討論設計潛力的曲線,其中第二條方案強調了測試作為設計的工具。那麼我們想就兩個方面來討論這個測試設計的問題。

  1. TDD,測試驅動開發
  2. BDD,行為驅動開發

推薦閱讀:

TAG:自動化測試 | 單元測試 | 軟體測試 |