Android單元測試之Mockito

在博客Android單元測試之JUnit4中,我們簡單地介紹了:什么是單元測試,為什么要用單元測試,并展示了一個簡單的單元測試例子。在文章中,我們只是展示了對有返回類型的目標public方法進行了單元測試,但是對于返回類型為void的public方法,又是如何進行單元測試呢?往往是驗證目標方法中的某個對象的某個方法是否得到了調用,或者驗證目標方法中的某個對象的某個狀態是否發生改變,以此來驗證目標方法是否按照我們想要的邏輯進行調用。

此外在寫單元測試的過程中,一個很普遍的問題是,要測試的目標類會有很多依賴,這些依賴的類/對象/資源又會有別的依賴,從而形成一個大的依賴樹,要在單元測試的環境中完整地構建這樣的依賴,是一件很困難的事情。

所幸,我們有一個應對這個問題的辦法:Mock。簡單地說就是對測試的類所依賴的其他類和對象,進行mock - 構建它們的一個假的對象,定義這些假對象上的行為,然后提供給被測試對象使用。被測試對象像使用真的對象一樣使用它們。用這種方式,我們可以把測試的目標限定于被測試對象本身,就如同在被測試對象周圍做了一個劃斷,形成了一個盡量小的被測試目標。

接下來主角登場了,那就是Mockito測試框架。

Mockito是什么

Mockito是一套非常強大的測試框架,被廣泛的應用于Java程序的unit test中。相比于EasyMock框架,Mockito使用起來簡單,學習成本很低,而且具有非常簡潔的API,測試代碼的可讀性很高。

在測試環境中,通過Mockito來mock出其他的依賴對象,用來替換真實的對象,使得待測的目標方法被隔離起來,避免一些外界因素的影響和依賴,能在我們預設的環境中執行,以達到兩個目的:

  1. 驗證這個對象的某些方法的調用情況,調用了多少次,參數是什么等等;
  2. 指定這個對象的某些方法的行為,返回特定的值,或是執行特定的動作;

Mockito初級使用

首先在Gradle配置如下:

repositories { 
    jcenter() 
}
dependencies { 
    testCompile 'junit:junit:4.12'
    testCompile "org.mockito:mockito-core:1.10.19"
}

示例如下:

import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;

import java.util.List;

public class ListTest {

    @Test
    public void testGet() throws Exception {
        // 創建mock對象
        List mockedList = Mockito.mock(List.class);

        // 設置mock對象的行為 - 當調用其get方法獲取第0個元素時,返回"one"
        Mockito.when(mockedList.get(0)).thenReturn("one");

        // 使用mock對象 - 會返回前面設置好的值"one",即便列表實際上是空的
        String str = (String) mockedList.get(0);

        Assert.assertTrue("one".equals(str));
        Assert.assertTrue(mockedList.size() == 0);

        // 驗證mock對象的get方法被調用過,而且調用時傳的參數是0
        Mockito.verify(mockedList).get(0);
    }

}

代碼中的注釋描述了代碼的邏輯:先創建mock對象,然后設置mock對象上的方法get,指定當get方法被調用,并且參數為0的時候,返回”one”;然后,調用被測試方法(被測試方法會調用mock對象的get方法);最后進行驗證。

通過上面的例子,我們可以初步了解到,在Mockito框架中,

  1. 通過Mockito.mock()方法來mock出對象來,這個對象可以是目標類的外界依賴對象,如List mockedList = Mockito.mock(List.class);
  2. 通過Mockito.when().thenReturn()方法為某個mock對象的方法指定返回值,以便執行特定的動作,如Mockito.when(mockedList.get(0)).thenReturn("one");
  3. 通過Mockito.verify().doSomeThing(matchParam)方法來驗證方法的調用情況(比如說調用次數,調用參數等),如Mockito.verify(mockedList).get(0);就是驗證mockList對象是否調用了get(0)方法

對Mockito存在的誤解

  1. Mockito.mock()并不是mock一整個類,而是根據傳進去的一個類,mock出屬于這個類的一個對象,并且返回這個mock對象;而傳進去的這個類本身并沒有改變,用這個類new出來的對象也沒有受到任何改變;
  2. Mockito.verify()的參數必須是mock對象,否則會拋出異常“org.mockito.exceptions.misusing.NotAMockException:Argument passed to verify() is of type UserManager and is not a mock!”。也就是說,Mockito只能驗證mock對象的調用情況;
  3. Mockito.mock()出來的對象并不會自動替換掉正式代碼里面的對象,你必須要有某種方式(依賴注入方式:構造方法注入,set方式注入,或者是參數形式注入)把mock對象應用到正式代碼里面;
  4. Mockito.spy()方法默認會調用這個類的real implementation,并返回相應的返回值,也可以通過Mockito.when().thenReturn()來指定spy對象的方法的行為;

對了,對spy對象的方法定制需要使用另一種方式:

    @Test
    public void testSpy() {
        List list = new LinkedList();
        List spy = Mockito.spy(list);

        //Impossible: real method is called so spy.get(0) throwsIndexOutOfBoundsException (the list is yet empty)
        when(spy.get(0)).thenReturn("foo");

        //You have to use doReturn() for stubbing
        doReturn("foo").when(spy).get(0);
    }

實驗發現,when(spy.get(0)).thenReturn("foo");這行測試代碼是測試不通過的,會拋出數組越界的異常來,

java.lang.IndexOutOfBoundsException: Index: 0, Size: 0

    at java.util.LinkedList.checkElementIndex(LinkedList.java:555)
    at java.util.LinkedList.get(LinkedList.java:476)
    at com.chriszou.auttutorial.test.what.ListTest.testSpy(ListTest.java:39)

Process finished with exit code 255

因為用when(spy.get(0))會導致類LinkedList的spy對象的get()方法被真正執行,這一點需要時刻注意,所以就需要另一種寫法。但是通過Mockito.mock()方法mock出來的對象,如果不指定的話,一個mock對象的所有非void方法都將返回默認值:int、long類型方法將返回0,boolean方法將返回false,對象方法將返回null等等;而void方法將什么都不做。如:

// 創建mock對象
List mockedList = Mockito.mock(List.class);
System.out.println(mockedList.get(100));

我們未指定mockedList.get(100)的返回值,這里返回的就是null。

Mockito進階

上篇博客的JUnit4單元測試例子中,我們講到可以通過@Before、@Test、@After等注解來表示測試方法。在Mockito中,同樣支持對變量進行注解,例如將mock對象設為測試類的屬性,然后通過注解的方式@Mock來定義它,這樣有利于減少重復代碼,增強可讀性,易于排查錯誤等。除了支持@Mock,Mockito支持的注解還有@Spy(監視真實的對象),@Captor(參數捕獲器),@InjectMocks(mock對象自動注入)。

Annotation的初始化

只有Annotation還不夠,要讓它們工作起來還需要進行初始化工作。初始化的方法為:MockitoAnnotations.initMocks(testClass)參數testClass是你所寫的測試類。一般情況下在Junit4的@Before定義的方法中執行初始化工作,如下:

@Before
public void setUp() {
    MockitoAnnotations.initMocks(this);
}

除了上述初始化的方法外,還可以使用Mockito提供的Junit Runner:MockitoJUnitRunner,這樣就省略了上面的步驟。

@RunWith(MockitoJUnit44Runner.class)
public class ComplaintPresenterTest {
    ...
}
@Mock注解

使用@Mock注解來定義mock對象有如下的優點:

  1. 方便mock對象的創建
  2. 減少mock對象創建的重復代碼
  3. 提高測試代碼可讀性
  4. 變量名字作為mock對象的標示,所以易于排錯

下面是一個例子:

public class ComplaintPresenterTest {

    @Mock
    private ComplaintManager complaintManager;

    @Mock
    private ComplaintContract.IView iView;

    private ComplaintPresenter complaintPresenter;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        complaintPresenter = new ComplaintPresenter(complaintManager, iView);
    }

    @Test
    public void getComplaintReasons() throws Exception {
        complaintPresenter.getComplaintReasons();
        Mockito.verify(complaintManager, Mockito.times(1)).getComplaintReasons(any(Callback.class));
    }

}
@Spy注解

使用@Spy生成的類,所有方法都是真實方法,返回值和真實方法一樣的,是使用Mockito.spy()的快捷方式

public class Test {  
    @Spy   
    List list = new LinkedList();
  
    @Before  
    public void init(){  
       MockitoAnnotations.initMocks(this);  
    }  
    ...  
}  
@Captor注解

@Captor是參數捕獲器的注解,通過注解的方式可以更便捷的對ArgumentCaptor進行定義。還可以通過ArgumentCaptor對象的forClass(Class<T> clazz)方法來構建ArgumentCaptor對象,然后便可在驗證時對方法的參數進行捕獲,最后驗證捕獲的參數值。如果方法有多個參數都要捕獲驗證,那就需要創建多個ArgumentCaptor對象處理。

ArgumentCaptor的Api
argument.capture() 捕獲方法參數;
argument.getValue() 獲取方法參數值,如果方法進行了多次調用,它將返回最后一個參數值;
argument.getAllValues() 方法進行多次調用后,返回多個參數值;

下面看一個例子:

public class ComplaintPresenterTest {

    @Mock(name = "complaintManager1")
    private ComplaintManager complaintManager;

    @Mock
    private ComplaintContract.IView iView;

    private ComplaintPresenter complaintPresenter;

    @Captor
    private ArgumentCaptor<Callback<OrderConfig>> captor;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        complaintPresenter = new ComplaintPresenter(complaintManager, iView);
    }

    @Test
    public void getComplaintReasons() throws Exception {
        complaintPresenter.getComplaintReasons();
        Mockito.verify(complaintManager).getComplaintReasons(captor.capture());
        Assert.assertNotNull(captor.getValue());
    }

}

上面例子就是驗證complaintManager是否調用了getComplaintReasons方法,是否傳入Callback<OrderConfig>參數,通過ArgumentCaptor可以對異步方法進行測試。可以參考這篇博客,通過ArgumentCaptor和doAnswer方式來實現對異步方法進行測試。

@InjectMocks注解

通過這個注解,可實現自動注入mock對象。當前版本只支持setter的方式進行注入,Mockito首先嘗試類型注入,如果有多個類型相同的mock對象,那么它會根據名稱進行注入。當注入失敗的時候Mockito不會拋出任何異常,所以你可能需要手動去驗證它的安全性。如下:

public class ComplaintPresenterTest {

    @Mock
    private ComplaintManager complaintManager;

    @Mock
    private ComplaintContract.IView iView;

    @Captor
    private ArgumentCaptor<Callback<OrderConfig>> captor;

    @InjectMocks
    private ComplaintPresenter complaintPresenter;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void getComplaintReasons() throws Exception {
        complaintPresenter.getComplaintReasons();
        Mockito.verify(complaintManager).getComplaintReasons(captor.capture());
        Assert.assertNotNull(captor.getValue());
    }

}
any參數匹配

很多時候你并不關心被調用方法的參數具體是什么,或者是你也不知道,你只關心這個方法得到調用了就行。這種情況下,Mockito提供了一系列的any方法,來表示任何的參數都行,如上面的例子:Mockito.verify(complaintManager, Mockito.times(1)).getComplaintReasons(any(Callback.class));
any(Callback.class)表示任何一個Callback對象都可以。null?也可以的!類似any,還有anyInt, anyLong, anyDouble等。anyObject表示任何對象,any(clazz)表示任何屬于clazz的對象。在寫這篇文章的時候,我剛剛發現,還有非常有意思也非常人性化的anyCollection,anyCollectionOf(clazz), anyList(Map, set), anyListOf(clazz)等。

Mockito高級進階

在上面的例子中,

// 設置mock對象的行為 - 當調用其get方法獲取第0個元素時,返回"one"
Mockito.when(mockedList.get(0)).thenReturn("one");

如果按照一般代碼的思路去理解,是要做這么一件事:調用mockedList.get方法,傳入0作為參數,然后得到其返回值(一個object),然后再把這個返回值傳給when方法,然后針對when方法的返回值,調用thenReturn。好像有點不通?mockedList.get(0)的結果,語義上是mockedList的一個元素,這個元素傳給when是表示什么意思?所以,我們不能按照尋常的思路去理解這段代碼。實際上這段代碼要做的是描述這么一件事情:當mockedList的get方法被調用,并且參數的值是0的時候,返回”one”。很不尋常,對嗎?如果用平常的面向對象的思想來設計API來做同樣的事情,估計結果是這樣的:

Mockito.returnValueWhen("one", mockedList, "get", 0);

第一個參數描述要返回的結果,第二個參數指定mock對象,第三個參數指定mock方法,后面的參數指定mock方法的參數值。這樣的代碼,更符合我們看一般代碼時候的思路。但是,把上面的代碼跟Mockito的代碼進行比較,我們會發現,我們的代碼有幾個問題:

  1. 不夠直觀
  2. 對重構不友好

第二點尤其重要。想象一下,如果我們要做重構,把get方法改名叫fetch方法,那我們要把”get”字符串替換成”fetch”,而字符串替換沒有編譯器的支持,需要手工去做,或者查找替換,很容易出錯。而Mockito使用的是方法調用,對方法的改名,可以用編譯器支持的重構來進行,更加方便可靠。

Mock對象這件事情,本質上是一個Proxy模式的應用。Proxy模式說的是,在一個真實對象前面,提供一個proxy對象,所有對真實對象的調用,都先經過proxy對象,然后由proxy對象根據情況,決定相應的處理,它可以直接做一個自己的處理,也可以再調用真實對象對應的方法。Proxy對象對調用者來說,可以是透明的,也可以是不透明的。在閱讀源碼之前,可以先了解一下CGLIB,CGLIB是一個強大的高性能的代碼生成包,被許多AOP的框架所使用,Mockito也使用了這個庫。眾所周知,JDK的動態代理用起來非常簡單,當它有一個限制,就是使用動態代理的對象必須實現一個或多個接口。那么如果想代理沒有實現接口的類,怎么辦呢?對的,可以通過CGLIB來實現,它就是這么強大,可以代理沒有實現接口的繼承的類。

Mockito局限性

正是由于Mockito生成mock對象的原理是基于CGLIB,而CGLIB生成代理對象有其局限性,如final類型、private類型以及靜態類型的方法不能mock。但是在我們項目中,如果要對靜態方法或者final方法進行單元測試,那該怎么辦呢?請關注博客Android單元測試之PowerMockito

小結

這篇博客主要介紹了mock的概念以及Mockito的使用,還有介紹了Mockito相關的注解使用,并簡單介紹了Mockito的實現原理。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 背景 在寫單元測試的過程中,一個很普遍的問題是,要測試的目標類會有很多依賴,這些依賴的類/對象/資源又會有別的依賴...
    johnnycmj閱讀 1,184評論 0 3
  • 寫在前面 因個人能力有限,可能會出現理解錯誤的地方,歡迎指正和交流! 關于單元測試 通常一個優秀的開源框架,一般都...
    汪海游龍閱讀 2,929評論 0 21
  • 前面花了很大篇幅來介紹JUnit4,JUnit4是整個單元測試的基礎,其他的測試框架都是跑在JUnit4上的。接下...
    云飛揚1閱讀 5,787評論 2 51
  • 什么是 Mock mock 的中文譯為: 仿制的,模擬的,虛假的。對于測試框架來說,即構造出一個模擬/虛假的對象,...
    Whyn閱讀 4,385評論 0 3
  • 本文介紹了Android單元測試入門所需了解的內容,包括JUnit、Mockito和PowerMock的使用,怎樣...
    于衛國閱讀 4,618評論 0 5