Android單元測試 - 如何開始?

基本單元測試框架

Java單元測試框架: Junit、Mockito、Powermockito 等;Android: Robolectric、AndroidJUnitRunner、Espresso 等。

最開始建議先學習 Junit & Mockito 。這兩款框架是java領域應用非常普及,使用簡單,網上文章非常多,官網的說明也很清晰。junit運行在 jvm 上,所以只能測試純java,若要測試依賴android庫的代碼,可以用mockito 隔離依賴 (下面會談及)。

Junit官網

Mockito官網

之后學習 AndroidJUnitRunner ,Google官方的android單元測試框架之一,使用跟 Junit 是一樣的,只不過需要運行在android真機或模擬器環境。由于 mockito 只在 jvm 環境生效,而android是運行在 DalvikART ,所以 AndroidJUnitRunner不能使用mockito

然后可以嘗試 Robolectric & EspressoRobolectric 運行在 jvm 上,但是框架本身引入了android依賴庫,所以可以做android單元測試,運行速度比運行在真機or模擬器快。但Robolectric也有局限性,例如不支持加載so,測試代碼也有點別扭。當然,robolectric可以配合junit、mockito使用。 Espresso 也是Google官方的android單元測試框架之一,強大就不用說了,測試代碼非常簡潔。Espresso本身運行在真機上,因此android任何代碼都能運行,不像junit&mockito那樣隔離依賴。缺點也是顯而易見,由于運行在真機,不能避免 “慢”

Robolectric官網

Android-testing-support-library官網

其實espresso應該是幾款框架中最簡單的,但筆者還是建議先學習junit&mockito。因為新手很可能會因為espresso的強大、簡單,而忽略了junit做單元測試帶來的巨大意義。例如,前文提到 “快速定位bug”、“提高代碼質量” ,espresso慢,有違“快速”;用espresso不用修改工程任何代碼,這不利于提高代碼質量。

本文主要介紹 junitmockito ,以及單元測試一些重要概念。

Junit

先給大家上兩段代碼壓壓驚:

public class Calculater {
    public int add(int a, int b) {
        return a + b;
    }
}

AssertEquals

單元測試用例:

public class CalculaterTest {

    Calculater calculater = new Calculater();

    @org.junit.Test
    public void testAdd() {
        int a = 1;
        int b = 2;

        int result = calculater.add(a, b);

        Assert.assertEquals(result, 3); // 驗證result==3,如果不正確,測試不通過
    }
}

以上是一個要測試的類 Calculater 和測試用例 CalculaterTest 。在 Intellij 或 Android Studio 對類 右鍵 -> run CalculaterTest ,用例中所有被 @org.junit.Test 注解的方法,就會被執行。

測試通過。

如果代碼改成 Assert.assertEquals(result, 4); ,測試會失敗。

Verify

verify 的作用,是驗證函數是否被調用(以及調用了多少次)。

public class CalculaterTest {

    @org.junit.Test
    public void testAdd2() {
        calculater = mock(Calculater.class);

        calculater.add(1, 2);

        verify(calculater).add(1, 2); // 驗證calculater.add(a, b)是否被調用過,且a==1 && b==2
        // 測試通過
    }
}

是不是很簡單?

Mockito

官網這樣描述:

Mockito is a mocking framework that tastes really good. It lets you write beautiful tests with a clean & simple API.

大概意思是, Mockito 是一個體驗很好的mocking框架,它可以讓你寫出漂亮、簡潔的測試代碼。

什么是mocking?下文會詳細說明。不如先讓你感受一下mockito代碼:

public interface IMathUtils {
    public int abs(int num); // 求絕對值
}
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class MockTest {

    public static void main(String[] args) {
        IMathUtils mathUtils = mock(IMathUtils.class); // 生成mock對象

        when(mathUtils.abs(-1)).thenReturn(1); // 當調用abs(-1)時,返回1

        int abs = mathUtils.abs(-1); // 輸出結果 1

        Assert.assertEquals(abs, 1);// 測試通過
    }
}

可以發現 IMathUtils 是一個接口,根本就沒有實現,用 Mockito 框架 mock 之后, IMathUtils.abs(-1) 就有 返回值1 了。這就是Mockito神奇的地方!Mockito代理了 IMathUtils.abs(num) 的行為,只要調用時符合 指定參數 (代碼中指定參數 -1 ),就可以得到 映射的返回值

Mockito的語法 when...thenReturn... 相當直觀,只要你小學有學英語^_^都能看懂。

讀者肯定認為Mockito用了Java代理,實際上要更高級一點,Mockito底層用了 CGLibgithub/cglib )做動態代理。

依賴隔離

依賴隔離,這是單元測試中一個 非常重要的概念 。一個單元的代碼,通常會有各種依賴。寫單元測試時,應該把這些依賴隔離,讓每個單元保持獨立。舉個例子:

public class Calculater {

    public double divide(int a, int b) {
        // 檢測被除數是否為0
        if (MathUtils.checkZero(b)) {
            throw new RuntimeException("dividend is zero");
        }

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

divide(a,b) 計算 a除以b ,但 被除數b 不應該為0,所以用 MathUtils.checkZero(b) 驗證 b==0 。咋看這里好像沒什么問題,但是,如果 MathUtils.checkZero 里面的判斷邏輯寫錯呢?例如:

public static boolean checkZero(int num) {
    return  num != 0; // bug
 }

如果不是 num==0 那么簡單,而是更復雜的算法呢?

因為 Calculater 引用的任何依賴,都可能出錯。 更糟糕的是 ,如果用 junit 做單元測試,依賴里面可能是 Android庫 或者 jni native 方法,依賴方法一執行就會報錯。以上的各種原因,都會 影響單元測試的結果 。所以,我們對代碼做如下改進:

public class Calculater {

    IMathUtils mathUtils;

    public double divide(int a, int b) {
        if (mathUtils.checkZero(b)) {
            throw ...
        }
        return (double) a / b;
    }
}
public interface IMathUtils {
    public boolean checkZero(int num);
}

我們可以在 Calculater 構造方法傳入 IMathUtils 派生類,又或者用 setter 。在項目執行代碼中,傳 MathUtils ,而單元測試時,可以寫一個 MathUtilsTest 繼承 IMathUtils ,傳給 Calculater 。只要保證 MathUtilsTest.checkZero() 正確就行。經過這么重構, Calculater 就不依賴原來的 MathUtils ,單元測試時可以替換專門的實現,達到了 依賴隔離的目的

有同學會問,這樣豈不是每個依賴都要寫一個專門給單元測試的類嗎?這就等于拷貝多一份代碼,并且寫各種接口,而且不能保證單元測試的類一定正確。

說得很有道理。筆者為了盡量簡單地演示代碼,舉了一個非常簡單的例子。我們如何讓單元測試更簡潔,并且讓它閱讀起來更有意義呢?

Mock

為了更好地解決上述問題,我們引入 Mock 概念。 Mock ,翻譯為模擬,在單元測試 mock 可以 模擬返回數據,也可以模擬接口、類的行為

什么是 模擬行為 ?例如剛才 mathUtils.checkZero(b) ,意義為:“當 mathUtils 調用 checkZero(num) ”時,判斷 num==0 ;又或者:“當調用 checkZero(0) 時返回 true , num 為其他值時返回 false ”,返回的 true、false 就是 模擬數據

例如,需要測試 a=2,b=1 和 a=2,b=0 調用 divide(a,b) 兩者結果分別是 2,拋出錯誤 ,使用 mockito 框架 mock mathUtils.checkZero()的行為 ,代碼如下:

public static void main(String[] args) {
    // 生成IMathUtils模擬對象
    IMathUtils mathUtils = mock(IMathUtils.class);

    when(mathUtils.checkZero(1)).thenReturn(false); // 當num==1時,checkZero(num)返回false
    when(mathUtils.checkZero(0)).thenReturn(true); // 當num==0時,checkZero(num)返回true

    Calculater calculater = new Calculater(mathUtils);

    assertEquals(calculater.divide(2,1), 2); // 驗證 divide(2,1) 結果是2

    try {
        calculater.divide(2, 0); // 預期拋出錯誤
        throw new RuntimeException("no expectant exception"); // 如果divide沒拋錯,則此處拋錯
    } catch (Exception e) {
        Assert.assertEquals(e.getMessage(), "dividend is zero"); // 驗證錯誤信息
    }
}

這段測試代碼可以 運行通過!

代碼剖析:

  • mock(IMathUtils.class) 生成 IMathUtils類的模擬對象 (稱mock對象)。這個mock對象調用任何方法都不會被實際執行;

  • when(mathUtils.checkZero(1)).thenReturn(false) ,當調用 checkZero(num) 并且 num==1 ,返回 false ,這里 mockito 模擬了 checkZero() 行為,并模擬了返回數據;

  • 所以, calculater.divide(2,1) 返回結果2, calculater.divide(2, 0) 拋出RuntimeException。

以上例子描述了,使用mockito模擬類方法和返回數據,通過mock隔離了 Calculater 對 IMathUtils 實現類的依賴,并通過單元測試,驗證了 divide() 的邏輯正確性。

條件覆蓋

無限條件

要驗證程序正確性,必然要給出所有可能的條件(極限編程),并驗證其行為或結果,才算是100%覆蓋條件。實際項目中,驗證邊界條件和一般條件就OK了。

還是上面那個例子,只給出兩個條件: a=2,b=1 和 a=2,b=0 , a=2,b=1 是一般條件, b=0 是邊界條件,還有一些邊界條件 a=NaN,b=NaN 等。要驗證 除法 正確性,恐怕得給出無限的條件,實際上,只要驗證幾個邊界條件和一般條件,基本認為代碼是正確了。

有限條件

再舉個例子: stateA='a0'、'a1', stateB='b0'、'b1'、'b2' ,根據 stateA 、 stateB 不同組合輸出不同結果,例如 a0b0 輸出 a0b0 , a0b1 輸出 a0b1 ,所以,共2*3=6種情況。這時,并不存在邊界條件,所以條件都是特定條件,并且條件有限。

這種情況在項目中很常見,以筆者經驗,建議單元測試時把所有情況都驗證一遍,確保沒有遺漏。

單元測試不是集成測試

集成測試

集成測試,也叫組裝測試、聯合測試。在單元測試的基礎上,將相關模塊組合成為子系統或系統進行測試,稱為 集成測試 。通俗一點,集成測試就是把多個(最少2個)組件合在一起,測試某個功能片段,甚至是單獨功能。

單元測試僅針對單元

在微信群很多同學問: “用Robolectric能不能請求網絡”,"Junit能直接請求服務器嗎"?

例如,我們使用MVP模式,如果我們想測試:調用PresenterA接口,請求真實網絡,并且返回數據后,解析成對象,并且根據返回數據執行對應邏輯。這明顯 就是一個集成測試,而不是單元測試 。PresenterA是一個單元,M層的Repository、DAO等是一個單元,更底層的sqlite第三方庫、網絡請求第三方庫(okhttp等) 也是單元.....組合了n個單元的測試,是 集成測試

Robolectric、Junit能否請求網絡?

包括筆者在內,很多同學一開始都會有這個疑問。

閱讀了本文第一部分,應該了解到 robolectric、junit是運行在jvm ,只要有一點點java開發經驗的同學,都知道 jvm本身能連接網絡 。如果你調用的方法所依賴的一切代碼,都不依賴Android庫(例如 okhttp、retrofit ),那99%都能在jvm上跑,并且能請求服務器。如果不幸有Android依賴,很大概率還是能在robolectric上跑的。

為什么robolectric不是100%能跑通測試呢? Robolectric僅支持API21及以下,并且不支持jni庫 。因此,如果你的代碼依賴了API21以上接口或者jni接口,robolectric也無能為力。天啊!怎么辦?

請讀者先不要沮喪,我們自有對策,不過要看讀者慧根了^_^!。前文 “依賴隔離” 提到,我們可以 通過一定手段,把jni、android依賴隔離掉 。咦?咱們的代碼是不是有救了?之后的文章,筆者會詳細給大家講解一下。

單元測試才是必要的

經過筆者指點,可能有讀者蠢蠢欲動去嘗試集成測試了.....且慢!說好的單元測試呢?集成測試看起來簡單,實際上由于依賴過多,很多時候很麻煩,而且運行慢;相比之下,單元測試則小巧、靈活得多,運行快,快速發現bug。在這方面,有一個理論 Test Pyramid

示意圖中,左箭頭表示速度,右箭頭表示開發成。可以看到,單元測試速度比集成測試(Service,也叫Integration)、UI測試要快,并且開發成本也是最低。Test Pyramid告訴我們,應該花大部分精力去寫單元測試,其次才是集成測試、UI測試。

筆者建議,還是先老老實實做單元測試,有時間精力再做集成測試。

小結

本文介紹了幾個單元測試框架,介紹了junit、mockito初步使用,闡述了依賴隔離、mocking的概念,解答了"robolectric、junit能否請求網絡"問題。結合閱讀 《談談為什么寫單元測試》 ,想必讀者對單元測試有了一個初步的了解。

如果讀者問筆者:“我的是小項目,是否有必要做單元測試?” 我很肯定地回答,任何項目都有必要做單元測試。至于單元測試是否耗費很多時間,或者效果不顯著,這要看使用者的編程經驗了,不能一概而論。

最后,叮囑讀者多敲代碼,真槍實彈地實踐單元測試。可以從公司項目小規模使用,形成自己單元測試風格后,就可以跟大范圍地推廣了。歡迎在本文留言討論!

 

來自:https://segmentfault.com/a/1190000006811141

 

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