單元測試利器Mockito框架

前言

Mockito 是當前最流行的 單元測試 Mock 框架。採用 Mock 框架,咱們能夠 虛擬 出一個 外部依賴,下降測試 組件 之間的 耦合度,只注重代碼的 流程與結果,真正地實現測試目的。java

正文

什麼是Mock

Mock 的中文譯爲仿製的,模擬的,虛假的。對於測試框架來講,即構造出一個模擬/虛假的對象,使咱們的測試能順利進行下去。android

Mock 測試就是在測試過程當中,對於某些 不容易構造(如 HttpServletRequest 必須在 Servlet 容器中才能構造出來)或者不容易獲取 比較複雜 的對象(如 JDBC 中的 ResultSet 對象),用一個 虛擬 的對象(Mock 對象)來建立,以便測試方法。編程

爲何使用Mock測試

單元測試 是爲了驗證咱們的代碼運行正確性,咱們注重的是代碼的流程以及結果的正確與否。後端

對比真實運行代碼,可能其中有一些 外部依賴 的構建步驟相對麻煩,若是咱們仍是按照真實代碼的構建規則構造出外部依賴,會大大增長單元測試的工做,代碼也會參雜太多非測試部分的內容,測試用例顯得複雜難懂。緩存

採用 Mock 框架,咱們能夠 虛擬 出一個 外部依賴,只注重代碼的 流程與結果,真正地實現測試目的。安全

Mock測試框架的好處

  1. 能夠很簡單的虛擬出一個複雜對象(好比虛擬出一個接口的實現類);
  2. 能夠配置 mock 對象的行爲;
  3. 可使測試用例只注重測試流程與結果;
  4. 減小外部類、系統和依賴給單元測試帶來的耦合。

Mockito的流程

如圖所示,使用 Mockito 的大體流程以下:多線程

  1. 建立 外部依賴Mock 對象, 而後將此 Mock 對象注入到 測試類 中;架構

  2. 執行 測試代碼app

  3. 校驗 測試代碼 是否執行正確。框架

Mockito的使用

Modulebuild.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/
  • mockito-android: 用於 設備測試,即須要運行 android 設備進行測試,其測試代碼路徑位於 module-name/src/androidTest/java/

mockito-core最新版本能夠在 Maven 中查詢:mockito-core。 mockito-android最新版本能夠在 Maven 中查詢:mockito-android

Mockito的使用示例

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

這裏摘用官網的 Demo:

檢驗調對象相關行爲是否被調用

import static org.mockito.Mockito.*;

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

// Use 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() 會經過。

配置/方法行爲

// 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 出來的對象其實質就是一個 代理,該代理在 沒有配置/指定行爲 的狀況下,默認返回 空值

上面的 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 種方法:

  1. JUnit 測試類添加 @RunWith(MockitoJUnitRunner.class)

  2. 在標示有 @Before 方法內調用初始化方法:MockitoAnnotations.initMocks(Object)

上面的測試用例,對於 @Mock 等註解的成員變量的初始化又多了一種方式 MockitoRule。規則 MockitoRule 會自動幫咱們調用 MockitoAnnotations.initMocks(this)實例化註解 的成員變量,咱們就無需手動進行初始化了。

Mockito的重要方法

實例化虛擬對象

// 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/Boolean 則返回 false

  • 行爲配置(stub)是能夠被複寫的:好比一般的對象行爲是具備必定的配置,可是測試方法能夠複寫這個行爲。請謹記行爲複寫可能代表潛在的行爲太多了。

  • 一旦配置了行爲,方法老是會返回 配置值,不管該方法被調用了多少次。

  • 最後一次行爲配置是更加劇要的,當你爲一個帶有相同參數的相同方法配置了不少次,最後一次起做用。

參數匹配

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() 等方法。

校驗次數

LinkedList mockedList = mock(LinkedList.class);
// Use mock
mockedList.add("once");
mockedList.add("twice");
mockedList.add("twice");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");

// Follow 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次

拋出異常

doThrow(new RuntimeException()).when(mockedList).clear();
// following throws RuntimeException
mockedList.clear();
複製代碼

按順序校驗

有時對於一些行爲,有前後順序之分,因此,當咱們在校驗時,就須要考慮這個行爲的前後順序:

// A. Single mock whose methods must be invoked in a particular order
List singleMock = mock(List.class);
// Use 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);
// Use 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");
複製代碼

存根連續調用

對於同一個方法,若是咱們想讓其在 屢次調用 中分別 返回不一樣 的數值,那麼就可使用存根連續調用:

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")
複製代碼

無返回值函數

對於 返回類型void 的方法,存根要求使用另外一種形式的 when(Object) 函數,由於編譯器要求括號內不能存在 void 方法。

例如,存根一個返回類型爲 void 的方法,要求調用時拋出一個異常:

doThrow(new RuntimeException()).when(mockedList).clear();
// Following throws RuntimeException:
mockedList.clear();
複製代碼

監視真實對象

前面使用的都是 mock 出來一個對象。這樣,當 沒有配置/存根 其具體行爲的話,結果就會返回 空類型。而若是使用 特務對象spy),那麼對於 沒有存根 的行爲,它會調用 原來對象 的方法。能夠把 spy 想象成局部 mock

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

// Optionally, you can stub out some methods:
when(spy.size()).thenReturn(100);
// Use 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();
複製代碼

測試驅動開發

行爲驅動開發 的格式使用 //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());
 }
複製代碼

自定義錯誤校驗輸出信息

// 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();
複製代碼

@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

參數捕捉

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的侷限

  1. 不能 mock 靜態方法;
  2. 不能 mock 構造器;
  3. 不能 mock equals()hashCode() 方法。

歡迎關注技術公衆號:零壹技術棧

零壹技術棧

本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。

相關文章
相關標籤/搜索