天天看點

Android單元測試(六):Mockito學習

前面花了很大篇幅來介紹JUnit4,JUnit4是整個單元測試的基礎,其他的測試架構都是跑在JUnit4上的。接下來我們将來學習怎麼樣在Android的單元測試中內建Mockito。

6.1 Mockito介紹

6.1.1 Mockito是什麼?

Mockito是一個用于java單元測試中的mocking架構,mock就是模拟的意思,就是能夠模拟一些類和方法的實作。

其官網位址:

http://site.mockito.org
6.1.2 為什麼需要mock?

在寫單元測試的時候,我們會遇到某個測試類有很多依賴,這些依賴類或對象又有别的依賴,這樣會形成一棵巨大的依賴樹,要在單元測試的環境中完整地建構這樣的依賴,是及其困難的,有時候甚至因為運作環境的關系,幾乎不可能完整地建構出這些依賴。如下圖所示:

Android單元測試(六):Mockito學習

如果我們要針對ClassA來寫單元測試,發現ClassA依賴ClassB和ClassD,ClassB又依賴ClassC和ClassE,ClassE又依賴ClassF。好吧,寫到這裡我自己都頭疼了,我僅僅是想為ClassA寫一個單元測試而已,卻不得不自己構造這麼多依賴對象,太複雜了。

由上圖可以看到,ClassA隻依賴ClassB和ClassD,我們實際上隻需要構造這2個依賴對象,這2個依賴對象分别實作了ClassB和ClassD的所有功能。ClassA并不關心ClassB的依賴對象ClassC和ClassE是怎麼構造,它隻關心ClassB和ClassD的構造,并且也無需關系他們的實作細節,有沒什麼方法能自動幫我們實作呢,那這樣我們編寫單元測試就容易得多了,Mockito架構就是為了解決這個問題而設計的。

Android單元測試(六):Mockito學習

如上圖所示,Mockito架構自動幫我們構造了2個mock對象:MockClassB和MockClassD,這樣ClassA的單元測試就簡單多了。

Mock測試就是在測試過程中,對于一些由于運作環境原因不能構造的對象、或者構造比較複雜的對象、或者我們并不需要關注的對象,用一個虛拟的對象(Mock對象)來替代進而友善測試的測試方法。

6.1.3 在Android中使用Mockito

在build.gradle中加入mockito依賴配置:

testCompile 'org.mockito:mockito-core:2.8.9'
           

最新版本可以去官網檢視

6.2 使用Mockito

幾乎所有的測試方法都在org.mockito.Mockito類中:

6.2.1 驗證行為
@Test
    public void testMock() {
        //建立一個mock對象
        List list = mock(List.class);

        //使用mock對象
        list.add("one");
        list.clear();

        //驗證mock對象的行為
        verify(list).add("one");  //驗證有add("one")行為發生
        verify(list).clear();          //驗證有clear()行為發生
    }
           

一旦建立一個mock對象,它會記住所有的互動,這樣我們就可以驗證自己感興趣的行為。

6.2.2 Stubbing

Stub對象用來提供測試時所需要的測試資料,對各種互動設定相應的回應。Mockito使用when(...).thenReturn(...)設定方法調用的傳回值,使用when(...).thenThrow(...)設定方法調用時抛出的異常。

@Test
    public void testMock2() {
        //不僅可以針對接口mock, 還可以針對具體類
        LinkedList list = mock(LinkedList.class);

        //設定傳回值,當調用list.get(0)時會傳回"first"
        when(list.get(0)).thenReturn("first");
        //當調用list.get(1)時會抛出異常
        when(list.get(1)).thenThrow(new RuntimeException());

        //會列印"print"
        System.out.println(list.get(0));
        //會抛出RuntimeException
        System.out.println(list.get(1));
        //會列印 null
        System.out.println(list.get(99));

        verify(list).get(0);
    }
           

對于stubbing,需要注意一下幾點:

  • 對于有傳回值的方法,mock會預設傳回null、空集合、預設值。比如為int/Integer傳回0,為boolean/Boolean傳回false、為Object傳回null。
  • 一旦stubbing,不管方法被調用多少次,都永遠傳回stubbing的值。
  • stubbing可以被覆寫, 如果對同一個方法進行多次stubbing,最後一次的stubbing會生效。
6.2.3 Argument matchers(參數比對器)
@Test
    public void testMock3() {
        List list = mock(List.class);
        //使用anyInt(), anyString(), anyLong()等進行參數比對
        when(list.get(anyInt())).thenReturn("item");

        //将會列印出"item"
        System.out.println(list.get(100));

        verify(list).get(anyInt());
    }
           
6.2.4 驗證方法的調用次數
@Test
    public void testMock4() {
        List list = mock(List.class);
        list.add("once");
        list.add("twice");
        list.add("twice");
        list.add("triple");
        list.add("triple");
        list.add("triple");

        //執行1次
        verify(list, times(1)).add("once");
        //執行2次
        verify(list, times(2)).add("twice");
        verify(list, times(3)).add("triple");

        //從不執行, never()等同于times(0)
        verify(list, never()).add("never happened");

        //驗證至少執行1次
        verify(list, atLeastOnce()).add("twice");
        //驗證至少執行2次
        verify(list, atLeast(2)).add("twice");
        //驗證最多執行4次
        verify(list, atMost(4)).add("triple");
    }
           

times(n):方法被調用n次

never():沒有被調用

atLeast(n):至少被調用n次

atLeastOnce():至少被調用1次,相當于atLeast(1)

atMost():最多被調用n次

6.2.5 驗證方法的調用順序
@Test
    public void testMock5() {
        List list = mock(List.class);
        list.add("first");
        list.add("second");

        InOrder myOrder = inOrder(list);
        myOrder.verify(list).add("first");
        myOrder.verify(list).add("second");
    }
           

可同時驗證多個mock對象的測試方法的執行順序:

InOrder myOrder = inOrder(firstMock, secondMock, ...)

6.2.6 verifyZeroInteractions && verifyNoMoreInteractions
@Test
    public void testMock6() {
        List list = mock(List.class);
        //驗證mock對象沒有産生任何互動,也即沒有任何方法調用
        verifyZeroInteractions(list);

        List list2 = mock(List.class);
        list2.add("one");
        list2.add("two");
        verify(list2).add("one");
        //驗證mock對象是否有被調用過但沒被驗證的方法。這裡會測試不通過,list2.add("two")方法沒有被驗證過
        verifyNoMoreInteractions(list2);
    }
           
6.2.7 使用@Mock建立mock對象
//通過注解會自動建立mock對象
    @Mock
    private List mockList;
    @Mock
    private Map mockMap;
           

要使用@Mock注解有2種配置方式:

  • 在base class中或者初始化的地方配置:
MockitoAnnotations.initMocks(this);
           
  • 使用JUnit4的rule來配置:
@Rule
    public MockitoRule rule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
           

以上2種方式可以達到同樣的效果。

6.2.8 do(...).when(...)
  • doThrow(Throwable...):進行異常測試
@Test
    public void testMock7() {
        List list = mock(List.class);
        list.add("123");
        //當list調用clear()方法時會抛出異常
        doThrow(new RuntimeException()).when(list).clear();
        list.clear();
    }
           
  • doReturn():指定傳回值
@Test
    public void testMock8() {
        List list = mock(List.class);
        doReturn("123").when(list).get(anyInt());
        System.out.println(list.get(0));
    }
           
  • doNothing() :指定void方法什麼都不做
  • doCallRealMethod():指定方法調用内部的真實邏輯
class Foo {  
        public void doFoo() {
            System.out.println("method doFoo called.");
        }

        public int getCount() {
            return 1;
        }
    }

    @Test
    public void testMock9() {
        Foo foo = mock(Foo.class);

        //什麼資訊也不會列印, mock對象并不會調用真實邏輯
        foo.doFoo();

        //啥也不會列印出來
        doNothing().when(foo).doFoo();
        foo.doFoo();

        doCallRealMethod().when(foo).doFoo();
        //這裡會調用真實邏輯, 列印出"method doFoo called."資訊
        foo.doFoo();
        
        //這裡會列印出0
        System.out.println(foo.getCount());
        doCallRealMethod().when(foo).getCount();
        //這裡會列印出"1"
        System.out.println(foo.getCount());
    }
           
6.2.9 使用spy()監視真正的對象

使用spy可以監視對象方法的真實調用。當我們mock某個類時,如果需要某些方法是真實調用,而某些方法是mock調用時,借助spy可以實作這些功能。

@Test
    public void testMock10(){
        List list = new ArrayList();
        List spy = spy(list);

        //subbing方法,size()并不會真實調用,這裡傳回10
        when(spy.size()).thenReturn(10);

        //使用spy對象會調用真實的方法
        spy.add("one");
        spy.add("two");

        //會列印出"one"
        System.out.println(spy.get(0));
        //會列印出"10",與前面的stubbing方法對應
        System.out.println(spy.size());

        //對spy對象依舊可以來驗證其行為
        verify(spy).add("one");
        verify(spy).add("two");
    }
           
6.2.10 參數捕捉
@Test
    public void testMock11() {
        List list = mock(List.class);
        ArgumentCaptor<String> args = ArgumentCaptor.forClass(String.class);
        list.add("one");

        //驗證後再捕捉參數
        verify(list).add(args.capture());
        Assert.assertEquals("one", args.getValue());
    }
           
6.2.11 重置mocks
@Test
    public void testMock12() {
        List list = mock(List.class);
        when(list.size()).thenReturn(100);

        //列印出"100"
        System.out.println(list.size());
        //充值mock, 之前的互動和stub将全部失效
        reset(list);
        //列印出"0"
        System.out.println(list.size());
    }
           
6.2.12 更多的注解

使用注解都需要預先進行配置,怎麼配置見6.2.7說明

  • @Captor 替代ArgumentCaptor
  • @Spy 替代spy(Object)
  • @Mock 替代mock(Class)
  • @InjectMocks 建立一個執行個體,其餘用@Mock(或@Spy)注解建立的mock将被注入到用該執行個體中

6.3 容易概念混淆的幾個點

6.3.1 @Mock與@Spy的異同
  • Mock對象隻能調用stubbed方法,不能調用其真實的方法。而Spy對象可以監視一個真實的對象,對Spy對象進行方法調用時,會調用真實的方法。
  • 兩者都可以stubbing對象的方法,讓方法傳回我們的期望值。
  • 兩者無論是否是真實的方法調用,都可進行verify驗證。
  • 對final類、匿名類、java的基本資料類型是無法進行mock或者spy的。
  • 注意mockito是不能mock static方法的。
6.3.2 @InjectMocks與@Mock等的差別

@Mock:建立一個mock對象。

@InjectMocks:建立一個執行個體對象,然後将@Mcok或者@Spy注解建立的mock對象注入到該執行個體對象中。

stackoverflow上對這個有一個比較形象的解釋:

https://stackoverflow.com/questions/16467685/difference-between-mock-and-injectmocks
@RunWith(MockitoJUnitRunner.class)
public class SomeManagerTest {

    @InjectMocks
    private SomeManager someManager;

    @Mock
    private SomeDependency someDependency; // 該mock對象會被注入到someManager對象中

    //你不用向下面這樣執行個體化一個SomeManager對象,@InjectMocks會自動幫你實作
    //SomeManager someManager = new SomeManager();    
    //SomeManager someManager = new SomeManager(someDependency);

}
           
6.3.3 when(...).thenReturn()與doReturn(...).when(...)兩種文法的異同
  • 兩者都是用來stubbing方法的,大部分情況下,兩者可以表達同樣的意思,與Java裡的do/while、while/do語句類似。
  • 對void方法不能使用when/thenReturn文法。
  • 對spy對象要慎用when/thenReturn,如:
List spyList = spy(new ArrayList());

        //下面代碼會抛出IndexOutOfBoundsException
        when(spyList.get(0)).thenReturn("foo");
       
        //這裡不會抛出異常
        doReturn("foo").when(spyList).get(0);
        System.out.println(spyList.get(0));
           

這段代碼運作會抛出異常,當調用when(spyList.get(0)).thenReturn("foo")時,會調用真實對象的get(0),由于list是空的是以會抛出IndexOutOfBoundsException異常。用doReturn/when文法則不會,因為它不會真實調用get(0)方法。

個人覺得讨論哪種文法好是沒有意義的,推薦使用doReturn/when文法,不管是mock還是spy對象都适用。

6.4 小結

本文主要介紹了mockito架構的使用方法,以及為什麼要使用mockito來進行單元測試。熟練掌握mockito的常用方法,對我們來寫單元測試來說絕對是事半功倍。

系列文章:

Android單元測試(一):前言 Android單元測試(二):什麼是單元測試 Android單元測試(三):測試難點及方案選擇 Android單元測試(四):JUnit介紹 Android單元測試(五):JUnit進階 Android單元測試(六):Mockito學習 Android單元測試(七):Robolectric介紹 Android單元測試(八):怎樣測試異步代碼

繼續閱讀