單元測試利器Mockito框架

零壹技術棧發表於2019-03-03

前言

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

正文

什麼是Mock

Mock 的中文譯為仿製的,模擬的,虛假的。對於測試框架來說,即構造出一個模擬/虛假的物件,使我們的測試能順利進行下去。

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

為什麼使用Mock測試

單元測試 是為了驗證我們的程式碼執行正確性,我們注重的是程式碼的流程以及結果的正確與否。

對比真實執行程式碼,可能其中有一些 外部依賴 的構建步驟相對麻煩,如果我們還是按照真實程式碼的構建規則構造出外部依賴,會大大增加單元測試的工作,程式碼也會參雜太多非測試部分的內容,測試用例顯得複雜難懂。

採用 Mock 框架,我們可以 虛擬 出一個 外部依賴,只注重程式碼的 流程與結果,真正地實現測試目的。

Mock測試框架的好處

  1. 可以很簡單的虛擬出一個複雜物件(比如虛擬出一個介面的實現類);
  2. 可以配置 mock 物件的行為;
  3. 可以使測試用例只注重測試流程與結果;
  4. 減少外部類、系統和依賴給單元測試帶來的耦合。

Mockito的流程

單元測試利器Mockito框架

如圖所示,使用 Mockito 的大致流程如下:

  1. 建立 外部依賴Mock 物件, 然後將此 Mock 物件注入到 測試類 中;

  2. 執行 測試程式碼

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

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

零壹技術棧

本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。

相關文章