單元測試模擬框架Mockito

東陸之滇發表於2019-03-14

概覽

Mockito 是Java中用於單元測試的模擬框架。

引入 pom 依賴

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
    <version>LATEST</version>
</dependency>
複製程式碼

啟用 Mockito

通過class引數(類、介面)建立一個mock物件,該物件與真實建立的物件有所區別。 使用 Mockito 類的一系列靜態方法。

public static <T> T mock(Class <T> classToMock)
複製程式碼

編寫一個 Mockito 示例

Mockito 需要依賴Junit。

package org.byron4j.cookbook.mocketio.basic;

import org.junit.Test;
import org.mockito.Mockito;

import java.util.ArrayList;
import java.util.List;

import static org.junit.Assert.assertEquals;

public class MockitoAnnotationTest {

    @Test
    public void whenNotUseMockAnnotation_thenCorrect() {
        // 建立一個mock出來的ArrayList物件
        List mockList = Mockito.mock(ArrayList.class);

        // 呼叫mock物件的方法
        mockList.add("one");
        //mockList.add("one");

        // 獲取mock物件的實際方法,獲取size,結果為0
        System.out.println("mockList.size(): " + mockList.size());
        // toString方法
        System.out.println("mockList's toString is: " + mockList);

        // 驗證mock物件mockList的add方法是否被呼叫了一次
        Mockito.verify(mockList).add("one");
        assertEquals(0, mockList.size());

        // 當呼叫mockList.size()的時候,總是返回100
        Mockito.when(mockList.size()).thenReturn(100);

        assertEquals(100, mockList.size());
    }
}

複製程式碼

執行輸出結果為:

mockList.size(): 0
mockList's toString is: Mock for ArrayList, hashCode: 409962262
複製程式碼
  • 使用List mockList = Mockito.mock(ArrayList.class);建立一個mock出來的ArrayList物件mockList
  • 呼叫mock物件的方法mockList.add("one");
  • 然後呼叫mockList.size()結果為0,表明mockList.add("one");程式碼僅僅表示發生了add行為本身,並不會對mockList的其他行為產生影響。
  • verify方法,驗證mock物件mockList的add方法是否被呼叫了一次
Mockito.verify(mockList).add("one");
assertEquals(0, mockList.size());
複製程式碼
  • 當呼叫mockList.size()的時候,總是返回100
Mockito.when(mockList.size()).thenReturn(100);
assertEquals(100, mockList.size());
複製程式碼

verify 方法

驗證包含的行為(方法)發生過一次(被呼叫一次),即verify(mock, times(1)),例如: verify(mock).someMethod("some arg");。等效於 verify(mock, times(1)).someMethod("some arg");

Mockito.verify(mockList).add("one");
複製程式碼

等效於

Mockito.verify(mockList, Mockito.times(1)).add("one");
複製程式碼

Mockito.times(1) 引數1表示期望執行的次數是1。

verify 方法傳入兩個引數:mock物件、驗證模式
public static <T> T verify(T mock,  VerificationMode mode);
複製程式碼

Mockito.times(int wantedNumberOfInvocations) 可以得到一個VerificationMode物件,實際呼叫了 VerificationModeFactory.times(wantedNumberOfInvocations)獲取到一個Times物件:new Times(wantedNumberOfInvocations),Times實現了VerificationMode介面。

  • 引數一: mock 物件,必須的

  • 引數二: 驗證模式:times(x), atLeastOnce() 或者 never() 等;如果是times(1)則可忽略該引數

times方法呼叫棧如下:

org.mockito.Mockito#times(int wantedNumberOfInvocations)
	org.mockito.internal.verification.VerificationModeFactory#times(int wantedNumberOfInvocations)
		org.mockito.internal.verification.Times(int wantedNumberOfInvocations)
複製程式碼

when 方法

Mockito.when方法定義如下:

public static <T> OngoingStubbing<T> when(T methodCall)
複製程式碼

when方法需要傳遞一個 mock物件的方法 的呼叫,例如本例中我們傳遞了mock物件mockList的mockList.size()方法的呼叫。 when方法會留一份存根,在我們希望模擬在特定情況下返回特定的返回值時,回撥用它。 簡單的意圖就是: 當x方法呼叫的時候,就返回y

示例:

  • when(mock.someMethod()).thenReturn(10); : 呼叫方法時返回10

  • when(mock.someMethod(anyString())).thenReturn(10); : 靈活引數

  • when(mock.someMethod("some arg")).thenThrow(new RuntimeException()); : 呼叫方法時,丟擲一個異常

  • when(mock.someMethod("some arg")).thenThrow(new RuntimeException()).thenReturn("foo"); : 連續呼叫不同的行為

  • when(mock.someMethod("some arg")).thenReturn("one", "two"); : 連續的存根,第一次呼叫返回"one",第二次以及之後的呼叫返回"two"

  • when(mock.someMethod("some arg")).thenReturn("one").thenReturn("two"); : 和上面一條等同效果

  • when(mock.someMethod("some arg")).thenThrow(new RuntimeException(), new NullPointerException(); : 連續存根,拋異常

@Test
public void whenTest() {
    List mock = Mockito.mock(List.class);
    Mockito.when(mock.size()).thenReturn(-1);
    System.out.println("mock.size():" + mock.size());



    // 連續存根
    Mockito.when(mock.size()).thenReturn(1).thenReturn(2).thenReturn(3);
    for(int i=1; i <= 5; i++){
        System.out.println("=====連續存根方式1:=====: " + mock.size());
    }

    Mockito.when(mock.size()).thenReturn(1,2, 3);
    for(int i=1; i <= 5; i++){
        System.out.println("#####連續存根方式2:#####: " + mock.size());
    }

    // 模擬異常
    Mockito.when(mock.size()).thenThrow(new RuntimeException(), new NullPointerException());
    try{
        mock.size();
    }catch (Exception e){
        System.out.println(e);
    }
    try{
        mock.size();
    }catch (Exception e){
        System.out.println(e);
    }

}
複製程式碼

執行輸出:

mock.size():-1
=====連續存根方式1:=====: 1
=====連續存根方式1:=====: 2
=====連續存根方式1:=====: 3
=====連續存根方式1:=====: 3
=====連續存根方式1:=====: 3
#####連續存根方式2:#####: 1
#####連續存根方式2:#####: 2
#####連續存根方式2:#####: 3
#####連續存根方式2:#####: 3
#####連續存根方式2:#####: 3
java.lang.RuntimeException
java.lang.NullPointerException
複製程式碼

啟用 Mockito 的註解功能

@RunWith(MockitoJUnitRunner.class) 開啟註解功能

使用 @RunWith(MockitoJUnitRunner.class) 在類上開啟Mockito註解功能。

@RunWith(MockitoJUnitRunner.class)
public class MockitoAnnotationStartup {
}
複製程式碼

@Mock註解

通過 @Mock註解可以得到mock物件,等價於 Mockito.mock(class)。

    /**註解得到的mock物件*/
    @Mock
    List<String> mockList;
    
    等價於
    
    List<String> mock = Mockito.mock(List.class);
複製程式碼

示例如下:

package org.byron4j.cookbook.mocketio.basic;

import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;

import java.util.List;

import static org.junit.Assert.assertEquals;

public class MockitoAnnoTest extends MockitoAnnotationStartup{

    /**註解得到的mock物件*/
    @Mock
    List<String> mockList;

    @Test
    public void testRaw(){
        List<String> mock = Mockito.mock(List.class);
        mock.add("one");
        mock.add("one");
        Mockito.verify(mock, Mockito.times(2)).add("one");

        Mockito.when(mock.size()).thenReturn(100);
        assertEquals(100, mock.size());


    }

    @Test
    public void testAnno(){
        mockList.add("one");
        mockList.add("one");
        Mockito.verify(mockList, Mockito.times(2)).add("one");

        Mockito.when(mockList.size()).thenReturn(100);
        assertEquals(100, mockList.size());


    }
}

複製程式碼

@Spy 註解

和 @Mock 註解類似,還有 @Spy 註解。spy是密探間諜的意思,假冒的。

List<String> mock = Mockito.spy(List.class);
複製程式碼

使用註解

@Spy
List<String> spyList;
複製程式碼

@Captor 註解(引數捕獲器)

引數捕獲器 ArgumentCaptor 對應註解 @Captor。

原始方式建立一個引數捕獲器:

@Test
public void whenNotUseCaptorAnnotation_thenCorrect() {
    List mockList = Mockito.mock(List.class);
    ArgumentCaptor<String> arg = ArgumentCaptor.forClass(String.class);
 
    mockList.add("one");
    Mockito.verify(mockList).add(arg.capture());
 
    assertEquals("one", arg.getValue());
}
複製程式碼

使用@Captor註解建立一個引數捕獲器:

@Mock
List mockedList;
 
@Captor
ArgumentCaptor argCaptor;
 
@Test
public void whenUseCaptorAnnotation_thenTheSam() {
    mockedList.add("one");
    Mockito.verify(mockedList).add(argCaptor.capture());
 
    assertEquals("one", argCaptor.getValue());
}
複製程式碼
  • ArgumentCaptor<String> arg = ArgumentCaptor.forClass(String.class); : 建立一個引數捕獲器
  • Mockito.verify(mockedList).add(argCaptor.capture()); : 在一個驗證中使用捕獲器捕獲方法add的引數; capture 方法必須在一個驗證中。
  • argCaptor.getValue() : 獲取引數捕獲器捕獲到的引數

@InjectMocks 註解

@InjectMocks 註解可以將mock的屬性自動注入到測試物件中。

@Mock
Map<String, String> wordMap;

@InjectMocks
MyDictionary myDictionary = new MyDictionary();


@Test
    public void testInjectMocks(){
        Mockito.when(wordMap.get("aWord")).thenReturn("aMeaning");

        assertEquals("aMeaning", myDictionary.getMeaning("aWord"));

        System.out.println(myDictionary.getMeaning("aWord"));
    }

    class MyDictionary{
        Map<String, String> wordMap;

        public String getMeaning(String word){
            return wordMap.get(word);
        }
    }
複製程式碼
  • MyDictionary 類存在屬性 wordMap : Map<String, String> wordMap;
  • Mock 一個變數名為 wordMap :
@Mock
Map<String, String> wordMap;
複製程式碼
  • 使用 @InjectMocks 註解標記 :
@InjectMocks
MyDictionary myDictionary = new MyDictionary();
複製程式碼

則會將mock出來的物件wordMap注入到myDictionary例項的同名屬性中。

注意事項:

使用註解最小化重複編寫建立mock物件的程式碼

使用註解讓測試案例可讀性更好

使用 @InjectMocks 註解注入 @Spy 和 @Mock 物件

參考資料:

相關文章