單元測試框架:Mockito

什么是 Mock

mock 的中文譯為: 仿制的,模擬的,虛假的。對于測試框架來說,即構造出一個模擬/虛假的對象,使我們的測試能順利進行下去。

為什么要使用 Mock 測試框架

單元測試是為了驗證我們的代碼運行正確性,我們注重的是代碼的流程以及結果的正確與否。而對比于真實運行代碼,可能其中有一些外部依賴的構建步驟相對麻煩,如果我們還是按照真實代碼的構建規則構造出外部依賴,會大大增加單元測試的工作,代碼也會參雜太多非測試部分的內容,測試用例顯得復雜難懂,而采用 Mock 框架,我們可以虛擬出一個外部依賴,只注重代碼的流程與結果,真正地實現測試目的。

Mock 測試框架好處

  • 可以很簡單的虛擬出一個復雜對象(比如虛擬出一個接口的實現類)
  • 可以配置 mock 對象的行為
  • 可以使測試用例只注重測試流程與結果
  • 減少外部類或系統帶來的副作用
    ······

Mockito 簡介

Most popular Mocking framework for unit tests written in Java

Mockito 是當前最流行的單元測試 mock 框架。

使用

在 Module 的 build.gradle中添加如下內容:

dependencies {
    //Mockito for unit tests
    testImplementation "org.mockito:mockito-core:2.+"
    //Mockito for Android tests
    androidTestImplementation 'org.mockito:mockito-android:2.+'
}

這里稍微解釋下:
mockito-core用于本地單元測試,其測試代碼路徑位于:module-name/src/test/java/
mockiot-android用于儀器測試,即需要運行android設備進行測試,其測試代碼路徑位于: module-name/src/androidTest/java/
更多詳細信息,請查看官網:測試應用

ps:
mockito-core最新版本可以在 Maven 中查詢:mockito-core
mockito-android最新版本可以在 Maven 中查詢:mockito-android

示例

  1. 普通單元測試使用 mockitomockito-core),路徑:module-name/src/test/java/

這里摘用官網的Demo:

now you can verify interactions -- 檢驗調對象相關行為是否被調用

import static org.mockito.Mockito.*;

// mock creation
List mockedList = mock(List.class);

// using mock object - it does not throw any "unexpected interaction" exception
mockedList.add("one"); //調用了add("one")行為
mockedList.clear();//調用了clear()行為

// selective, explicit, highly readable verification
verify(mockedList).add("one");//檢驗add("one")是否已被調用
verify(mockedList).clear();//檢驗clear()是否已被調用

這里 mock 了一個 List(這里只是為了用作 Demo 示例,通常對于 List 這種簡單的類對象創建而言,直接 new 一個真實的對象即可,無需進行 mock),verify 會檢驗對象是否在前面已經執行了相關行為,這里mockedListverify之前已經執行了add("one")clear()行為,所以verify會通過。

stub method calls -- 配置/方法行為

// you can mock concrete classes, not only interfaces
LinkedList mockedList = mock(LinkedList.class);

// stubbing appears before the actual execution
when(mockedList.get(0)).thenReturn("first");

// the following prints "first"
System.out.println(mockedList.get(0));

// the following prints "null" because get(999) was not stubbed
System.out.println(mockedList.get(999));

這里對幾個比較重要的點進行解析:

  • when(mockedList.get(0)).thenReturn("first")
    這句話 Mockito 會解析為:當對象 mockedList調用方法方法get并且參數為0時,返回結果為"first",這相當于定制了我們 mock 對象的行為結果(mock LinkedList對象為mockedList,指定其行為get(0)返回結果為"first")。
  • mockedList.get(999)
    由于mockedList沒有指定get(999)的行為,所以其結果為null。因為 Mockito 的底層原理是使用 cglib 動態生成一個代理類對象,因此,mock 出來的對象其實質就是一個代理,該代理在沒有配置/指定行為的情況下,默認返回空值:
  1. Android單元測試使用 mockitomockito-android),路徑:module-name/src/androidTest/java/,該測試需運行安卓真機/模擬器。

上面的 Demo 使用的是靜態方法mock模擬出一個實例,我們還可以通過注解@Mock也模擬出一個實例:

    @Mock
    private Intent mIntent;

    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();

    @Test
    public void mockAndroid(){
        Intent intent = mockIntent();
        assertThat(intent.getAction()).isEqualTo("com.yn.test.mockito");
        assertThat(intent.getStringExtra("Name")).isEqualTo("Whyn");
    }

    private Intent mockIntent(){
        when(mIntent.getAction()).thenReturn("com.yn.test.mockito");
        when(mIntent.getStringExtra("Name")).thenReturn("Whyn");
        return mIntent;
    }

對于 @Mock, @Spy, @InjectMocks等注解的成員變量的初始化到目前為止有2種方法:

現在,正如我們上面的測試用例所示,對于@Mock等注解的成員變量的初始化又多了一種方法:MockitoRule
規則MockitoRule會自動幫我們調用MockitoAnnotations.initMocks(this)去實例化出注解的成員變量,我們就無需手動進行初始化了。

ps:上面要注意的一個點就是,斷言使用的是開源庫:Truth

更多Demo,請查看:Mockito

Mockito 一些重要方法簡介

  1. @Mock/mock:實例化虛擬對象
//You can mock concrete classes, not just interfaces
LinkedList mockedList = mock(LinkedList.class);

//stubbing
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());

//following prints "first"
System.out.println(mockedList.get(0));

//following throws runtime exception
System.out.println(mockedList.get(1));

//following prints "null" because get(999) was not stubbed
System.out.println(mockedList.get(999));

//Although it is possible to verify a stubbed invocation, usually it's just redundant
//If your code cares what get(0) returns, then something else breaks (often even before verify() gets executed).
//If your code doesn't care what get(0) returns, then it should not be stubbed. Not convinced? See here.
verify(mockedList).get(0);
  • 對于所有方法,mock對象默認返回null,原始類型/原始類型包裝類默認值,或者空集合。比如對于int/Integer類型,則返回0,對于boolean/Bollean則返回false

  • 行為配置(stub)是可以被復寫的:比如通常的對象行為是具有一定的配置,但是測試方法可以復寫這個行為。請謹記行為復寫可能表明潛在的行為太多了。

  • 一旦配置了行為,方法總是會返回配置值,無論該方法被調用了多少次。

  • 最后一次行為配置是更加重要的 - 當你為一個帶有相同參數的相同方法配置了很多次,最后一次起作用。

  1. Argument matchers - 參數匹配
    Mockito 通過參數對象的equals()方法來驗證參數是否一致,當需要更多的靈活性時,可以使用參數匹配器:

 //stubbing using built-in anyInt() argument matcher
 when(mockedList.get(anyInt())).thenReturn("element");

 //stubbing using custom matcher (let's say isValid() returns your own matcher implementation):
 when(mockedList.contains(argThat(isValid()))).thenReturn("element");

 //following prints "element"
 System.out.println(mockedList.get(999));

 //you can also verify using an argument matcher
 verify(mockedList).get(anyInt());

 //argument matchers can also be written as Java 8 Lambdas
 verify(mockedList).add(argThat(someString -> someString.length() > 5));

參數匹配器允許更加靈活的驗證和行為配置。更多內置匹配器和自定義參數匹配器例子請參考:ArgumentMatchersMockitoHamcrest

注意:如果使用了參數匹配器,那么所有的參數都需要提供一個參數匹配器:

verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
//above is correct - eq() is also an argument matcher

verify(mock).someMethod(anyInt(), anyString(), "third argument");
//above is incorrect - exception will be thrown because third argument is given without an argument matcher.

類似anyObject()eq()這類匹配器并不返回匹配數值。他們內部記錄一個匹配器堆棧并返回一個空值(通常為null)。這個實現是為了匹配 java 編譯器的靜態類型安全,這樣做的后果就是你不能在檢驗/配置方法外使用anyObject()eq()等方法。

  1. Verifying exact number of invocations / at least x / never - 校驗次數

 //using mock
 mockedList.add("once");

 mockedList.add("twice");
 mockedList.add("twice");

 mockedList.add("three times");
 mockedList.add("three times");
 mockedList.add("three times");

 //following two verifications work exactly the same - times(1) is used by default
 verify(mockedList).add("once");
 verify(mockedList, times(1)).add("once");

 //exact number of invocations verification
 verify(mockedList, times(2)).add("twice");
 verify(mockedList, times(3)).add("three times");

 //verification using never(). never() is an alias to times(0)
 verify(mockedList, never()).add("never happened");

 //verification using atLeast()/atMost()
 verify(mockedList, atLeastOnce()).add("three times");
 verify(mockedList, atLeast(2)).add("three times");
 verify(mockedList, atMost(5)).add("three times");

校驗次數方法常用的有如下幾個:

Method Meaning
times(n) 次數為n,默認為1(times(1)
never() 次數為0,相當于times(0)
atLeast(n) 最少n次
atLeastOnce 最少一次
atMost(n) 最多n次

4. 配置返回類型為void的方法拋出異常:doThrow

doThrow(new RuntimeException()).when(mockedList).clear();

//following throws RuntimeException:
mockedList.clear();

5. Verification in order - 按順序校驗
有時對于一些行為,有先后順序之分,所以,當我們在校驗時,就需要考慮這個行為的先后順序:

// A. Single mock whose methods must be invoked in a particular order
 List singleMock = mock(List.class);

 //using a single mock
 singleMock.add("was added first");
 singleMock.add("was added second");

 //create an inOrder verifier for a single mock
 InOrder inOrder = inOrder(singleMock);

 //following will make sure that add is first called with "was added first, then with "was added second"
 inOrder.verify(singleMock).add("was added first");
 inOrder.verify(singleMock).add("was added second");

//----------------------------------------------------------------

 // B. Multiple mocks that must be used in a particular order
 List firstMock = mock(List.class);
 List secondMock = mock(List.class);

 //using mocks
 firstMock.add("was called first");
 secondMock.add("was called second");

 //create inOrder object passing any mocks that need to be verified in order
 InOrder inOrder = inOrder(firstMock, secondMock);

 //following will make sure that firstMock was called before secondMock
 inOrder.verify(firstMock).add("was called first");
 inOrder.verify(secondMock).add("was called second");

 // Oh, and A + B can be mixed together at will
  1. Stubbing consecutive calls (iterator-style stubbing) - 存根連續調用
    對于同一個方法,如果我們想讓其在多次調用中分別返回不同的數值,那么就可以使用存根連續調用:

 when(mock.someMethod("some arg"))
   .thenThrow(new RuntimeException())
   .thenReturn("foo");

 //First call: throws runtime exception:
 mock.someMethod("some arg");

 //Second call: prints "foo"
 System.out.println(mock.someMethod("some arg"));

 //Any consecutive call: prints "foo" as well (last stubbing wins).
 System.out.println(mock.someMethod("some arg"));

也可以使用下面更簡潔的存根連續調用方法:

 when(mock.someMethod("some arg"))
   .thenReturn("one", "two", "three");

注意:存根連續調用要求必須使用鏈式調用,如果使用的是同個方法的多個存根配置,那么只有最后一個起作用(覆蓋前面的存根配置)。

//All mock.someMethod("some arg") calls will return "two"
 when(mock.someMethod("some arg"))
   .thenReturn("one")
 when(mock.someMethod("some arg"))
   .thenReturn("two")
  1. doReturn()|doThrow()| doAnswer()|doNothing()|doCallRealMethod() family of methods
    對于返回類型為void的方法,存根要求使用另一種形式的when(Object)函數,因為編譯器要求括號內不能存在void方法。
    例如,存根一個返回類型為void的方法,要求調用時拋出一個異常:
   doThrow(new RuntimeException()).when(mockedList).clear();

   //following throws RuntimeException:
   mockedList.clear();
  1. Spying on real objects - 監視真實對象
    我們前面使用的都是 mock 出來一個對象,這樣,當我們沒有配置/存根其具體行為的話,結果就會返回空類型。而使用特務對象(spy),那么對于我們沒有存根的行為,它會調用原來對象的方法。可以把spy想象成 局部mock
   List list = new LinkedList();
   List spy = spy(list);

   //optionally, you can stub out some methods:
   when(spy.size()).thenReturn(100);

   //using the spy calls *real* methods
   spy.add("one");
   spy.add("two");

   //prints "one" - the first element of a list
   System.out.println(spy.get(0));

   //size() method was stubbed - 100 is printed
   System.out.println(spy.size());

   //optionally, you can verify
   verify(spy).add("one");
   verify(spy).add("two");

注意:由于spy局部mock,所以有時候使用when(Object)時,無法做到存根作用,此時,就可以考慮使用doReturn|Answer|Throw()這類方法進行存根:

   List list = new LinkedList();
   List spy = spy(list);

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

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

spy并不是對真實對象的代理,相反的,它對傳遞過來的真實對象進行復制,所以,對于任何真實對象的操作,spy對象并不會感知到,同理,對spy對象的任何操作,也不會影響到真實對象。

當然,如果你想使用mock對象進行 局部mock,通過doCallRealMethod|thenCallRealMethod方法也是可以的:

//you can enable partial mock capabilities selectively on mocks:
    Foo mock = mock(Foo.class);
    //Be sure the real implementation is 'safe'.
    //If real implementation throws exceptions or depends on specific state of the object then you're in trouble.
    when(mock.someMethod()).thenCallRealMethod();
  1. Aliases for behavior driven development (Since 1.8.0) - 測試驅動開發
    以行為驅動開發格式使用 //given //when //then 注釋為測試用法基石編寫測試用例,這正是 Mockito 官方編寫測試用例方法,強烈建議使用這種方式進行測試編寫。
 import static org.mockito.BDDMockito.*;

 Seller seller = mock(Seller.class);
 Shop shop = new Shop(seller);

 public void shouldBuyBread() throws Exception {
   //given
   given(seller.askForBread()).willReturn(new Bread());

   //when
   Goods goods = shop.buyBread();

   //then
   assertThat(goods, containBread());
 }
  1. Custom verification failure message (Since 2.1.0) - 自定義錯誤校驗輸出信息
 // will print a custom message on verification failure
 verify(mock, description("This will print on failure")).someMethod();

 // will work with any verification mode
 verify(mock, times(2).description("someMethod should be called twice")).someMethod();

11.@InjectMock -- 構造器,方法,成員變量依賴注入
使用@InjectMock注解時,Mockito 會為類構造器,方法或者成員變量依據它們的類型進行自動mock

public class InjectMockTest {
    @Mock
    private User user;
    @Mock
    private ArticleDatabase database;
    @InjectMocks
    private ArticleManager manager;
    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();

    @Test
    public void testInjectMock() {
        // calls addListener with an instance of ArticleListener
        manager.initialize();
        // validate that addListener was called
        verify(database).addListener(any(ArticleListener.class));
    }

    public static class ArticleManager {
        private User user;
        private ArticleDatabase database;

        public ArticleManager(User user, ArticleDatabase database) {
            super();
            this.user = user;
            this.database = database;
        }

        public void initialize() {
            database.addListener(new ArticleListener());
        }
    }

    public static class User {
    }

    public static class ArticleListener {
    }

    public static class ArticleDatabase {
        public void addListener(ArticleListener listener) {
        }
    }
}

成員變量manager類型為ArticleManager,其上注解了@InjectMocks,所以要mockmanagerMockito 會自動mockArticleManager所需的構造參數(即userdatabase),最終mock得到一個ArticleManager,賦值給manager

  1. ArgumentCaptor -- 參數捕捉
    ArgumentCaptor允許我們在verify的時候獲取方法參數內容,這使得我們能在測試過程中能對調用方法參數進行捕捉并測試。
    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();
    @Captor
    private ArgumentCaptor<List<String>> captor;
    @Test
    public void testArgumentCaptor(){
        List<String> asList = Arrays.asList("someElement_test", "someElement");
        final List<String> mockedList = mock(List.class);
        mockedList.addAll(asList);

        verify(mockedList).addAll(captor.capture());//when verify,you can capture the arguments of the calling method
        final List<String> capturedArgument = captor.getValue();
        assertThat(capturedArgument, hasItem("someElement"));
    }

Mocktio 限制

  • 不能mock靜態方法
  • 不能mock構造器
  • 不能mock方法equals(), hashCode()
    更多限制點,請查看:FAQ

PowerMockito

針對 Mocktio 無法mock靜態方法等限制,使用 PowerMockito 則可以解決這一限制。

  1. Dowanload:
 testImplementation "org.mockito:mockito-core:2.8.47"
 testImplementation 'org.powermock:powermock-api-mockito2:1.7.1'
 testImplementation 'org.powermock:powermock-module-junit4:1.7.1'

詳情請查看:Mockito 2 Maven

注: 上面只所以不使用最新的 Mockito 版本,是因為根據 PowerMockito 官方文檔,目前 PowerMockito 版本對應支持的 Mockito 版本如下圖所示:

Supported versions

因此,這里就選擇 Mockito 2.8.47(2.8.x最新版本)

  1. 示例
//static method
public class Static {
    public static String firstStaticMethod() {
        return "I am a firstStatic method";
    }

    public static String secondStaticMethod() {
        return "I am a secondStatic method";
    }
}
//

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容