Android單元測試(4):Mock 和Mockito的使用

小創發表於2016-05-09

幾點說明:
1. 程式碼中的 //<== 表示跟上面的相比,這是新增的,或者是修改的程式碼,不知道怎麼樣在程式碼塊裡面再強調幾行程式碼T_T。。。

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

在第一篇文章裡面我們提到,返回型別為void方法的單元測試方式,往往是驗證裡面的某個物件的某個方法是否得到了呼叫。在那篇文章裡面,我舉的例子是activity裡面的一個login方法:

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

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

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

Mock的概念:兩種誤解

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

  1. 驗證這個物件的某些方法的呼叫情況,呼叫了多少次,引數是什麼等等
  2. 指定這個物件的某些方法的行為,返回特定的值,或者是執行特定的動作

要使用Mock,一般需要用到mock框架,這篇文章我們使用Mockito這個框架,這個是Java界使用最廣泛的一個mock框架。

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

然而我們要驗證的是LoginPresenter裡面的mUserManager這個物件,但是現在我們沒有辦法獲得這個物件,因為mUserManager是private的,怎麼辦?先不想太多,我們簡單除暴點,給LoginPresenter加一個getter,稍後你會明白我現在為什麼做這樣的決定。

好了,現在我們可以驗證mUserManager被呼叫的情況了:

終於到了解釋如何驗證一個物件的某個方法的呼叫情況了。使用Mockito,驗證一個物件的方法呼叫情況的姿勢是:
Mockito.verify(objectToVerify).methodToVerify(arguments);
其中,objectToVerifymethodToVerify分別是你想要驗證的物件和方法。對應上面的例子,那就是:
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);返回的物件來驗證,程式碼如下:

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

錯誤資訊的大意是,我們想驗證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:

同時,getter我們用不到了,於是這裡就直接刪了。那麼按照上面的思路,寫出來的測試程式碼如下:

最後執行一次,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方法是如下實現的:

這裡,我們有個PasswordValidator來驗證密碼的有效性,但是這個類的verifyPassword()方法執行需要很久,比如說需要聯網。這個時候在測試的環境下我們想簡單處理,指定讓它直接返回true或false。你可能會想,這樣做可以嗎?真的好嗎?回答是肯定的,因為這裡我們要測的是login()這個方法,這其實跟PasswordValidator內部的邏輯沒有太大關係,這才是單元測試真正該有的粒度。

話說回來,這種指定mock物件的某個方法,讓它返回特定值的寫法如下: Mockito.when(mockObject.targetMethod(args)).thenReturn(desiredReturnValue);
應該很好理解,結合上面PasswordValidator的例子:

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

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

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

在這裡,我們想進一步測試傳給mUserManager.performLoginNetworkCallback裡面的程式碼,驗證view得到了更新等等。在測試環境下,我們並不想依賴mUserManager.performLogin的真實邏輯,而是讓mUserManager直接呼叫傳入的NetworkCallbackonSuccessonFailure方法。這種指定mock物件執行特定的動作的寫法如下:

Mockito.doAnswer(desiredAnswer).when(mockObject).targetMethod(args);

傳給doAnswer()的是一個Answer物件,我們想要執行什麼樣的動作,就在這裡面實現。結合上面的例子解釋:

這裡,當呼叫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物件的用法介紹如下:

總之,spy與mock的唯一區別就是預設行為不一樣:spy物件的方法預設呼叫真實的邏輯,mock物件的方法預設什麼都不做,或直接返回預設值。

小結

這篇文章介紹了mock的概念以及Mockito的使用,可能Mockito的很多的一些其他方法沒有介紹,但這只是閱讀文件的問題而已,更重要的是理解mock的概念。 如果你想了解Mockito更詳細的用法可以參考這篇文章,寫的是相當的好。

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

文中程式碼在Github

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

Android單元測試(4):Mock 和Mockito的使用

相關文章