Android單元測試 - 如何開始?

原文連接:http://www.jianshu.com/p/bc99678b1d6e
做者:鍵盤男kkmike999html

回顧java

《談談爲何寫單元測試》android

基本單元測試框架

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

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

Junit官網
Mockito官網算法

以後學習AndroidJUnitRunner,Google官方的android單元測試框架之一,使用跟Junit是同樣的,只不過須要運行在android真機或模擬器環境。因爲mockito只在jvm環境生效,而android是運行在DalvikART,因此AndroidJUnitRunner不能使用mockitosql

而後能夠嘗試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。在IntellijAndroid Studio對類右鍵->run CalculaterTest,用例中全部被@org.junit.Test註解的方法,就會被執行。

run calculaterTest

pass

測試經過。

若是代碼改爲Assert.assertEquals(result, 4);,測試會失敗。

not pass

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)時返回truenum爲其餘值時返回false」,返回的true、false就是模擬數據

例如,須要測試a=2,b=1a=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隔離了CalculaterIMathUtils實現類的依賴,並經過單元測試,驗證了divide()的邏輯正確性。

條件覆蓋

無限條件

要驗證程序正確性,必然要給出全部可能的條件(極限編程),並驗證其行爲或結果,纔算是100%覆蓋條件。實際項目中,驗證邊界條件和通常條件就OK了。

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

有限條件

再舉個例子:stateA='a0'、'a1', stateB='b0'、'b1'、'b2',根據stateAstateB不一樣組合輸出不一樣結果,例如a0b0輸出a0b0a0b1輸出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

Test Pyramid

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

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


小結

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

若是讀者問筆者:「個人是小項目,是否有必要作單元測試?」 我很確定地回答,任何項目都有必要作單元測試。至於單元測試是否耗費不少時間,或者效果不顯著,這要看使用者的編程經驗了,不能一律而論。

最後,叮囑讀者多敲代碼,真槍實彈地實踐單元測試。能夠從公司項目小規模使用,造成本身單元測試風格後,就能夠跟大範圍地推廣了。歡迎在本文留言討論!

相關文章
相關標籤/搜索