單元測試學習筆記

示例代碼太少,之後會逐漸補上。html

目錄:java

綜述

若是你查過一些關於單元測試的資料,你可能會和我同樣發現一個問題。有一些文章在說到單元測試的時候,提到了要作邊界測試,要考慮各類分支;也有一些文章則說的是修改原有代碼,例如依賴隔離;還有一些文章說的是測試的框架的使用,例如 Junit 。那麼它們之間有着什麼樣的聯繫呢?android

最開始,咱們可能更關注邊界測試和分支測試。但遺憾的是,這方面的資料相對來講較少。更多的是依賴隔離這類的文章。爲何?segmentfault

由於有不少代碼是沒法被測試的。安全

可以被測試的代碼須要知足某些條件。你可能會以爲很麻煩,作單元測試還要爲了知足這些條件去修改原來的代碼。事實上,知足這些條件能使你的代碼變得健壯。若是你寫的代碼是沒法被測試的,那麼你的首要任務就是將它們重構爲可測試的單元。要想知道如何寫出可測試的代碼,就得了解 Mock 和 Stub 。這也就是你在看一些加減乘除單元測試例子以後,仍然不知道怎麼測試本身的代碼的緣由。(每次看到這樣的文章就好氣啊_(:з」∠)_)網絡

可是即便重構了,還有一個問題。你總得寫代碼來執行對其餘代碼進行測試吧?這部分的代碼可能很複雜,也可能變得難以維護。因而測試框架就出現了,幫你減輕作測試的負擔。數據結構

簡單說,它們三者之間的關係是:先重構已有代碼,使其成爲可測試的單元,爲接下去的測試作準備。接着寫出對這些單元進行測試的代碼,驗證結果。爲了使測試代碼易於編寫和維護,藉助測試框架。框架

單元測試時所面臨的問題

爲了使代碼可被測試,須要對其進行重構。在這個過程當中會遇到一些問題:ide

  • 一個類的方法裏包含了其餘類的方法,怎麼測試?
  • 若是代碼依賴於 Web 服務,例如請求某個網站的數據,怎麼測試?
  • 一個類的方法裏包含了該類的其餘方法,要怎麼測試?
  • 一個類的方法有不少個對數據處理的步驟,是要測試最終結果,仍是要對每一個處理的步驟可能出現的問題進行測試?
  • 一個方法沒有返回值(即 void )怎麼辦?——交互測試

對於前兩個問題,能夠用依賴隔離來解決。《單元測試的藝術》的3.1有個用來理解依賴隔離的例子:
航天飛機在送入太空以前,要先進行測試,不然飛到一半出了問題怎麼辦?
而有一部分測試是確保宇航員已經準備好進入太空。可是你又不能讓宇航員真的坐實際的航天飛機去測試。有個辦法就是建造一套仿真系統,模擬航天飛機控制檯的環境。輸入某種特定的外部環境,而後看宇航員是否執行了正確的操做。
在這個例子中,經過模擬外部環境來解除了對實際外部環境(航天飛機進入太空)的依賴。一樣的思路能夠用到測試中。函數

依賴隔離

先從寫出可以測試的代碼開始提及吧。

參考文章:Android單元測試 - 如何開始?

這裏的依賴指的是:當前 類A 須要調用 類B 的方法,則說 A 依賴於 B 。

隔離方法:

  1. 將 B 改爲一個 接口C 。
  2. 將 A 中的 B類 出現的位置替換爲 接口C 。

A 和 B 隔離先後對比:
隔離前:A -> B
隔離後:A -> C -> B

在項目實際代碼以及測試代碼中使用不一樣的B:

  • 在項目執行代碼中:傳入 類A 的對象是 接口C 的一個 派生類D (實現了 接口C )。 類D 是 項目中實際運行的代碼,提供了對接口的完整實現。A -> C -> D
  • 在單元測試的代碼(獨立於項目執行代碼,發佈軟件時要把這部分刪掉)中:傳入 A 的對象也是實現了 接口C 的一個 派生類E 。可是這個類與D不一樣,它提供的實現可能只是一個return。從而模擬(Mock)了派生類D的特定行爲,使得在測試的時候,不須要使用D類。A -> C -> E

這樣作的好處是,一旦隔離完成,之後就沒必要大幅度修改A。在隔離的時候,要將全部依賴項改成從外部傳入。這就須要給類A添加一個set方法,傳入接口C的實現(implement),即上面的D和E。

依賴隔離的例子

類A:

public class Calculater {

    public double divide(int a, int b) {
        // 檢測被除數是否爲0
        if (MathUtils.checkZero(b)) {
            throw new RuntimeException("divisor must not be zero");
        }

        return (double) a / b;
    }
}

它調用了類B(MathUtils)的 checkZero 方法。因而咱們說類A依賴於類B的 checkZero 方法。須要注意的是這個 MathUtils 不是從外部傳入的

類B是一個具體實現的類:

public class MathUtils {
    public static boolean checkZero(int num) {
        return num == 0;
    }
}

在知道產生依賴以後,要將類B改爲一個接口(方法名前綴I表示這是一個接口Interface):

public interface IMathUtils {
    public boolean checkZero(int num);
}

在類A的代碼中,將B替換成該接口:

public class Calculater {

    private IMathUtils mMathUtils = new MathUtils();   // 這裏的代碼改動了
    
    // 這裏添加了set方法。向該類傳入了mathUtils
    public void setMathUtils(IMathUtils mathUtils){
        mMathUtils = mathUtils;
    }
    
    public double divide(int a, int b) {
        if (mMathUtils.checkZero(b)) { // 這裏的代碼改動了,將靜態類改爲對象
            throw new RuntimeException("divisor must not be zero");
        }
        return (double) a / b;
    }
}

以前的B是一個靜態類,不須要聲明,但改爲接口後須要聲明。

接口的實現:

  • 對於實際運行的代碼,須要一個類去實現 IMathUtils 接口,而後傳入 Calculater 。
    修改類B:
    JAVA public class MathUtils implements IMathUtils{ public boolean checkZero(int num) { return num == 0; } }

  • 對於用於測試的代碼,也須要一個類實現 IMathUtils 接口,而後傳入 Calculater 。但不一樣的是,這個類的實現可能只需添加一個 return 語句,不用細緻實現。
    老是正確的接口:
    JAVA public class FakeMathUtils implements IMathUtils{ public boolean checkZero(int num) { return true; } }
    return的時候,能夠設一個變量,方便配置不一樣取值,不然還得建立新的類。
    ```JAVA
    public class FakeMathUtils implements IMathUtils{
    public boolean isZero = true;

    public boolean checkZero(int num) {
          return isZero;
      }

    }
    ```

交互測試

若是一個特定的工做單元的最終結果是調用一個第三方對象(而不是返回某個值,即 void ),你就須要進行交互測試。

這個第三方對象不受你的控制。你沒法查看它的源代碼,或者這不是你負責測試的部分。所以你只需確保傳給它的參數是正確的就能夠了。

那麼如何確保傳過去的參數是正確的?

在這以前,要確保已經依賴隔離。

假設接口爲:

public interface IPerson {
    ...
    public void doSomethingWithData(String data);
}

待測試類的某個方法:

public class A {
    private String data = "";
    ...

    public void methodA(IPerson person) {
        ...
        person.doSomethingWithData(data);
    }

    public void setData(String data) {
        this.data = data;
    }
}

真正使用的 Person 類是如何實現的呢?假設咱們無從得知。咱們的任務是保證傳入的 data 是符合咱們預期的。只要傳入的內容符合預期,那麼就說明咱們要測試的方法是沒問題的。

僞實現:

public class FakePerson implements IPerson {
    private String data = "";

    ...
    public void doSomethingWithData(String data) {
        this.data = data;
    }

    public String getData() {
        return data;
    }
}

在調用 methodA 的時候,傳入 FakePerson 實例。

A test = new A();
test.setData("hahahaha");

IPerson fakePerson = new FakePerson();
test.methodA(fakePerson);

Assert.AssertEquals("hahahaha", fakePerson.getData());

僞對象 FakePerson 在被 測試類A 的 methodA 方法中調用,該方法會給僞對象傳入某個信息。

僞對象 FakePerson 不對該信息進行進一步處理,只是賦值給類成員變量存儲起來。

因爲僞對象是從外部傳入的 test.methodA(fakePerson); ,所以能夠直接在外部獲取存儲的信息 fakePerson.getData() 。在assert的時候,獲取該信息,查看是否和預期的一致。

參考:

《單元測試的藝術》第四章

Android單元測試在蘑菇街支付金融部門的實踐

單元測試框架

在測試以前,要建立一個專門用於測試的類。這個類的類名以Test結尾。在類裏面添加測試方法,測試方法名前面要加上 test ,接在 test 後面的是被測試的方法名。在該方法內作三件事:

  1. 測試以前須要準備的數據,例如 new 出要測試的類——Setup
  2. 執行要測試的類的方法——Action
  3. 最後添加 Assert 以驗證結果——Verify

測試框架裏的 AssertXxx 是什麼玩意兒?

咱們寫的測試代碼在運行的時候會產生一些結果,驗證這些結果是否符合預期的一個低效方法就是將這些結果輸出到控制檯(Console)或者文件,而後你本身用眼睛一個個去對比。

若是你懶得去比呢?又或者說你對比的時候以爲沒錯,可是其實是由於一個1l的錯誤致使你沒有發現呢?

就讓 Assert 來幫你解決這些煩人的問題吧! Assert (中文爲:斷言)就是讓你將預期的結果和程序運行的結果傳入它的方法裏面,由它來替你作對比的事情。

例如一個測試結果是否相等的 Assert :
assertEquals(你本身算出的結果, 程序運行的結果);
若是兩個結果不一樣,即程序運行的結果不符合你的預期,那麼它就會提示你這裏出現了錯誤。

今後,你就從幾百甚至是幾萬條的測試代碼輸出的對比中解放出來,大大節約了時間。

有些文章標題看着像是介紹單元測試,其實是介紹單元測試框架。測試框架(Junit,Nunit等)其實是提供便於測試的方案的框架,學習這些內容是學習框架的結構,以及如何使用框架定義的各類 assert ,而不是學習單元測試的方法。這二者要區分開來!!!!!

快捷實現用於測試接口的框架(Mockito)

對於剛纔那個接口 IMathUtils ,咱們能夠不用再新建一個類去實現它,而是交給 Mockito 。
IMathUtils mathUtils = mock(IMathUtils.class); // 生成mock對象

when(mathUtils.checkZero(1)).thenReturn(false); // 這裏是快捷實現。它告訴 Mockito :若是在下面代碼調用了mathUtils.checkZero()並傳入參數1,那麼就讓調用這個方法的地方返回false這個值。

作好以上準備後

  1. 單元測試整體上須要作些什麼?
    • 只考慮代碼在最正確的操做和條件下可否得出正確的結果
    • 在數據的邊界條件下可否獲得正確的結果
    • 代碼在全部可能的錯誤數據下可否給出錯誤提示或者不至於崩潰
    • 如何消除依賴隔離
  2. 單元測試的任務(摘自:Java單元測試(Junit+Mock+代碼覆蓋率)
    • 接口功能測試:用來保證接口功能的正確性。
    • 局部數據結構測試(不經常使用):用來保證接口中的數據結構是正確的
      • 好比變量有無初始值
      • 變量是否溢出
    • 邊界條件測試
      • 變量沒有賦值(即爲NULL)
      • 變量是數值(或字符) 時
        • 主要邊界:最小值,最大值,無窮大(對於 double 等)
        • 溢出邊界(指望異常或拒絕服務):Min - 1,Max + 1
        • 臨近邊界:Min + 1,Max - 1
      • 變量是字符串時
        • 應用上面提到的字符變量的邊界
        • 空字符串
        • 對字符串長度應用數值變量的邊界
      • 變量是集合時
        • 空集合(Empty)
        • 對集合的大小應用數值變量的邊界
        • 調整次序:升序、降序
      • 變量有規律時
        • 好比對於Math.sqrt,給出n^2-1,和n^2+1的邊界
    • 全部獨立執行通路測試:保證每一條代碼,每一個分支都通過測試
      • 代碼覆蓋率
        • 語句覆蓋:保證每個語句都執行到了
        • 斷定覆蓋(分支覆蓋):保證每個分支都執行到
        • 條件覆蓋:保證每個條件都覆蓋到 true 和 false (即 if 、 while 中的條件語句)
        • 路徑覆蓋:保證每個路徑都覆蓋到
      • 相關軟件
        • Cobertura:語句覆蓋
        • Emma: Eclipse插件Eclemma
    • 各條錯誤處理通路測試:保證每個異常都通過測試
  3. Android 單元測試的任務(摘自:Android單元測試在蘑菇街支付金融部門的實踐
    • 全部的Model、Presenter/ViewModel、Api、Utils等類的public方法
    • Data類除了getter、setter、toString、hashCode等通常自動生成的方法以外的邏輯部分
    • 自定義View的功能:好比set data之後,text有沒有顯示出來等等,簡單的交互,好比click事件,負責的交互通常不測,好比touch、滑動事件等等。
    • Activity的主要功能:好比view是否是存在、顯示數據、錯誤信息、簡單的點擊事件等。比較複雜的用戶交互好比onTouch,以及view的樣式、位置等等能夠不測。由於很差測。

重構與單元測試

在單元測試前要重構,在重構前要編寫集成測試。
集成測試 ——> 重構 ——> 單元測試
重構的過程當中,每次只作少許的改動。儘量多的運行集成測試,以此瞭解重構是否使得系統原有的功能被破壞。
要點:關注系統中你須要修復或者添加功能的部分,不要在其餘部分浪費精力。其餘部分等到須要處理的時候再考慮。

修復 BUG 或添加新功能的單元測試

先編寫一個單元測試,這個測試針對於這個 BUG 。因爲它是一個 BUG ,因此顯然這個單元測試一開始給出的結果會是失敗的。此時你修復 BUG ,並運行測試。若是測試成功,則表示你成功修復了這個 BUG ;若是測試失敗,則表示 BUG 仍然存在。

換句話說,這個單元測試暴露了這個 BUG 。 BUG 原本沒看出來,而這個單元測試的失敗代表了 BUG 的存在。

添加新功能也是一樣。寫出一個會失敗的測試,表示缺乏這個功能。而後經過修改代碼使得測試經過,就代表你成功添加了新功能。

得到接口的幾種方法(基於值和狀態的測試)

在本篇的 MathUtils 例子中,經過setMathUtils()傳入 IMathUtils 的實現。這是經過 getter 和 setter 對類的成員變量操做的方法。這種方法稱爲依賴/屬性注入。除此以外,還有其餘方法。

  • 在方法調用點注入僞對象(《單元測試的藝術》3.4.6)
    這種方法與屬性注入須要先獲取實例再傳入不一樣,它經過在構造函數裏使用工廠方法獲取實例。
    • 方案一:工廠類
      在被測試類的構造方法裏執行了靜態的工廠方法。不過工廠方法執行以前,經過 setter 傳入用於測試的接口實現。這種方法與屬性注入的不一樣之處在於,將 set 方法移入另外建立的工廠類。在測試的時候你徹底不須要管被測試類,只須要對工廠類進行操做就能夠。
      須要注意什麼問題?你須要瞭解什麼代碼會在何時調用這個工廠,根據時機 set 進所需的工廠實現。
    • 方案二:本地工廠方法
      不使用工廠類,而是在被測試類裏新建一個工廠方法。將被測試類設置爲抽象類,完整地實現了除工廠方法外的全部方法,讓子類繼承並重寫工廠方法。測試時有測試的實現,實際運行時有運行的實現。
      何時應該使用?模擬給被測試代碼的輸入。
      何時不該該使用?測試代碼對服務的調用是否正確時。
      當被測試代碼已是依賴隔離或者應用了屬性注入的時候,不考慮。若沒有,則優先考慮。
  • 構造函數注入,賦值給類的成員變量
    建立新的構造函數,或者在原有構造函數上添加參數。若是類須要注入多個依賴,則會下降代碼的可讀性和可維護性(構造函數的參數個數可能變化的通病)。
    • 優化方案一:參數對象重構。將參數整合爲一個對象,傳入該對象。
    • 優化方案二:控制反轉。控制反轉的一個例子是 JAVA 的反射機制,根據類名生成對象。控制反轉能夠看作是將工廠方法中的生成對象的代碼改到 XML 文件中。
    • 何時使用?第一:使用控制反轉容器的框架。第二:想告訴 API 使用者這些參數是必須的(若是是可選的,則使用 getter 和 setter )。
    • 須要注意什麼問題?大多數人不知道什麼是控制反轉原則。這意味着你一旦寫出方案二這樣的代碼,就須要在別人不懂的時候教他。
  • 把參數放到須要被測試的方法的參數列表裏

一些補充

  1. 應該對哪些代碼編寫單元測試?哪些代碼不太須要編寫單元測試?
    不常常改動的代碼,特別是底層核心的代碼須要編寫單元測試。常常改動的代碼就不太須要編寫單元測試。畢竟你剛寫完單元測試不久,整個代碼就被修改了,你得再從新編寫單元測試。

  2. Mock/Stub
    Mock 和 Stub 是兩種測試代碼功能的方法。 Mock 測重於對功能的模擬。 Stub 測重於對功能的測試重現。好比對於 List 接口, Mock 會直接對 List 進行模擬( assert 寫在調用 List 的 test 方法裏面);而 Stub 會新建一個實現了 List 的 TestList ,在其中編寫測試的代碼( assert 寫在這個 TestList 裏面)。《單元測試的藝術》4.2

    Stub 不會使測試失敗,僅僅是用來模擬各類場景。 Mock 相似 Stub ,但它還能使用 assert 。

    優先選擇 Mock 方式,由於 Mock 方式下,模擬代碼與測試代碼放在一塊兒,易讀性好,並且擴展性、靈活性都比 Stub 好。但須要注意,一個測試有多個 Stub 是可行的,但有多個 Mock 就會產生麻煩,由於多個 Mock 對象說明你同時測試了多個事情。編寫測試代碼時,不對 Stub 進行 assert ,而是統一到最後由 Mock 進行 assert 。若是你對明顯是用作 Stub 的僞對象進行了斷言,這屬於過分指定。《單元測試的藝術》4.5

    若是在一個單元測試中,驗證了多個點,你可能沒法知道究竟是哪一個點出了錯。應該儘量分離。

  3. 想作單元測試結果作成集成測試
    若是既要請求網絡,又要保存數據,還要顯示界面,那就是集成測試了。

  4. 在使用斷言確認字符串的時候,應該把整個預期字符串都寫上麼?
    在《重構:改善既有代碼的設計》裏面,有個測試讀取文件的例子。

  5. 單元測試框架中的setup()
    • setup()方法應該初始化全部測試方法都須要的對象。至於只有某個測試方法用到的對象,交給這個測試方法來初始化。
    • 防止過分重構setup()方法。在重構時徵求同伴的意見。
    • 不要在setup中準備僞對象。
  6. 還須要注意什麼?
    • 編寫測試時,要時刻考慮到閱讀測試的人。想象一下他們第一次讀到代碼時的情形,確保他們不會生氣。
    • 一個單元測試方法不能調用另一個單元測試方法。若是想刪除重複代碼,那就抽取共同的代碼到另外一個方法中。
    • 隔離測試的方法:把你當前正在寫的測試當作系統中惟一的一個測試。可是要注意,你必須把單元測試可能修改的狀態恢復到初始值
    • 最安全的作法:每一個測試使用一個單獨的對象實例。
    • 想要進行流測試,最好使用某種集成測試框架。
    • 斷言和操做須要分離。在斷言的參數裏面,只傳入最終結果,其方法調用過程須要分離開。

參考:

相關文章
相關標籤/搜索