Android單元測試之Mockito

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

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

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

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

Mockito是什么

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

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

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

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 {
        // 創(chuàng)建mock對象
        List mockedList = Mockito.mock(List.class);

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

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

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

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

}

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

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

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

對Mockito存在的誤解

  1. Mockito.mock()并不是mock一整個類,而是根據(jù)傳進(jìn)去的一個類,mock出屬于這個類的一個對象,并且返回這個mock對象;而傳進(jìn)去的這個類本身并沒有改變,用這個類new出來的對象也沒有受到任何改變;
  2. Mockito.verify()的參數(shù)必須是mock對象,否則會拋出異?!皁rg.mockito.exceptions.misusing.NotAMockException:Argument passed to verify() is of type UserManager and is not a mock!”。也就是說,Mockito只能驗證mock對象的調(diào)用情況;
  3. Mockito.mock()出來的對象并不會自動替換掉正式代碼里面的對象,你必須要有某種方式(依賴注入方式:構(gòu)造方法注入,set方式注入,或者是參數(shù)形式注入)把mock對象應(yīng)用到正式代碼里面;
  4. Mockito.spy()方法默認(rèn)會調(diào)用這個類的real implementation,并返回相應(yīng)的返回值,也可以通過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);
    }

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

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))會導(dǎo)致類LinkedList的spy對象的get()方法被真正執(zhí)行,這一點(diǎn)需要時刻注意,所以就需要另一種寫法。但是通過Mockito.mock()方法mock出來的對象,如果不指定的話,一個mock對象的所有非void方法都將返回默認(rèn)值:int、long類型方法將返回0,boolean方法將返回false,對象方法將返回null等等;而void方法將什么都不做。如:

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

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

Mockito進(jìn)階

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

Annotation的初始化

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

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

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

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

使用@Mock注解來定義mock對象有如下的優(yōu)點(diǎn):

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

下面是一個例子:

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生成的類,所有方法都是真實(shí)方法,返回值和真實(shí)方法一樣的,是使用Mockito.spy()的快捷方式

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

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

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

下面看一個例子:

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是否調(diào)用了getComplaintReasons方法,是否傳入Callback<OrderConfig>參數(shù),通過ArgumentCaptor可以對異步方法進(jìn)行測試。可以參考這篇博客,通過ArgumentCaptor和doAnswer方式來實(shí)現(xiàn)對異步方法進(jìn)行測試。

@InjectMocks注解

通過這個注解,可實(shí)現(xiàn)自動注入mock對象。當(dāng)前版本只支持setter的方式進(jìn)行注入,Mockito首先嘗試類型注入,如果有多個類型相同的mock對象,那么它會根據(jù)名稱進(jìn)行注入。當(dāng)注入失敗的時候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參數(shù)匹配

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

Mockito高級進(jìn)階

在上面的例子中,

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

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

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

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

  1. 不夠直觀
  2. 對重構(gòu)不友好

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

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

Mockito局限性

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

小結(jié)

這篇博客主要介紹了mock的概念以及Mockito的使用,還有介紹了Mockito相關(guān)的注解使用,并簡單介紹了Mockito的實(shí)現(xiàn)原理。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容