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

幾點說明:

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

  2. 很多時候,為了避免中文歧義,我會用英文表述

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

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

}</code></pre>

對于這個login方法的單元測試,應該是調用Activity里面的這個login方法,然后驗證 mUserManager 的 performLogin 方法得到了驗證。但是如果使用Activity,我們就需要用到 Robolectric 框架,然而我們到目前為止還沒有講到Robolectric的使用。所以在這篇文章中,我們假設這段代碼是放在一個Presenter(LoginPresenter)里面的,這個是 MVP模式 里面的概念,這個 LoginPresenter 是一個純java類,而用戶名和密碼是外面傳進來的:

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

}</code></pre>

根據 前面一篇關于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”
    ...
}

}</code></pre>

現在,關鍵的問題來了,怎么驗證 LoginPresenter 里面的 mUserManager 的 performLogin() 方法得到了調用,以及它的參數是正確性呢?如果大家看了 該系列的第一篇文章 就知道,這里需要用到 mock ,那么接下來,我們就介紹mock這個東西。

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”
    ...
}

}</code></pre>

然而我們要驗證的是 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;
}

}</code></pre>

好了,現在我們可以驗證 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”
    ...
}

}</code></pre>

終于到了解釋如何驗證一個對象的某個方法的調用情況了。使用Mockito,驗證一個對象的方法調用情況的姿勢是:

Mockito.verify(objectToVerify).methodToVerify(arguments);

其中, objectToVerify 和 methodToVerify 分別是你想要驗證的對象和方法。對應上面的例子,那就是:

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");  //<==
}

}</code></pre>

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

具體出錯的是最后這一行代碼: 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");  //<==
}

}</code></pre>

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

錯誤信息的大意是,我們想驗證 mockUserManager 的 performLogin() 方法得到了調用,然而其實并沒有。

這就是我想解釋的,關于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;
}

}</code></pre>

同時,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");

}</code></pre>

最后運行一次,hu。。。終于通過了!

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

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

好了,現在我想大家對mock的概念應該有了正確的認識,對怎么樣使用mock也有了認識,接下來我們就可以全心全意介紹Mockito的功能和使用了。

Mockito的使用

1. 驗證方法調用

前面我們講了驗證一個對象的某個method得到調用的方法:

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

這句話的作用是,驗證 mockUserManager 的 performLogin() 得到了調用,同時參數是“xiaochuang”和"xiaochuang password"。其實更準確的說法是,這行代碼驗證的是, mockUserManager 的 performLogin() 方法得到了 一次 調用。因為這行代碼其實是:

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對象的某個方法返回特定的值。

現在假設我們上面的 LoginPresenter 的 login 方法是如下實現的:

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

mUserManager.performLogin(null, password);

}</code></pre>

這里,我們有個 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);</code></pre>

同樣的,你可以用 any 系列方法來指定"無論傳入任何參數值,都返回xxx":

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

指定方法返回特定值就介紹到這,更詳細更高級的用法大家可以自己google。接下來介紹,怎么樣指定一個方法執行特定的動作,這個功能一般是用在目標的方法是void類型的時候。

現在假設我們的 LoginPresenter 的 login() 方法是這樣的:

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

}</code></pre>

在這里,我們想進一步測試傳給 mUserManager.performLogin 的 NetworkCallback 里面的代碼,驗證view得到了更新等等。在測試環境下,我們并不想依賴 mUserManager.performLogin 的真實邏輯,而是讓 mUserManager 直接調用傳入的 NetworkCallback 的 onSuccess 或 onFailure 方法。這種指定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));</code></pre>

這里,當調用 mockUserManager 的 performLogin 方法時,會執行answer里面的代碼,我們上面的例子是直接調用傳入的 callback 的 onFailure 方法,同時傳給 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

}</code></pre>

總之,spy與mock的唯一區別就是默認行為不一樣:spy對象的方法默認調用真實的邏輯,mock對象的方法默認什么都不做,或直接返回默認值。

小結

這篇文章介紹了mock的概念以及Mockito的使用,可能Mockito的很多的一些其他方法沒有介紹,但這只是閱讀文檔的問題而已,更重要的是理解mock的概念。

如果你想了解Mockito更詳細的用法可以參考 這篇文章 ,寫的是相當的好。

下一篇文章我們將介紹依賴注入的概念,以及(或許)使用dagger2來更方便的做依賴注入,以及在單元測試里面的應用,這里依然后很多的誤區,需要大家注意的,想知道具體是什么嗎?那就Stay tuned!

文中代碼在: Github這個項目

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

Android 單元測試: 首先,從是什么開始

Android單元測試(二):再來談談為什么

Android單元測試(三):JUnit單元測試框架的使用

來源:小創

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