Android單元測試(四):Mock以及Mockito的使用

幾點說明:html

  1. 代碼中的 //<== 表示跟上面的相比,這是新增的,或者是修改的代碼,不知道怎麼樣在代碼塊裏面再強調幾行代碼T_T。。。java

  2. 不少時候,爲了不中文歧義,我會用英文表述android

在第一篇文章裏面咱們提到,返回類型爲void方法的單元測試方式,每每是驗證裏面的某個對象的某個方法是否獲得了調用。在那篇文章裏面,我舉的例子是activity裏面的一個login方法:git

public void login() {
    String username = ...//get username from username EditText
    String password = ...//get password from password EditText
    //do other operation like validation, etc
    ...

    mUserManager.performLogin(username, password);
}

對於這個login方法的單元測試,應該是調用Activity裏面的這個login方法,而後驗證mUserManagerperformLogin方法獲得了驗證。可是若是使用Activity,咱們就須要用到Robolectric框架,然而咱們到目前爲止尚未講到Robolectric的使用。因此在這篇文章中,咱們假設這段代碼是放在一個Presenter(LoginPresenter)裏面的,這個是MVP模式裏面的概念,這個LoginPresenter是一個純java類,而用戶名和密碼是外面傳進來的:github

public class LoginPresenter {
    private UserManager mUserManager = new UserManager();

    public void login(String username, String password) {
        if (username == null || username.length() == 0) return;
        if (password == null || password.length() < 6) return;

        mUserManager.performLogin(username, password);
    }

}

根據前面一篇關於JUnit的文章的講解,咱們很容易的寫出針對login()方法的單元測試:框架

public class LoginPresenterTest {

    @Test
    public void testLogin() throws Exception {
        LoginPresenter loginPresenter = new LoginPresenter();
        loginPresenter.login("xiaochuang", "xiaochuang password");

        //驗證LoginPresenter裏面的mUserManager的performLogin()方法獲得了調用,同時參數分別是「xiaochuang」、「xiaochuang‘s password」
        ...
    }
}

如今,關鍵的問題來了,怎麼驗證LoginPresenter裏面的mUserManagerperformLogin()方法獲得了調用,以及它的參數是正確性呢?若是你們看了該系列的第一篇文章就知道,這裏須要用到mock,那麼接下來,咱們就介紹mock這個東西。ide

Mock的概念:兩種誤解

Mock的概念,其實很簡單,咱們前面也介紹過:所謂的mock就是建立一個類的虛假的對象,在測試環境中,用來替換掉真實的對象,以達到兩大目的:函數

  1. 驗證這個對象的某些方法的調用狀況,調用了多少次,參數是什麼等等單元測試

  2. 指定這個對象的某些方法的行爲,返回特定的值,或者是執行特定的動做測試

要使用Mock,通常須要用到mock框架,這篇文章咱們使用Mockito這個框架,這個是Java界使用最普遍的一個mock框架。

對於上面的例子,咱們要驗證mUserManager的一些行爲,首先要mock UserManager這個類,mock這個類的方式是:
Mockito.mock(UserManager.class);
mock了UserManager類以後,咱們就能夠開始測試了:

public class LoginPresenterTest {

    @Test
    public void testLogin() {
        Mockito.mock(UserManager.class);    //<==
        LoginPresenter loginPresenter = new LoginPresenter();
        loginPresenter.login("xiaochuang", "xiaochuang password");
        
        //驗證LoginPresenter裏面的mUserManager的performLogin()方法獲得了調用,參數分別是「xiaochuang」、「xiaochuang‘s password」
        ...
    }
}

然而咱們要驗證的是LoginPresenter裏面的mUserManager這個對象,可是如今咱們沒有辦法得到這個對象,由於mUserManager是private的,怎麼辦?先不想太多,咱們簡單除暴點,給LoginPresenter加一個getter,稍後你會明白我如今爲何作這樣的決定。

public class LoginPresenter {
    private UserManager mUserManager = new UserManager();

    public void login(String username, String password) {
        if (username == null || username.length() == 0) return;
        if (password == null || password.length() < 6) return;

        mUserManager.performLogin(username, password);
    }
    
    public UserManager getUserManager() {   //<==
        return mUserManager;
    }
}

好了,如今咱們能夠驗證mUserManager被調用的狀況了:

public class LoginPresenterTest {

    @Test
    public void testLogin() throws Exception {
        Mockito.mock(UserManager.class);
        LoginPresenter loginPresenter = new LoginPresenter();
        loginPresenter.login("xiaochuang", "xiaochuang password");

        UserManager userManager = loginPresenter.getUserManager();  //<==
        //驗證userManager的performLogin()方法獲得了調用,參數分別是「xiaochuang」、「xiaochuang password」
        ...
    }
}

終於到了解釋如何驗證一個對象的某個方法的調用狀況了。使用Mockito,驗證一個對象的方法調用狀況的姿式是:
Mockito.verify(objectToVerify).methodToVerify(arguments);
其中,objectToVerifymethodToVerify分別是你想要驗證的對象和方法。對應上面的例子,那就是:
Mockito.verify(userManager).performLogin("xiaochuang", "xiaochuang password");
好,如今咱們把這行代碼放到測試裏面:

public class LoginPresenterTest {

    @Test
    public void testLogin() throws Exception {
        Mockito.mock(UserManager.class);
        LoginPresenter loginPresenter = new LoginPresenter();
        loginPresenter.login("xiaochuang", "xiaochuang password");

        UserManager userManager = loginPresenter.getUserManager();
        Mockito.verify(userManager).performLogin("xiaochuang", "xiaochuang password");  //<==
    }
}

接着咱們跑一下這個測試方法,結果發現,額。。。出錯了:

具體出錯的是最後這一行代碼:Mockito.verify(userManager).performLogin("xiaochuang", "xiaochuang password");。這個錯誤的大概意思是,傳給Mockito.verify()的參數必須是一個mock對象,而咱們傳進去的不是一個mock對象,因此出錯了。

這就是我想解釋的,關於mock的第一個誤解:Mockito.mock()並非mock一整個類,而是根據傳進去的一個類,mock出屬於這個類的一個對象,而且返回這個mock對象;而傳進去的這個類自己並無改變,用這個類new出來的對象也沒有受到任何改變!

結合上面的例子,Mockito.mock(UserManager.class);只是返回了一個屬於UserManager這個類的一個mock對象。UserManager這個類自己沒有受到任何影響,而LoginPresenter裏面直接new UserManager()獲得的mUserManager也是正常的一個對象,不是一個mock對象。Mockito.verify()的參數必須是mock對象,也就是說,Mockito只能驗證mock對象的方法調用狀況。所以,上面那種寫法就出錯了。

好的,知道了,既然這樣,看來咱們須要使用Mockito.mock(UserManager.class);返回的對象來驗證,代碼以下:

public class LoginPresenterTest {

    @Test
    public void testLogin() throws Exception {
        UserManager mockUserManager = Mockito.mock(UserManager.class);  //<==
        LoginPresenter loginPresenter = new LoginPresenter();
        
        loginPresenter.login("xiaochuang", "xiaochuang password");

        Mockito.verify(mockUserManager).performLogin("xiaochuang", "xiaochuang password");  //<==
    }
}

在運行一下,發現,額。。。又出錯了:

錯誤信息的大意是,咱們想驗證mockUserManagerperformLogin()方法獲得了調用,然而其實並無。

這就是我想解釋的,關於mock的第二個誤解:mock出來的對象並不會自動替換掉正式代碼裏面的對象,你必需要有某種方式把mock對象應用到正式代碼裏面

結合上面的例子,UserManager mockUserManager = Mockito.mock(UserManager.class);的確給咱們建立了一個mock對象,保存在mockUserManager裏面。然而,當咱們調用loginPresenter.login("xiaochuang", "xiaochuang password");的時候,用到的mUserManager依然是使用new UserManager()建立的正常的對象。而mockUserManager並無獲得任何的調用,所以,當咱們驗證它的performLogin()方法獲得了調用時,就失敗了。

對於這個問題,很明顯,咱們必須在調用loginPresenter.login()以前,把mUserManager引用換成mockUserManager所引用的mock對象。最簡單的辦法,就是加一個setter:

public class LoginPresenter {

    private UserManager mUserManager = new UserManager();

    public void login(String username, String password) {
        if (username == null || username.length() == 0) return;
        if (password == null || password.length() < 6) return;

        mUserManager.performLogin(username, password);
    }

    public void setUserManager(UserManager userManager) {  //<==
        this.mUserManager = userManager;
    }

}

同時,getter咱們用不到了,因而這裏就直接刪了。那麼按照上面的思路,寫出來的測試代碼以下:

@Test
public void testLogin() throws Exception {
    UserManager mockUserManager = Mockito.mock(UserManager.class);
    LoginPresenter loginPresenter = new LoginPresenter();
    loginPresenter.setUserManager(mockUserManager);  //<==

    loginPresenter.login("xiaochuang", "xiaochuang password");

    Mockito.verify(mockUserManager).performLogin("xiaochuang", "xiaochuang password");
}

最後運行一次,hu。。。終於經過了!

固然,若是你的正式代碼裏面沒有任何地方用到了那個setter的話,那麼專門爲了測試而增長了一個方法,畢竟不是很優雅的解決辦法,更好的解決辦法是使用依賴注入,簡單解釋就是把UserManager做爲LoginPresenter的構造函數的參數,傳進去。具體操做請期待下一篇文章^_^,這裏咱們專門講mock的概念和Mockito的使用。

然而仍是忍不住想多嘴一句:
優雅歸優雅,有麼有必要,值不值得,卻又是另一回事。整體來講,我認爲是值得的,由於這可讓這個類變得可測,也就意味着咱們能夠驗證這個類的正確性,更給之後重構這個類有了保障,防止誤改錯這個類等等。所以,不少時候,若是你爲了作單元測試,不得已要給一些類加一些額外的代碼。那就加吧!畢竟優雅不能當飯吃,而解決問題、修復bug能夠,作出優秀的、少有bug的產品更能夠,因此,Just Do It!

好了,如今我想你們對mock的概念應該有了正確的認識,對怎麼樣使用mock也有了認識,接下來咱們就能夠全心全意介紹Mockito的功能和使用了。

Mockito的使用

1. 驗證方法調用

前面咱們講了驗證一個對象的某個method獲得調用的方法:
Mockito.verify(mockUserManager).performLogin("xiaochuang", "xiaochuang password");
這句話的做用是,驗證mockUserManagerperformLogin()獲得了調用,同時參數是「xiaochuang」和"xiaochuang password"。其實更準確的說法是,這行代碼驗證的是,mockUserManagerperformLogin()方法獲得了一次調用。由於這行代碼實際上是:
Mockito.verify(mockUserManager, Mockito.times(1)).performLogin("xiaochuang", "xiaochuang password");
的簡寫,或者說重載方法,注意其中的Mockito.times(1)
所以,若是你想驗證一個對象的某個方法獲得了屢次調用,只須要將次數傳給Mockito.times()就行了。
Mockito.verify(mockUserManager, Mockito.times(3)).performLogin(...); //驗證mockUserManager的performLogin獲得了三次調用。

對於調用次數的驗證,除了能夠驗證固定的多少次,還能夠驗證最多,最少歷來沒有等等,方法分別是:atMost(count), atLeast(count), never()等等,都是Mockito的靜態方法,其實大部分時候咱們會static import Mockito這個類的全部靜態方法,這樣就不用每次加上Mockito.前綴了。本文下面我也按照這個規則。(其實我早就想說這句話啦,只是一直沒找到好的時機[喜極而泣])

不少時候你並不關心被調用方法的參數具體是什麼,或者是你也不知道,你只關心這個方法獲得調用了就行。這種狀況下,Mockito提供了一系列的any方法,來表示任何的參數都行:
Mockito.verify(mockUserManager).performLogin(Mockito.anyString(), Mockito.anyString());
anyString()表示任何一個字符串均可以。null?也能夠的!
相似anyString,還有anyInt, anyLong, anyDouble等等。anyObject表示任何對象,any(clazz)表示任何屬於clazz的對象。在寫這篇文章的時候,我剛剛發現,還有很是有意思也很是人性化的anyCollection,anyCollectionOf(clazz), anyList(Map, set), anyListOf(clazz)等等。看來我以前寫了很多冤枉代碼啊T_T。。。

2. 指定mock對象的某些方法的行爲

到目前爲止,咱們介紹了mock的一大做用:驗證方法調用。咱們說mock主要有兩大做用,第二個大做用是:指定某個方法的返回值,或者是執行特定的動做。

那麼接下來,咱們就來介紹mock的第二大做用,先介紹其中的第一點:指定mock對象的某個方法返回特定的值。
如今假設咱們上面的LoginPresenterlogin方法是以下實現的:

public void login(String username, String password) {
    if (username == null || username.length() == 0) return;
    //假設咱們對密碼強度有必定要求,使用一個專門的validator來驗證密碼的有效性
    if (mPasswordValidator.verifyPassword(password)) return;  //<==

    mUserManager.performLogin(null, password);
}

這裏,咱們有個PasswordValidator來驗證密碼的有效性,可是這個類的verifyPassword()方法運行須要好久,好比說須要聯網。這個時候在測試的環境下咱們想簡單處理,指定讓它直接返回true或false。你可能會想,這樣作能夠嗎?真的好嗎?回答是確定的,由於這裏咱們要測的是login()這個方法,這其實跟PasswordValidator內部的邏輯沒有太大關係,這纔是單元測試真正該有的粒度。
話說回來,這種指定mock對象的某個方法,讓它返回特定值的寫法以下:
Mockito.when(mockObject.targetMethod(args)).thenReturn(desiredReturnValue);
應該很好理解,結合上面PasswordValidator的例子:

//先建立一個mock對象
PasswordValidator mockValidator = Mockito.mock(PasswordValidator.class);

//當調用mockValidator的verifyPassword方法,同時傳入"xiaochuang_is_handsome"時,返回true
Mockito.when(mockValidator.verifyPassword("xiaochuang_is_handsome")).thenReturn(true);
    
//當調用mockValidator的verifyPassword方法,同時傳入"xiaochuang_is_not_handsome"時,返回false
Mockito.when(validator.verifyPassword("xiaochuang_is_not_handsome")).thenReturn(false);

一樣的,你能夠用any系列方法來指定"不管傳入任何參數值,都返回xxx":

//當調用mockValidator的verifyPassword方法時,返回true,不管參數是什麼
Mockito.when(validator.verifyPassword(anyString())).thenReturn(true);

指定方法返回特定值就介紹到這,更詳細更高級的用法你們能夠本身google。接下來介紹,怎麼樣指定一個方法執行特定的動做,這個功能通常是用在目標的方法是void類型的時候。
如今假設咱們的LoginPresenterlogin()方法是這樣的:

public void loginCallbackVersion(String username, String password) {
    if (username == null || username.length() == 0) return;
    //假設咱們對密碼強度有必定要求,使用一個專門的validator來驗證密碼的有效性
    if (mPasswordValidator.verifyPassword(password)) return;

    //login的結果將經過callback傳遞回來。
    mUserManager.performLogin(username, password, new NetworkCallback() {  //<==
        @Override
        public void onSuccess(Object data) {
            //update view with data
        }

        @Override
        public void onFailure(int code, String msg) {
            //show error msg
        }
    });
}

在這裏,咱們想進一步測試傳給mUserManager.performLoginNetworkCallback裏面的代碼,驗證view獲得了更新等等。在測試環境下,咱們並不想依賴mUserManager.performLogin的真實邏輯,而是讓mUserManager直接調用傳入的NetworkCallbackonSuccessonFailure方法。這種指定mock對象執行特定的動做的寫法以下:
Mockito.doAnswer(desiredAnswer).when(mockObject).targetMethod(args);
傳給doAnswer()的是一個Answer對象,咱們想要執行什麼樣的動做,就在這裏面實現。結合上面的例子解釋:

Mockito.doAnswer(new Answer() {
    @Override
    public Object answer(InvocationOnMock invocation) throws Throwable {
        //這裏能夠得到傳給performLogin的參數
        Object[] arguments = invocation.getArguments();

        //callback是第三個參數
        NetworkCallback callback = (NetworkCallback) arguments[2];
        
        callback.onFailure(500, "Server error");
        return 500;
    }
}).when(mockUserManager).performLogin(anyString(), anyString(), any(NetworkCallback.class));

這裏,當調用mockUserManagerperformLogin方法時,會執行answer裏面的代碼,咱們上面的例子是直接調用傳入的callbackonFailure方法,同時傳給onFailure方法500和"Server error"。

固然,使用Mockito.doAnswer()須要建立一個Answer對象,這有點麻煩,代碼看起來也繁瑣,若是想簡單的指定目標方法「什麼都不作」,那麼可使用Mockito.doNothing()。若是想指定目標方法「拋出一個異常」,那麼可使用Mockito.doThrow(desiredException)。若是你想讓目標方法調用真實的邏輯,可使用Mockito.doCallRealMethod()。(什麼??? 默認不是會這樣嗎??? No! )

Spy

最後介紹一個Spy的東西。前面咱們講了mock對象的兩大功能,對於第二大功能: 指定方法的特定行爲,不知道你會不會好奇,若是我不指定的話,它會怎麼樣呢?那麼如今補充一下,若是不指定的話,一個mock對象的全部非void方法都將返回默認值:int、long類型方法將返回0,boolean方法將返回false,對象方法將返回null等等;而void方法將什麼都不作。
然而不少時候,你但願達到這樣的效果:除非指定,否者調用這個對象的默認實現,同時又能擁有驗證方法調用的功能。這正好是spy對象所能實現的效果。建立一個spy對象,以及spy對象的用法介紹以下:

//假設目標類的實現是這樣的
public class PasswordValidator {
    public boolean verifyPassword(String password) {
        return "xiaochuang_is_handsome".equals(password);
    }
}

@Test
public void testSpy() {
    //跟建立mock相似,只不過調用的是spy方法,而不是mock方法。spy的用法
    PasswordValidator spyValidator = Mockito.spy(PasswordValidator.class);

    //在默認狀況下,spy對象會調用這個類的真實邏輯,並返回相應的返回值,這能夠對照上面的真實邏輯
    spyValidator.verifyPassword("xiaochuang_is_handsome"); //true
    spyValidator.verifyPassword("xiaochuang_is_not_handsome"); //false
    
    //spy對象的方法也能夠指定特定的行爲
    Mockito.when(spyValidator.verifyPassword(anyString())).thenReturn(true);
    
    //一樣的,能夠驗證spy對象的方法調用狀況
    spyValidator.verifyPassword("xiaochuang_is_handsome");
    Mockito.verify(spyValidator).verifyPassword("xiaochuang_is_handsome"); //pass
}

總之,spy與mock的惟一區別就是默認行爲不同:spy對象的方法默認調用真實的邏輯,mock對象的方法默認什麼都不作,或直接返回默認值。

小結

這篇文章介紹了mock的概念以及Mockito的使用,可能Mockito的不少的一些其餘方法沒有介紹,但這只是閱讀文檔的問題而已,更重要的是理解mock的概念。
若是你想了解Mockito更詳細的用法能夠參考這篇文章,寫的是至關的好。

下一篇文章咱們將介紹依賴注入的概念,以及(或許)使用dagger2來更方便的作依賴注入,以及在單元測試裏面的應用,這裏依而後不少的誤區,須要你們注意的,想知道具體是什麼嗎?那就
Stay tuned!

文中代碼在:Github這個項目

最後,若是你也對安卓單元測試感興趣的話,歡迎加入咱們的交流羣:

有任何意見或建議,或者發現文中任何問題,歡迎留言!

做者 小創 更多文章 | Github | 公衆號

相關文章
相關標籤/搜索