1. 程式人生 > >一文看懂Mockito『手把手教你 Mockito 的使用』

一文看懂Mockito『手把手教你 Mockito 的使用』

什麼是 Mockito

Mockito 是一個強大的用於 Java 開發的模擬測試框架, 通過 Mockito 我們可以建立和配置 Mock 物件, 進而簡化有外部依賴的類的測試.
使用 Mockito 的大致流程如下:

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

  • 執行測試程式碼.

  • 校驗測試程式碼是否執行正確.

為什麼使用 Mockito

我們已經知道了 Mockito 主要的功能就是建立 Mock 物件, 那麼什麼是 Mock 物件呢? 對 Mock 物件不是很瞭解的朋友, 可以參考這篇文章.
現在我們對 Mock 物件有了一定的瞭解了, 那麼自然就會有人問了, 為什麼要使用 Mock 物件? 使用它有什麼好處呢?
下面我們以一個簡單的例子來展示一下 Mock 物件到底有什麼用.
假設我們正在編寫一個銀行的服務 BankService, 這個服務的依賴關係如下:

 

當我們需要測試 BankService 服務時, 該真麼辦呢?
一種方法是構建真實的 BankDao, DB, AccountService 和 AuthService 例項, 然後注入到 BankService 中.
不用我說, 讀者們也肯定明白, 這是一種既笨重又繁瑣的方法, 完全不符合單元測試的精神. 那麼還有一種更加優雅的方法嗎? 自然是有的, 那就是我們今天的主角 Mock Object. 下面來看一下使用 Mock 物件後的框架圖:

 

我們看到, BankDao, AccountService 和 AuthService 都被我們使用了虛擬的物件(Mock 物件) 來替換了, 因此我們就可以對 BankService 進行測試, 而不需要關注它的複雜的依賴了.

Mockito 基本使用

為了簡潔期間, 下面的程式碼都省略了靜態匯入 import static org.mockito.Mockito.*;

Maven 依賴

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>2.0.111-beta</version>
</dependency>

建立 Mock 物件

@Test
public void createMockObject() {
    // 使用 mock 靜態方法建立 Mock 物件.
    List mockedList = mock(List.class);
    Assert.assertTrue(mockedList instanceof List);

    // mock 方法不僅可以 Mock 介面類, 還可以 Mock 具體的型別.
    ArrayList mockedArrayList = mock(ArrayList.class);
    Assert.assertTrue(mockedArrayList instanceof List);
    Assert.assertTrue(mockedArrayList instanceof ArrayList);
}

如上程式碼所示, 我們呼叫了 mock 靜態方法來建立一個 Mock 物件. mock 方法接收一個 class 型別, 即我們需要 mock 的型別.

配置 Mock 物件

當我們有了一個 Mock 物件後, 我們可以定製它的具體的行為. 例如:

@Test
public void configMockObject() {
    List mockedList = mock(List.class);

    // 我們定製了當呼叫 mockedList.add("one") 時, 返回 true
    when(mockedList.add("one")).thenReturn(true);
    // 當呼叫 mockedList.size() 時, 返回 1
    when(mockedList.size()).thenReturn(1);

    Assert.assertTrue(mockedList.add("one"));
    // 因為我們沒有定製 add("two"), 因此返回預設值, 即 false.
    Assert.assertFalse(mockedList.add("two"));
    Assert.assertEquals(mockedList.size(), 1);

    Iterator i = mock(Iterator.class);
    when(i.next()).thenReturn("Hello,").thenReturn("Mockito!");
    String result = i.next() + " " + i.next();
    //assert
    Assert.assertEquals("Hello, Mockito!", result);
}

我們使用 when(​...).thenReturn(​...) 方法鏈來定義一個行為, 例如 "when(mockedList.add("one")).thenReturn(true)" 表示: 當呼叫了mockedList.add("one"), 那麼返回 true.. 並且要注意的是, when(​...).thenReturn(​...) 方法鏈不僅僅要匹配方法的呼叫, 而且要方法的引數一樣才行.
而且有趣的是, when(​...).thenReturn(​...) 方法鏈可以指定多個返回值, 當這樣做後, 如果多次呼叫指定的方法, 那麼這個方法會依次返回這些值. 例如 "when(i.next()).thenReturn("Hello,").thenReturn("Mockito!");", 這句程式碼表示: 第一次呼叫 i.next() 時返回 "Hello,", 第二次呼叫 i.next() 時返回 "Mockito!".

上面的例子我們展示了方法呼叫返回值的定製, 那麼我們可以指定一個丟擲異常嗎? 當然可以的, 例如:

@Test(expected = NoSuchElementException.class)
public void testForIOException() throws Exception {
    Iterator i = mock(Iterator.class);
    when(i.next()).thenReturn("Hello,").thenReturn("Mockito!"); // 1
    String result = i.next() + " " + i.next(); // 2
    Assert.assertEquals("Hello, Mockito!", result);

    doThrow(new NoSuchElementException()).when(i).next(); // 3
    i.next(); // 4
}

上面程式碼的第一第二步我們已經很熟悉了, 接下來第三部我們使用了一個新語法: doThrow(ExceptionX).when(x).methodCall, 它的含義是: 當呼叫了 x.methodCall 方法後, 丟擲異常 ExceptionX.
因此 doThrow(new NoSuchElementException()).when(i).next() 的含義就是: 當第三次呼叫 i.next() 後, 丟擲異常 NoSuchElementException.(因為 i 這個迭代器只有兩個元素)

校驗 Mock 物件的方法呼叫

Mockito 會追蹤 Mock 物件的所用方法呼叫和呼叫方法時所傳遞的引數. 我們可以通過 verify() 靜態方法來來校驗指定的方法呼叫是否滿足斷言. 語言描述有一點抽象, 下面我們仍然以程式碼來說明一下.

@Test
public void testVerify() {
    List mockedList = mock(List.class);
    mockedList.add("one");
    mockedList.add("two");
    mockedList.add("three times");
    mockedList.add("three times");
    mockedList.add("three times");
    when(mockedList.size()).thenReturn(5);
    Assert.assertEquals(mockedList.size(), 5);

    verify(mockedList, atLeastOnce()).add("one");
    verify(mockedList, times(1)).add("two");
    verify(mockedList, times(3)).add("three times");
    verify(mockedList, never()).isEmpty();
}

上面的例子前半部份沒有什麼特別的, 我們關注後面的:

verify(mockedList, atLeastOnce()).add("one");
verify(mockedList, times(1)).add("two");
verify(mockedList, times(3)).add("three times");
verify(mockedList, never()).isEmpty();

讀者根據程式碼也應該可以猜測出它的含義了, 很簡單:

  • 第一句校驗 mockedList.add("one") 至少被呼叫了 1 次(atLeastOnce)

  • 第二句校驗 mockedList.add("two") 被呼叫了 1 次(times(1))

  • 第三句校驗 mockedList.add("three times") 被呼叫了 3 次(times(3))

  • 第四句校驗 mockedList.isEmpty() 從未被呼叫(never)

使用 spy() 部分模擬物件

Mockito 提供的 spy 方法可以包裝一個真實的 Java 物件, 並返回一個包裝後的新物件. 若沒有特別配置的話, 對這個新物件的所有方法呼叫, 都會委派給實際的 Java 物件. 例如:

@Test
public void testSpy() {
    List list = new LinkedList();
    List spy = spy(list);

    // 對 spy.size() 進行定製.
    when(spy.size()).thenReturn(100);

    spy.add("one");
    spy.add("two");

    // 因為我們沒有對 get(0), get(1) 方法進行定製,
    // 因此這些呼叫其實是呼叫的真實物件的方法.
    Assert.assertEquals(spy.get(0), "one");
    Assert.assertEquals(spy.get(1), "two");

    Assert.assertEquals(spy.size(), 100);
}

這個例子中我們例項化了一個 LinkedList 物件, 然後使用 spy() 方法對 list 物件進行部分模擬. 接著我們使用 when(...).thenReturn(...) 方法鏈來規定 spy.size() 方法返回值是 100. 隨後我們給 spy 添加了兩個元素, 然後再 呼叫 spy.get(0) 獲取第一個元素.
這裡有意思的地方是: 因為我們沒有定製 add("one"), add("two"), get(0), get(1), 因此通過 spy 呼叫這些方法時, 實際上是委派給 list 物件來呼叫的. 
然而我們 定義了 spy.size() 的返回值, 因此當呼叫 spy.size() 時, 返回 100.

引數捕獲

Mockito 允准我們捕獲一個 Mock 物件的方法呼叫所傳遞的引數, 例如:

@Test
public void testCaptureArgument() {
    List<String> list = Arrays.asList("1", "2");
    List mockedList = mock(List.class);
    ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class);
    mockedList.addAll(list);
    verify(mockedList).addAll(argument.capture());

    Assert.assertEquals(2, argument.getValue().size());
    Assert.assertEquals(list, argument.getValue());
}

我們通過 verify(mockedList).addAll(argument.capture()) 語句來獲取 mockedList.addAll 方法所傳遞的實參 list.