單元測試之旅--預見優秀

AstridAugus 7年前發布 | 67K 次閱讀 單元測試 Java

單元測試之旅:預見優秀

大綱

  • 單元測試入門——優秀基因:從單元測試的定義開始,主要討論單元測試如何開展的話題。
  • 單元測試進階——尋求優秀:在熟悉單元測試的基礎上,主要討論如何進行優秀的單元測試。
  • 單元測試實踐——構建優秀:對優秀的單元測試進行具體實踐,以及探討單元測試更多的場景。

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

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

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

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

文中的相關約定:

文中的示例代碼塊均使用Java語言。 文中的粗體部分表示重點內容和重點提示。 文中的引用框部分,一般是定義或者來源于其它地方。 文中標題的【探討】,表示此部分講師與學員共同探討并由講師引導,得到方案。 文中的 代碼變量和說明 用方框圈起來的,是相關代碼的變量、方法、異常等。

1.1 單元測試的價值

  • 什么是單元測試

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

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

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

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

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

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

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

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

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

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

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

    ?

    </li>
  • 優秀單元測試的特性

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

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

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

    1. 自動的、可重復的執行的測試
    2. 開發人員比較容易實現編寫的測試
    3. 一旦寫好,將來任何時間都依舊可以用
    4. 團隊的任何人都可運行的測試
    5. 一般情況下單擊一個按鈕就可以運行
    6. 測試可以可以快速的運行
    7. ……
    8. </ol>

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

      • 自動化(Automatic)
      • 徹底的(Thorough)
      • 可重復(Repeatable)
      • 獨立的(Independent)
      • 專業的(Professional)

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

      • A -TRIP 自動化(Automatic)

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

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

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

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

        總之,單元測試越多,代碼問題越少。

      • A-T R IP 可重復(Repeatable)

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

        如果每次測試執行不能產生相同的結果,那么真相只有一個:代碼中有真正的Bug。

      • A-TR I P 獨立的(Independent)

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

        獨立的,意味著你可以在任何時間以任何順序運行任何測試。每一個測試都應該是一個孤島。

      • A-TRI P 專業的(Professional)

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

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

      ?

      </li>
    9. 生產力的因素

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

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

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

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

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

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

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

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

        ?

        </li>
      5. 設計潛力的曲線

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

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

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

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

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

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

          </li> </ul>

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

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

            ?

            </li>
          • 分析:為什么不寫單元測試

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

            ?

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

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

            ?

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

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

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

            • 帶來更高的測試覆蓋率

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

            • 提高團隊效率

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

            • 自信的重構和改進實現

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

            • 將預期的行為文檔化

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

            • </ul>

              ?

              </li> </ul>

              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; } }</code></pre>

                雖然我們針對該類,沒有列出文檔,但是 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</code></pre>

                第一個 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.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</code></pre>

                按照第一節中的描述的優秀的單元測試,上述代碼毫無疑問,稱不上優秀的單元測試,因為測試連運行都無法運行。令人高興的是,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);
                }
                

                }</code></pre>

                如上的操作,我們便定義了一個單元測試,使用 JUnit 編寫了測試。主要的要點如下:

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

                為了進行驗測試驗證,我們使用了由 JUnit 的 Assert 類提供的 assert 方法。正如我們在上面的例子中使用的那樣,我們在測試類中靜態的導入這些方法,同時還有更多的方法以供我們使用,如下我們列出一些流行的 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) | 用匹配器進行斷言,高級應用*,不再此文檔討論 |

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

              • </ul>
                • 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 , 長度不一致,會后執行名字短的。
                  5. @AfterClass / @BeforeClass 也可以是多個,并且有執行順序。只會在測試類的實例化前后各執行一次。
                    • @BeforeClass 多個名字長度一致, z -> a , 長度不一致,會先執行名字短的。
                    • @AfterClass 多個名字長度一致, a -> z , 長度不一致,會后執行名字短的。
                  6. @AfterClass / @BeforeClass 都必須是 public static void, 參數為空
                  7. 測試結果有 通過、不通過和錯誤 三種。
                  </li> </ul>
                  • 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));
                      }
                      

                      }</code></pre>

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

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

                      ?

                      </li> </ul> </li> </ul>

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

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

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

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

                        ?

                      • 練習:測試的邊界條件

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

                        ?

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

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

                        ?

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

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

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

                        ?

                      2. 單元測試進階——尋求優秀

                      2.1 使用測試替身

                      在現代開發者測試的上下文中,除了允許在某些依賴缺失的情況下編譯執行代碼以外,崇尚測試的程序員還創建了一套“僅供測試”的工具,用于隔離被測試的代碼、加速執行測試、使得隨機行為變得確定、模擬特殊情況以及能夠使測試訪問隱藏信息等。滿足這些目的的各種對象具有相似之處,但又有所區別,我們統稱為測試替身(test double)。

                      這一節我們先探討開發者采用測試替身的理由,理解了測試替身潛在的好處以后,我們再解析來看看各種可供選擇的測試替身的類型。

                      • 測試替身的威力

                        引入測試替身的最根本的原因是——將被測試代碼與周圍隔離開。為了時不時的驗證一段代碼的行為是否符合期望值,我們最好的選擇就是替換其周圍的代碼,使得獲取對環境的完整控制,從而在其中測試目標代碼。

                        通過以下的幾個部分,我們來討論測試替身的好處。

                        • 隔離被測試的代碼

                          代碼的世界,一般包括了兩種:被測試代碼和與被測試代碼進行交互的代碼。

                          接下來我們用一個簡單的例子,展示如何隔離代碼。示例代碼如下:

                          public class Car {
                              private Engine engine;

                          public Car(Engine engine) {
                              this.engine = engine;
                          }
                          
                          public void start() {
                              this.engine.startUp();
                          }
                          
                          public void stop() {
                              this.engine.shutDown();
                          }
                          
                          public void drive(Route route) {
                              for (Directions directions : route.directions()) {
                                  directions.follow();
                              }
                          }
                          

                          }</code></pre>

                          這個例子中,包括了兩個協作類: Engine 和 Route ,還有一個間接使用者: Directions

                          我們站在 Car 的視角,用測試替身替換 Engine 和 Route , 用偽實現替換Route,那么我們就完全控制了向 Car 提供的各種 Directions 。

                          類之間的關系如下:

                          </li>
                        • 加速執行測試

                          由于 Car 需要調用 Directions ,而后者的產生依賴于 Route ,假設在 Route 層面需要的時間比較多,測試來不及等這么久的情況下,可以通過使用對 Route 放置測試替身,實現快速的不用等待的測試執行。

                          放置一個測試替身,令它總是返回預先計算好的路徑,這樣會避免不必要的等待,而且測試運行的更快了,

                        • 使執行變得確定

                          任何的測試代碼,都可能包含了不確定的隨機性。為了驗證代碼和測試具有確定的結果,我們需要能夠針對同樣的代碼進行重復的運行測試,并總能夠得到相同的結果。

                          事實上,這個情況非常理想狀態。很多時候,生產的代碼有隨機因素。或許不確定的行為,最典型的情形就是依賴于時間的行為。回到我們的 Car 的這個例子,不同的時間,得到的路線( Route 的 Directions )可能是不同的。在高峰時間和非高峰時間,得到的路徑導航,可能是不相同的。我們通過對 Route 進行測試替身,使得之前不確定的測試變得確定起來。

                        • 暴露隱藏的信息

                          在 Car 這個例子里面,可以用測試替身完成最后一個需要它的理由。我們能看到,當 Car 進行啟動的時候,需要調用了engine的 start() 的方法。engine目前是私有型,我們在測試中無法獲得的engine的項目類型。那么我們需要用一個測試替身,來通過給它增加狀態的方式,驗證單元測試對亂碼的討厭。

                          被測試的代碼:

                          public class TestEngine extends Engine {
                              public boolean isRunning() {
                                  return isRunning;
                              }

                          private boolean isRunning;
                          
                          public void start() {
                              this.isRunning = true;
                          }
                          

                          }</code></pre>

                          ?

                          </li> </ul> </li> </ul>
                          • 測試替身的類型

                            主要的測試替身有 樁 ( Stub )、偽造對象( Fake )、測試間諜( Spy )以及模擬對象( Mock )四種。

                            1. Stub (樁):一般什么都不做,實現空的方法調用或者簡單的硬編碼返回即可。
                            2. Fake (偽造對象):真實事物的簡答版本,優化的偽造真實事物的行為,但是沒有副作用或者使用真實事物的其它后果。比如替換數據庫的對象,而得到虛假的偽造對象。
                            3. Spy (測試間諜):需要得到對象內部的狀態的時候,而該對象對外又是封閉的,那么需要做一個測試間諜,事先學會反饋消息,然后潛入對象內部去獲取對象的狀態。測試間諜是一種測試替身,它用于記錄過去發生的情況,這樣測試在事后就能知道所發生的一切。
                            4. Mock (模擬對象):模擬對象是一個特殊的測試間諜。是一個在特定的情況下可以配置行為的對象,規定了在什么情況下,返回什么樣的值的一種測試替身。Mock已經有了非常成熟的對象庫,包括JMock、Mockito和EasyMock等。

                          2.2 [探討]優秀單元測試的支柱

                          • 分析:獨立的測試易于單獨運行

                            什么樣的單元測試是獨立的測試?

                            ?

                          • 分析:可維護的測試才是有意義的

                            什么樣的措施可以使得單元測試是可維護的?

                            ?

                          • 可讀的代碼才是可維護的

                            如何從測試用例的要素中匹配單元測試代碼的可讀性?

                            ?

                          • 可靠的測試才是可靠的

                            從哪些角度的思考與設計可以讓單元測試代碼變得可信賴和可靠?

                            ?

                          2.3 識別單元測試中的壞味道

                          • 過度斷言

                            過度斷言是如此謹慎的敲定每個待檢查行為的細節,以致它變得脆弱,并且掩蓋了整體廣度很深度之下的意圖。當遇到過度斷言,很難說清楚它要檢查什么,并且當你退后一步觀察,會看到測試打斷的頻率可能遠超平均水平。它如此挑剔,以致無論任何變化都會造成輸出與期望不同。

                            我們看下面的例子來具體討論。被測試的類叫做 LogFileTransformer ,是一個用來轉換日志格式的類。

                            public class LogFileTransformerTest {
                                private String expectedOutput;
                                private String logFile;
                                @Before
                                public void setUpBuildLogFile(){
                                    StringBuilder lines = new StringBuilder();
                                    lines.append("[2015-05-23 21:20:33] LAUNCHED");
                                    lines.append("[2015-05-23 21:20:33] session-di###SID");
                                    lines.append("[2015-05-23 21:20:33] user-id###UID");
                                    lines.append("[2015-05-23 21:20:33] presentation-id###PID");
                                    lines.append("[2015-05-23 21:20:33] screen1");
                                    lines.append("[2015-05-23 21:20:33] screen2");
                                    //TODO: lines.append(...)
                                    logFile = lines.toString();
                                }
                                @Before
                                public void setUpBuildTransformedFile(){
                                    StringBuilder lines = new StringBuilder();
                                    lines.append("LAUNCHED");
                                    lines.append("session-di###SID");
                                    lines.append("user-id###UID");
                                    lines.append("presentation-id###PID");
                                    lines.append("screen1");
                                    lines.append("screen2");
                                    //TODO: lines.append(...)
                                    expectedOutput = lines.toString();
                                }
                                @Test
                                public void testTransformationGeneratesRgiht(){
                                    TransfermationGenerator generator = new TransfermationGenerator();
                                    File outputFile = generator.transformLog(logFile);
                                    Assert.assertTrue("目標文件轉換后不存在!", outputFile.exists());
                                    Assert.assertEquals("目標文件轉換后不匹配!", expectedOutput, getFileContent(outputFile));
                                }
                            }

                            看到過度斷言了么?這里有兩個斷言,但是哪個是罪魁禍首,什么造成斷言被濫用了呢?

                            第一個斷言檢查目標文件是否創建,第二個斷言檢查目標文件的內容是否符合期望。現在,第一個斷言的價值值得商榷,而且很可能需要被刪除。但是我們主要關注第二個斷言——過度斷言:

                            Assert.assertEquals("目標文件轉換后不匹配!", expectedOutput, getFileContent(outputFile));

                            ?

                            看上去,它精確的驗證了測試名稱所暗示的內容,這是個重要的斷言。問題是這個測試太寬泛了,導致斷言對整個日志文件進行大規模的比較。這是一張厚厚的安全網,毫無疑問,即使是輸出中最微小的變化,也會是斷言失敗。這也正是存在的問題。

                            上述例子太容易失敗而變得脆弱,斷言并無本質的錯誤,但是問題在于測試違反了構成優秀測試的基本指導原則。

                            一個測試應該只有一個失敗原因

                            那么我們如何改進這個測試?

                            我們需要避免全文測試,就算需要要求,也需要分部分內容去測試。

                            @Test
                            public void testTransformationGeneratesRgiht2(){
                                TransfermationGenerator generator = new TransfermationGenerator();
                                File outputFile = generator.transformLog(logFile);
                                Assert.assertTrue("目標文件轉換后不匹配!", getFileContent(outputFile).contains("screen1###0"));
                                Assert.assertTrue("目標文件轉換后不匹配!", getFileContent(outputFile).contains("screen1###51"));
                            }
                            @Test
                            public void testTransformationGeneratesRgiht3(){
                                TransfermationGenerator generator = new TransfermationGenerator();
                                File outputFile = generator.transformLog(logFile);
                                Assert.assertTrue("目標文件轉換后不匹配!", getFileContent(outputFile).contains("session-di###SID#0"));
                            }

                            修改后,分部對指定的部分進行測試。

                            ?

                          • 人格分裂

                            改進測試的一個最簡單的方法,就是找出人格分裂的情況。當測試出現了人格分裂的時候,我們認為它本身體現了多個測試,那是不對的。一個測試應當僅檢查一件事并妥善執行。

                            我們看下面的例子。測試類針對一些命令行接口,用不同的命令行參數來測試 Configuration 類對象的行為。

                            public class ConfigurationTest {
                                @Test
                                public void testParingCommandLineArguments() {
                                    String[] args = {"-f", "hello.txt", "-v", "--version"};
                                    Configuration c = new Configuration();
                                    c.processArguments(args);
                                    Assert.assertEquals("hello.txt", c.getFileName());
                                    Assert.assertFalse(c.isDebuggingEnabled());
                                    Assert.assertFalse(c.isWarningsEnabled());
                                    Assert.assertTrue(c.isVerbose());
                                    Assert.assertTrue(c.shouldShowVersion());

                                c = new Configuration();
                                try{
                                    c.processArguments(new String[] {"-f"});
                                    Assert.fail("should 測試失敗" );
                                }catch (InvalidArgumentException expected){
                                    // 沒有問題
                                }
                            }
                            

                            }</code></pre>

                            這個測試的多重人格體現在它涉及了文件名、調試、警告、信息開關、版本號顯示,還處理了空的命令行參數列表。這里沒有遵循 準備 --> 執行 --> 斷言 的結構。很明顯這里斷言了許多東西,雖然它們全部與解析命令行參數有關,但是還是可以彼此隔離的。

                            這個測試的主要問題是胃口太大,同時還存在一些重復,我們先排除這些干擾,這樣就可以看清主要問題了。

                            首先,在測試里用了多次對 Configuration 類的構造器實例化的操作,我們可以將此類的操作抽取出來,并用 @Before 方法中實例化。這樣也去掉了測試中的一部分重復。

                            代碼如下:

                            protected Configuration c;
                            @Before
                            public void instantiateDefaultConfiguration() {
                                c = new Configuration();
                            }

                            去掉重復的實例化以后,我們剩下來對 processArguments() 的兩次不同調用和6個不同的斷言(包括了 try-catch-fail 模式)。這樣意味著我們至少要用兩個不同的場景——也就是兩個不同的測試。

                            結合上面的 @Before ,代碼如下:

                            @Test
                            public void validArgumentsProvided(){
                                  String[] args = {"-f", "hello.txt", "-v", "--version"};
                                c.processArguments(args);
                                Assert.assertEquals("hello.txt", c.getFileName());
                                Assert.assertFalse(c.isDebuggingEnabled());
                                Assert.assertFalse(c.isWarningsEnabled());
                                Assert.assertTrue(c.isVerbose());
                                Assert.assertTrue(c.shouldShowVersion());
                            }
                            @Test
                            public void missingArgument(){
                                try{
                                      c.processArguments(new String[] {"-f"});
                                      Assert.fail("should 測試失敗" );
                                }catch (InvalidArgumentException expected){
                                      // 沒有問題
                                }
                            }

                            但是其實我們還在半路上,一些檢查條件是命令行參數的顯然結果,另一些是隱含的默認值。從這個角度改進,我們將測試分解成多個測試類。如下圖所示:

                            這次重構意味著有一個測試關注于驗證正確的默認值,另一個測試類驗證顯示設置的命令行值能正確工作,第三個指出應當如何處理錯誤的配置項。代碼如下:

                            • AbstractConfigTestCase

                              public abstract class AbstractConfigTestCase {
                                  protected Configuration c;

                              @Before
                              public void instantiateDefaultConfiguration() {
                                  c = new Configuration();
                                  c.processArguments(args());
                              }
                              
                              protected String[] args() {
                                  return new String[] {};
                              }
                              

                              }</code></pre>

                              ?

                              </li>
                            • TestDefaultConfigValues

                              public class TestDefaultConfigValues extends AbstractConfigTestCase {
                                  @Test
                                  public void defaultOptionsAreSetCorrectly() {
                                      assertFalse(c.isDebuggingEnabled());
                                      assertFalse(c.isWarningsEnabled());
                                      assertFalse(c.isVerbose());
                                      assertFalse(c.shouldShowVersion());
                                  }
                              }

                              ?

                            • TestExplicitlySetConfigValues

                              public class TestExplicitlySetConfigValues extends AbstractConfigTestCase {
                                  @Override
                                  protected String[] args() {
                                      return new String[] { "-f", "hello.txt", "-v", "-d", "-w", "--version" };
                                  }

                              @Test
                              public void explicitOptionsAreSetCorrectly() {
                                  assertEquals("hello.txt", c.getFileName());
                                  assertTrue(c.isDebuggingEnabled());
                                  assertTrue(c.isWarningsEnabled());
                                  assertTrue(c.isVerbose());
                                  assertTrue(c.shouldShowVersion());
                              }
                              

                              }</code></pre>

                              ?

                              </li>
                            • TestConfigurationErrors

                              public class TestConfigurationErrors extends AbstractConfigTestCase {
                                  @Override
                                  protected String[] args() {
                                      return new String[] { "-f" };
                                  }

                              @Test(expected = InvalidArgumentException.class)
                              public void missingArgumentRaisesAnError() {
                              }
                              

                              }</code></pre>

                              ?

                              </li> </ul> </li>
                            • 過分保護

                              運行 Java 代碼的時候,常見的Bug之一就是突然出現 NullPointerException 或 InndexOutOfBoundsException ,這是由于方法意外的收到空指針或者空串參數造成的。當然這些可以由程序員對其進行單元測試,從而增強守衛,保護好自己。

                              但是,程序員往往不是保護測試免于以 NullPointerException 而失敗,而是讓測試優雅的以華麗措辭的斷言而失敗。這是一種典型的壞味道。

                              代碼示例:用了兩個斷言來驗證正確的計算:一個驗證返回的Data對象不為空,另一個驗證實際的計數是正確的。

                              public class TestCount {
                                  @Test
                                  public void count(){
                                      Data data = project.getData();
                                      Assert.assertNotNull(data);
                                      Assert.assertEquals(8, data.count());
                                  }
                              }

                              這是過度保護的測試,以為 assertNotNull(data) 是多余的。在調用方法之前,第一個斷言檢查 data 不為空,如果為空,測試就失敗,這樣的測試受到了過度的保護。這是因為當 data 為空的時候,就算沒有第一個斷言,測試仍然會時報。第二個斷言試圖調用 data 上的count()時,測試會不幸的以 NullPointerException 而失敗。

                              需要做的事情,是刪除冗余的斷言,它基本上是不能提供附加價值的斷言和測試語句。

                              刪除第5行。 Assert.assertNotNull(data);

                            • 重復測試

                              程序員在寫代碼的時候,往往關注和追求整潔的代碼(clean code)。而重復就是導致代碼失去整潔的罪魁禍首之一。那么什么是重復呢?簡單來說,重復是存在多份拷貝或對單一概念的多次表達——這都是不必要的重復。

                              重復是不好的,它增加了代碼的不透明性,使得散落在各處的概念和邏輯很難理解。此外,對于修改代碼的程序員來說,每一處重復都是額外的開銷。如果忘記或者遺漏了某處的改動,那么又增加了出現Bug的機會。

                              代碼示例:這個代碼展示了幾種形式的重復。

                              public class TestTemplate {
                                  @Test
                                  public void emptyTemplate() throws Exception {
                                      assertEquals("", new Template("").evaluate());
                                  }

                              @Test
                              public void plainTextTemplate() throws Exception {
                                  assertEquals("plaintext", new Template("plaintext").evaluate());
                              }
                              

                              }</code></pre>

                              代碼中出現了最常見的文本字符串重復,在兩個斷言中,空字符串和 plaintext 字符都出現了兩次。我們叫這種重復為文字重復。我們可以通過定義局部變量來移除它們。同時在上述測試類中,還存在另一種重復,也許比顯而易見的字符串重復有趣的多。當我們提取那些局部變量的時候,這種重復會變得更加清晰。

                              首先,我們抽取重復的字符串,清理這些壞的味道。

                              public class TestTemplate {
                                  @Test
                                  public void emptyTemplate() throws Exception {
                                      String template = "";
                                      assertEquals(template, new Template(template).evaluate());
                                  }

                              @Test
                              public void plainTextTemplate() throws Exception {
                                  String template = "plaintext";
                                  assertEquals(template, new Template(template).evaluate());
                              }
                              

                              }</code></pre>

                              其次,確實還有一些比較嚴重的重復,我們看這兩個測試,只有字符串是不同的。當我們抽取的字符串之后,剩下的斷言是一模一樣的,這種操作不同數據的重復邏輯,我們叫做結構重復。以上的兩個代碼塊用一致的結構操作了不同的數據。

                              我們去掉這種重復,提煉重復后,產生一個自定義的斷言方式。

                              public class TestTemplate {
                                  @Test
                                  public void emptyTemplate() throws Exception {
                                      assertTemplateRendersAsItself("");
                                  }

                              @Test
                              public void plainTextTemplate() throws Exception {
                                  assertTemplateRendersAsItself("plaintext");
                              }
                              
                              private void assertTemplateRendersAsItself(String template) {
                                  assertEquals(template, new Template(template).evaluate());
                              }
                              

                              }</code></pre>

                              ?

                              </li>
                            • 條件邏輯

                              在測試中,一旦存在條件邏輯的時候,一般都不是一件好事兒。這里的條件邏輯,是一種壞味道。假設我們正在重構代碼,并運行之前的單元測試來保證代碼一切正常。可是此時發現某個測試失敗了。看上去很出乎意料,沒想到這點小的變更卻會影響測試,但是它的確發生了。我們查看代碼,卻突然發現自己無法知道,測試失敗的時候,代碼當時在干什么。

                              代碼示例:測試創建了 DictionaryDemo (字典)對象,用數據填充它,并驗證請求到的 Iterator (迭代器)的內容是正確的。

                              public class DictionaryTest {
                                  @Test
                                  public void returnsAnIteratorForContents(){
                                      DictionaryDemo dictionary = new DictionaryDemo() ;
                                      dictionary.add("key1", new Long(3));
                                      dictionary.add("key2", "45678");
                                      for (Iterable e = dictionary.iterator(); e.hasNext();){
                                          Map.Entry entry = (Map.Entry) e.next();
                                          if( "key1".equals(entry.getKey())){
                                              Assert.assertEquals(3L, entry.getValue());
                                          }
                                          if( "key2".equals(entry.getKey())){
                                              Assert.assertEquals("45678", entry.getValue());
                                          }
                                      }
                                  }
                              }

                              我們可以看到,這個測試針對的只是 DictionaryDemo 的內部行為,但是仍然非常難理解和解釋。通過遍歷條目,我們得到返回的 Iterator ,并根據鍵值對的關系,通過 Key,找到該條目的 Value。但是實際上,如果這兩個 Key 沒有被添加進去的時候,這個測試不會報錯。這里存在了壞的味道。通過使用自定義斷言,得到修改。

                              代碼如下:

                              public class DictionaryTest {

                              @Test
                              public void returnsAnIteratorForContents2(){
                                  DictionaryDemo dictionary = new DictionaryDemo() ;
                                  dictionary.add("key1", new Long(3));
                                  dictionary.add("key2", "45678");
                                  assertContains(dictionary.iterator(), "key1", 3L);
                                  assertContains(dictionary.iterator(), "key2", "45678");
                              
                              }
                              
                              private void assertContains(Iterator i, Object key, Object value){
                                  while (i.hasNext()){
                                      Map.Entry entry =  (Map.Entry) i.next();
                                      if( key.equals(entry.getKey())){
                                          Assert.assertEquals(value, entry.getValue());
                                          return;
                                      }
                                  }
                                  Assert.fail();
                              }
                              

                              }</code></pre>

                              最后強調一下, Assert.fail() 很容易被遺漏掉。接下來我們就要再一次修改這樣的壞味道了。

                              ?

                              </li>
                            • 永不失敗的測試

                              永不失敗的測試,如果是真的能夠做到百戰百勝,那么是再好不過了。但是往往事與愿違,永不失敗的測試往往比沒有測試還糟糕。因為它給了虛假的安全感,這樣的測試沒有價值,出了事情它絕不警告你。

                              檢查代碼是否拋出期望的異常,或許這是一個最常見的在永不失敗的測試的場景。

                              示例代碼:

                              public class HelloWorldTests {
                                  @Test
                                  public void includeForMissingResourceFails(){
                                      try {
                                          new Environment().include("somethingthatdoesnotexist");
                                      }catch (IOException e){
                                          Assert.assertThat(e.getMessage(),
                                                  contains("somethingthatdoesnotexist"));
                                      }
                                  }
                              }

                              這個代碼清單中測試的結果是這樣的:

                              1. 如果代碼如期工作并拋出異常,那么這個異常就被catch代碼塊捕獲,于是測試通過。
                              2. 如果代碼沒有如期工作,也就是沒有拋出異常,那么方法返回,測試通過,我們并未意識到代碼有任何問題。
                              3. </ol>

                                但是,這是一個拋異常的測試,在沒有拋出異常的時候,測試其實是失敗的,需要調用fail()來表示失敗。

                                public class HelloWorldTests {
                                    @Test
                                    public void includeForMissingResourceFails(){
                                        try {
                                            new Environment().include("somethingthatdoesnotexist");
                                              Assert.fail();
                                        }catch (IOException e){
                                            Assert.assertThat(e.getMessage(),
                                                    contains("somethingthatdoesnotexist"));
                                        }
                                    }
                                }

                                ?

                                簡單的增加對 JUnit 中 fail() 方法的調用,是得測試起作用。現在除非拋出期望的異常,否則測試失敗。

                                另外 JUnit 4 引入的一個新特性是 @Test 注解的 expected 屬性。

                                public class HelloWorldTests {
                                    @Test(expected = IOException.class)
                                    public void includeForMissingResourceFails(){
                                        new Environment().include("somethingthatdoesnotexist");
                                    }
                                }

                                這樣的特性,更短、更容易解析、更不易出錯和遺漏。當然這種方法的缺點也很明顯:我們不能訪問所拋出的實際異常對象,無法進一步對異常進行斷言。總之,要防止偶然的寫一個用不失敗的的測試,最好的方法是養成運行測試的習慣,或許是臨時修改被測試的代碼來故意觸發一次失敗,從而看到所犯的錯誤以及壞味道。

                                </li> </ul>

                                2.4 [探討]在項目中進行單元測試

                                • 分析:項目中單元測試策略

                                  在一個項目中,單元測試的策略的制定與執行需要考慮哪些因素?

                                  ?

                                • 分析:如何組織單元測試的數據

                                  在一個項目中,單元測試的數據是否應該以硬編碼的形式寫入代碼中?如果不是的話,需要如何組織這些測試用的數據呢?

                                  ?

                                • 分析:誰該為項目的質量負責

                                  請思考一個問題,一個典型的項目組(包含項目經理、測試、開發和需求分析師)中誰應該為項目的質量負責?

                                  ?

                                3. 單元測試實踐——構建優秀

                                3.1 在組織中引入單元測試

                                在一個組織中成功的引入測試驅動開發和單元測試并集成到該組織的文化中,對該組織的發展和團隊的效率將會有極大的提升。然后有時候這個引入會失敗,成功的組織則存在一些共性的東西,我們在這一節將探討一下如何增加引入單元測試的成功率。

                                在任何類型的組織中,改變人們的習慣多半與心理學有關,而并非是技術問題。人們不喜歡變化,而且變化常常伴隨著很多的FUD(fear, uncertainty, and doubt——害怕、不確定性和懷疑)。于是如何說服組織成員或者讓組織接受新的變化,并不是一件容易和輕松的事情。

                                • 怎樣成為變革推動者

                                  開始實施變革之前,人們會開始對它們關心的事情提出棘手的問題,例如這樣做會“浪費”多少時間?這對于開發人員來說意味著什么?我們怎么知道它有效呢?這些問題可以嘗試用下面的成功的方式進行解決。你會發現,當你能夠回答這些問題,并說服組織中的其它人,會對組織的變革提供非常大的幫助。

                                  這里有一些幫助的小提示:

                                  • 選擇較小的團隊
                                  • 新建子團隊
                                  • 考慮項目的可行性

                                  此外,在變革中需要找到阻礙者,并尋找到它們不愿意進行單元測試的嘗試的原因所在,加以解決。此外可以考慮賦予它們全新的職責,會讓它們覺得被依賴而且對組織有意義。

                                  ?

                                  </li>
                                • 成功之路

                                  組織或者團隊開始改變流程主要有兩個方式:自下而上或者自上而下。

                                  1. 自下而上:先說服程序員,使得程序員采納并且提倡,然后產生組織的變革,最終說服管理層接受。
                                  2. 自上而下:經理通過給團隊做一個演示來開始實施,或者使用自己的權力進行推動變革。
                                  3. </ol>

                                    代碼的完整性,Code Integrity

                                    這個術語,通常意味著代碼做它該做的事,而團隊知道代碼不能做哪些事。

                                    代碼的完整性包括如下實踐:

                                    • 自動化構建
                                    • 持續集成
                                    • 單元測試與測試驅動開發
                                    • 代碼一致性和商定的質量標準
                                    • 盡量快速的修復缺陷

                                    為了“我們的代碼完整性很好”這個目標,也可以開始如上的實踐。

                                    ?

                                    </li>
                                  4. 鎖定目標

                                    沒有目標,將會很難衡量改變,并且與他人交流。可以考慮下面的目標

                                    1. 提高代碼測試覆蓋率
                                    2. 提高相對代碼改動量的測試覆蓋率
                                    3. 減少重復出現的缺陷
                                    4. 減少修復缺陷的平均時間
                                    5. </ol> </li> </ul>

                                      3.2 使用 Maven 運行 JUnit 單元測試

                                      • Maven的功能與安裝

                                        Maven 是一個用于項目構建的工具,通過它便捷的管理項目的生命周期。同時 Maven 不只是一個簡單的項目構建工具,還是一個依賴管理工具和項目信息管理工具。它提供了中央倉庫,能幫我們自動下載構建。

                                        在之前的課程中,我們使用 IDEA 工具,通過直接導入 JUnit 的 *.jar 包文件,進行單元測試的構建的。在這里我們繼續使用 Maven 作為構建工具,來構建 JUnit 單元測試項目。

                                        首先,不要相信任何 IDE(Integration Development Environment,集成開發工具)中自帶的 Maven 插件,包括 IDEA自帶的。那么我們需要安裝 Maven。

                                        具體的安裝步驟如下:

                                        • 檢查 JDK 的安裝

                                          在安裝 Maven 之前,首先要確認已經正確的安裝了 JDK。Maven 可以運行在 JDK 1.4 以及以上的版本。目前的 JDK 1.8 的版本是可以的。需要下載 JDK 并進行安裝。安裝好 JDK 以后,需要檢查 %JAVA_HOME% 的環境變量是否設定。

                                          輸入 cmd | 打開 Windows 命令行, 輸入 echo %JAVA_HOME%

                                          Microsoft Windows [Version 10.0.14393]
                                          (c) 2016 Microsoft Corporation. All rights reserved.

                                          C:\Users\xxx>echo %JAVA_HOME% C:\Program Files\Java\jdk1.8.0_66</code></pre>

                                          ?

                                          </li>
                                        • 下載并安裝 Maven

                                          Maven 可以免費在官網下載并安裝。打開 Manve 的下載頁面,下載針對所用平臺的對應的版本,然后在 C 盤解壓即可。

                                          Maven 的下載地址: https://maven.apache.org/download.cgi

                                          解壓以后,需要設定 Windows 的環境變量。

                                          1. %M2_HOME%:在系統變量添加,路徑為安裝的 Maven 的根目錄,例如 C:\Apache\apache-maven-3.3.9
                                          2. path:在系統變量中,找到path,添加上去 ;%M2_HOME%\bin;
                                          3. 重新打開 Windows 命令行,輸入 mvn -version
                                          4. </ol>
                                            Microsoft Windows [Version 10.0.14393]
                                            (c) 2016 Microsoft Corporation. All rights reserved.

                                            C:\Users\xxx>mvn -version Apache Maven 3.3.9 (bb52d8502b132ec0a5a3f4c09453c07478323dc5; 2015-11-11T00:41:47+08:00) Maven home: C:\Apache\apache-maven-3.3.9 Java version: 1.8.0_66, vendor: Oracle Corporation Java home: C:\Program Files\Java\jdk1.8.0_66\jre Default locale: en_US, platform encoding: GBK OS name: "windows 10", version: "10.0", arch: "amd64", family: "dos"</code></pre>

                                            ?

                                            </li> </ul> </li>
                                          5. 建立一個Maven項目

                                            使用 IDEA 新建 Maven Project,并添加依賴如下:

                                            <dependency>
                                               <groupId>junit</groupId>
                                               <artifactId>junit</artifactId>
                                               <version>4.12</version>
                                               <scope>test</scope>
                                             </dependency>

                                            在彈出的浮層中點擊“Enable Auto-import”即可。

                                            然后在 src/main/test/java 文件夾下面可以新建 Java Class 進行測試類的編寫。

                                            將被測試的類 放在 src/main/java 的文件夾下。

                                            ?

                                          6. 使用Maven生成JUnit報告

                                            Maven 本身并不是一個單元測試框架,能做的只是在構建執行到特定生命周期階段的時間,通過插件來執行 JUnit 的測試用例。這個插件就是 maven-surefire-plugin,可以稱之為測試運行器。

                                            默認情況下,maven-surefire-plugin 的 test 目標會自動執行測試用例源碼路徑(默認為 src/main/test/java/)下所有符合一組命名模式的測試類。這組模式為:

                                            | 模式 | 描述 | | ------------------- | ------------------------------- | | **/Test*.java | 任何子目錄下所有命名以 Test 開頭的 Java 類 | | **/*Test.java | 任何子目錄下所有命名以 Test 結尾的 Java 類 | | **/*TestCase.java | 任何子目錄下所有命名以 TestCase 結尾的 Java 類 |

                                            按照上述描述的模式,添加以下依賴:

                                            <plugin>
                                              <groupId>org.apache.maven.plugins</groupId>
                                              <artifactId>maven-surefire-plugin</artifactId>
                                              <version>2.19.1</version>
                                              <configuration>
                                                <skipTests>false</skipTests>
                                                <source>1.8</source>
                                                <target>1.8</target>
                                                <includes>
                                                  <include>**/*Tests.java</include>
                                                  <include>**/*TestCase.java</include>
                                                </includes>
                                              </configuration>
                                            </plugin>

                                            然后在需要運行的目錄中,執行 mvn test ,便可完成測試,并生成報告。默認情況下,maven-surefire-plugin 會在項目的 target/surefire-reports 目錄下生成兩種格式的錯誤報告:

                                            • 簡單文本格式
                                            • 與 JUnit 兼容的 XML 格式
                                            • </ul>

                                              這樣的報告對于獲得信息足夠了,XML 格式的測試報告主要是為了支持工具的解析,是 Java 單元測試報告的事實標準。

                                              </li> </ul>

                                              3.3 單元測試框架在自動化測試中的應用

                                              • 自動化測試的介紹

                                                當前,軟件測試貫穿到整個軟件開發生命周期的全過程中,不再停留在編程之后的某個階段,尤其是敏捷開發開始廣泛的應用于互聯網行業以后,敏捷測試就把軟件測試解釋為 對軟件產品質量的持續評估 。在敏捷方法中,持續測試被提倡。當前的持續測試的實施,主要依托于持續集成。

                                                自動化測試:以人為驅動的測試行為轉化為機器執行的一種過程

                                                這里我們使用 Selenium 工具進行自動化測試的應用。

                                                Selenium is a suite oftools to automate web browsers across many platforms.

                                                selenium硒, /s?'lini?m/

                                                Selenium是開源的自動化測試工具,它主要是用于Web 應用程序的自動化測試,不只局限于此,同時支持所有基于web 的管理任務自動化。

                                                Selenium 是用于測試 Web 應用程序用戶界面 (UI) 的常用框架。它是一款用于運行端到端功能測試的超強工具。您可以使用多個編程語言編寫測試,并且 Selenium 能夠在一個或多個瀏覽器中執行這些測試。

                                              • 使用 JUnit + Selenium 進行自動化測試

                                                接下來我們使用 Junit + Selenium 構建自動化測試

                                                步驟如下:

                                                • 安裝 Java 和 IDEA

                                                • 使用 IDEA 創建 Maven Project,并使用如下 pom.xml 文件

                                                  <?xml version="1.0" encoding="UTF-8"?>
                                                  <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                                                           xmlns="http://maven.apache.org/POM/4.0.0"
                                                           xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
                                                      <modelVersion>4.0.0</modelVersion>
                                                      <parent>
                                                          <groupId>org.seleniumhq.selenium</groupId>
                                                          <artifactId>selenium-parent</artifactId>
                                                          <version>2.53.1</version>
                                                      </parent>
                                                      <artifactId>selenium-server</artifactId>
                                                      <name>selenium-server</name>
                                                      <dependencies>
                                                          <dependency>
                                                              <groupId>org.seleniumhq.selenium</groupId>
                                                              <artifactId>selenium-java</artifactId>
                                                              <version>${project.version}</version>
                                                          </dependency>
                                                          <dependency>
                                                              <groupId>org.seleniumhq.selenium</groupId>
                                                              <artifactId>selenium-remote-driver</artifactId>
                                                              <version>${project.version}</version>
                                                          </dependency>
                                                          <dependency>
                                                              <groupId>commons-io</groupId>
                                                              <artifactId>commons-io</artifactId>
                                                          </dependency>
                                                          <dependency>
                                                              <groupId>org.apache.commons</groupId>
                                                              <artifactId>commons-exec</artifactId>
                                                          </dependency>
                                                          <dependency>
                                                              <groupId>junit</groupId>
                                                              <artifactId>junit</artifactId>
                                                          </dependency>
                                                          <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-csv -->
                                                          <dependency>
                                                              <groupId>org.apache.commons</groupId>
                                                              <artifactId>commons-csv</artifactId>
                                                              <version>1.4</version>
                                                          </dependency>
                                                          <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
                                                          <dependency>
                                                              <groupId>mysql</groupId>
                                                              <artifactId>mysql-connector-java</artifactId>
                                                              <version>6.0.3</version>
                                                          </dependency>
                                                      </dependencies>
                                                      <build>
                                                          <plugins>
                                                              <plugin>
                                                                  <groupId>org.apache.maven.plugins</groupId>
                                                                  <artifactId>maven-surefire-plugin</artifactId>
                                                                  <version>2.19.1</version>
                                                                  <configuration>
                                                                      <skipTests>false</skipTests>
                                                                      <includes>
                                                                          <include>**/*Tests.java</include>
                                                                      </includes>
                                                                  </configuration>
                                                              </plugin>
                                                          </plugins>
                                                      </build>
                                                  </project>

                                                  ?

                                                • 在 src/test/java 下創建 Java Class 進行編寫自動化測試腳本。腳本如下:

                                                  public class RanzhiTestCase{
                                                      // 聲明成員變量
                                                      private WebDriver driver;
                                                      private String baseUrl;

                                                  @Before
                                                  public void setUp(){
                                                      this.driver = new FirefoxDriver();
                                                      this.baseUrl = "http://localhost:808/ranzhi/www";
                                                  
                                                  @After
                                                  public void tearDown(){
                                                      this.driver.quit();
                                                  }
                                                  
                                                  @Test
                                                  public void testLogIn() {
                                                      // 聲明局部變量,傳遞全局的driver給它進行操作
                                                      WebDriver driver = this.driver;
                                                      // 步驟1
                                                      // 用局部變量driver 打開然之的登錄地址
                                                      driver.get(baseUrl);
                                                      // 讓java代碼停止運行1秒鐘,等待瀏覽器進一步響應
                                                      try {
                                                          Thread.sleep(1000);
                                                      } catch (InterruptedException e) {
                                                          e.printStackTrace();
                                                      }
                                                  
                                                      // 斷言:檢查是否打開了正確的登錄地址
                                                      Assert.assertEquals("登錄頁面打開錯誤",
                                                              this.baseUrl + "/sys/user-login-L3JhbnpoaS93d3cvc3lzLw==.html",
                                                              driver.getCurrentUrl());
                                                      // 步驟2
                                                      // 輸入用戶名 密碼 進行登錄
                                                      // 輸入用戶名
                                                      WebElement accountElement = driver.findElement(By.id("account"));
                                                      accountElement.clear();
                                                      accountElement.sendKeys("admin");
                                                      // 輸入密碼
                                                      WebElement passwordElement = driver.findElement(By.id("password"));
                                                      passwordElement.clear();
                                                      passwordElement.sendKeys("123456");
                                                      // 點擊登錄按鈕
                                                      driver.findElement(By.id("submit")).click();
                                                  
                                                      try {
                                                          Thread.sleep(2000);
                                                      } catch (InterruptedException e) {
                                                          e.printStackTrace();
                                                      }
                                                      Assert.assertEquals("登錄頁面登錄跳轉失敗",
                                                              this.baseUrl + "/sys/index.html",
                                                              driver.getCurrentUrl());
                                                  }
                                                  }</code></pre> </li> 
                                                   <li> <p>Selenium 推薦的 Page Object 設計模式進行方案設計</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/15e637376684dc263f38f726e9917eaf.png"></p> </li> 
                                                  </ul> </li> 
                                                  

                                                  </ul>

                                                  3.4 使用 Jenkins 進行持續質量審查

                                                  • 什么持續集成

                                                    持續集成,Continuous integration ,簡稱CI。

                                                    隨著軟件開發復雜度的不斷提高,團隊開發成員間如何更好地協同工作以確保軟件開發的質量已經慢慢成為開發過程中不可回避的問題。尤其是近些年來,敏捷(Agile) 在軟件工程領域越來越紅火,如何能再不斷變化的需求中快速適應和保證軟件的質量也顯得尤其的重要。

                                                    持續集成正是針對這一類問題的一種軟件開發實踐。首先我們看一下,敏捷教父 Martin Fowler 對持續集成的定義:

                                                    Martin Fowler: Continuous Integration is a software development practice where members of a team integrate their work frequently, usually each person integrates at least daily - leading to multiple integrations per day. Each integration is verified by an automated build (including test) to detect integration errors as quickly as possible. Many teams find that this approach leads to significantly reduced integration problems and allows a team to develop cohesive software more rapidly.

                                                    具體定義:持續集成式一種軟件開發實踐。它倡導團隊的成員必須經常的集成它們的工作,通常至少每天一次甚至更多次集成。每次集成都需要通過自動化的構建(包括編譯代碼、構建應用、部署程序以及自動化測試)來驗證,從而盡早盡快的發現集成中的錯誤。大量的團隊利用這樣的方式來更快的開發內聚的軟件。大大減少此過程中的集成問題。

                                                    具體的流程圖如下:

                                                    持續集成強調開發人員提交了新代碼之后,立刻進行構建、(單元、自動化)測試。根據測試結果,我們可以確定新代碼和原有代碼能否正確地集成在一起。

                                                    首先,解釋下集成。我們所有項目的代碼都是托管在SVN服務器上。每個項目都要有若干個單元測試,并有一個所謂集成測試。所謂集成測試就是把所有的單元測試跑一遍以及其它一些能自動完成的測試。只有在本地電腦上通過了集成測試的代碼才能上傳到SVN服務器上,保證上傳的代碼沒有問題。所以,集成指集成測試。

                                                    再說持續。不言而喻,就是指長期的對項目代碼進行集成測試。既然是長期,那肯定是自動執行的,否則,人工執行則沒有保證,而且耗人力。對此,我們有一臺服務器,它會定期的從SVN中檢出代碼,并編譯,然后跑集成測試。每次集成測試結果都會記錄在案。完成這方面工作的就是下面要介紹的Jenkins軟件。當然,它的功能遠不止這些。在我們的項目中,執行這個工作的周期是1天。也就是,服務器每1天都會準時地對SVN服務器上的最新代碼自動進行一次集成測試。

                                                  • Jenkins環境搭建

                                                    Jenkins,原名Hudson,2011年改為現在的名字,它 是一個開源的實現持續集成的軟件工具。

                                                    Hudson是在2004年的夏天由Sun公司開發的(就是開發Java的那家),2005年2月開源并發布了第一個版本。Hudson發布的時候CruiseControl是CI界的老大哥,但是很快,在大約2007年的時候Hudson已經超越CruiseControl。2008年5月的JavaOne大會上,Hudson獲得了開發解決方案類的Duke's Choice獎項。從此,小弟翻身做大哥,Hudson成為CI的代名詞。

                                                    2009年6月,Oracle收購Sun。2010年9月,Oracle注冊了Hudson的商標,然后就沒有然后了。原Hudson的成員創建了Jenkins。

                                                    Jenkins 能實施監控集成中存在的錯誤,提供詳細的日志文件和提醒功能,還能用圖表的形式形象地展示項目構建的趨勢和穩定性。

                                                    需要從官網下載Jenkins的文件,在本地安裝Java(jdk)的環境以后,直接執行以下語句進行安裝:

                                                    java -jar jenkins.war

                                                    隨后訪問 http://localhost:8080即可

                                                    最后,將Jenkins安裝成Windows服務啟動。

                                                    在Jenkins的主頁中選擇 Manager Jenkins

                                                    接下來選擇 Install as Windows Service

                                                    在Installation Directory中選擇jenkin的安裝路徑。這里會默認產生,直接點擊Install就可以了。

                                                    隨后我們點擊Yes,然后可以看到Windows服務中添加了Jenkins的服務,并已經設置為自動和啟動狀態。

                                                  • 部署Jenkins與運行

                                                    • 創建Jenkins Job

                                                      Jenkins提供了四種類型的Job:

                                                      1. 構建一個自由風格的軟件項目:這個是Jenkins的主要功能,可以構建一個你自己需要的項目。
                                                      2. 構建一個maven項目:這是基于maven構建的項目,整個過程將會基于你的.pom文件進行構建,大大減輕構建的配置
                                                      3. 構建一個多配置項目:這種項目適用多配置的項目,比如多個平臺定制的構建,多個測試環境的部署構建等。
                                                      4. 監控一個外部的任務:這種事允許你記錄和執行不在Jenkins中的Job,這些Job可以運行在遠程的主機上,Jenkins通過遠程自動構建,作為一個控制面板而存在。
                                                    • 運行Jenkins Job

                                                      運行Job只需要在頁面的左側選中已經列出的項目,進行操作就可以了。選擇立即構建,便可以進行自動構建的工作了。

                                                    • 定時構建 Job

                                                      目前有一個 每日構建 的概念。

                                                      Daily Build,每日構建。需要Jenkins在每日固定的時間進行代碼自動構建、集成和測試的工作。那么需要定制執行時間。Jenkins的自動構建定制時間是遵循cron語法的。具體來說,每一行包括了5個用白空格或者Tab隔開的字段,分別是: MINUTE HOUR DOM MONTH DOW 。具體的格式我們參考下圖

                                                      | 字段 | 說明 | 示例 | | ------ | ---------------------------------------- | ---- | | MINUTE | Minutes within the hour (0–59) | 30 | | HOUR | The hour of the day (0–23) | 17 | | DOM | The day of the month (1–31) | 1 | | MONTH | The month (1–12) | 5 | | DOW | The day of the week (0–7) where 0 and 7 are Sunday. | 5 |

                                                      然后每個格式,都可以由 * , - , / 和 , 4種字符組成:

                                                      • * 代表所有可能的值
                                                      • - 代表整數之間的范圍
                                                      • / 代表指定的時間的間隔頻率
                                                      • , 代表指定的列表范圍

                                                      命令的格式參考和示例: 注意時間是倫敦時間

                                                      | 分鐘 | 小時 | 天 | 月份 | 星期 | 命令格式 | 描述 | | ---- | ---- | ------- | ---- | ---- | ------------------ | -------------------- | | H | 16 | 1,10,20 | | | H 16 1,10,20 * * | 每個月的1,10,20日的16:00執行 | | H | 16 | | | 1-5 | H 16 * * 1-5 | 每個周的周一到周五的16:00執行 | | 30 | 17 | | | 1,5 | 30 17 * * 1,5 | 每個周的周一和周五的17:30執行 |

                                                      </li>
                                                    • Jenkins Job 示例

                                                      ## 1. 創建SVN倉庫

                                                      https://172.31.95.168/svn/DemoRepo/

                                                      ciuser / ciuser

                                                      2. 從SVN倉庫簽出文件到CI Server

                                                      3. 構建 build

                                                      4. 部署 deploy

                                                      4.1 備份目標文件夾

                                                      若有ranzhi_bak 需要先刪除

                                                      rd /s /q c:\xampp\htdocs\ranzhi_bak
                                                      

                                                      把 htdoc的ranzhi改成 ranzhi_bak

                                                      xcopy c:\xampp\htdocs\ranzhi c:\xampp\htdocs\ranzhi_bak\ /s /e /y
                                                      rd /s /q c:\xampp\htdocs\ranzhi
                                                      

                                                      4.2 復制構建的版本到目標文件夾

                                                      把workspace的 ranzhi 復制到 htdocs下面

                                                      xcopy "%WORKSPACE%\ranzhi" c:\xampp\htdocs\ranzhi\ /s /e /y
                                                      

                                                      4.3 恢復配置文件

                                                      復制 ranzhi_bak\config\my.php 到 ranzhi\config\my.php

                                                      xcopy c:\xampp\htdocs\ranzhi_bak\config\my.php c:\xampp\htdocs\ranzhi\config\ /e
                                                      

                                                      5. 自動化測試

                                                      python D:\Git\Coding\BWFTraining\3.03_Selenium\codes\weekend2demo\ranzhi_test_runner.py</code></pre>

                                                      ?

                                                      </li> </ul> </li> </ul>

                                                      單元測試在路上

                                                      • [探討]單元測試在敏捷開發的場景下對技術和產品的影響?
                                                        • 技術人員需要掌握的技術棧
                                                        • 企業產品的質量度量與提升
                                                        </li> </ul>

                                                         

                                                        來自:http://gitbook.cn/books/58f5a5c8b7f44bd669c11b79/index.html

                                                         

 本文由用戶 AstridAugus 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!