1. 程式人生 > 其它 >模擬測試框架之Mockito使用及原理分析

模擬測試框架之Mockito使用及原理分析

前言

當我們進行單元測試時,可能某個依賴的服務還沒有開發完成(如RPC或HTTP呼叫),這種情況下我們就可以對依賴服務建立一個模擬物件,這樣我們就可以更加關注於當前的測試類,而不是依賴的服務類。Mockito是一個強大的模擬測試框架,可以讓我們很方便的建立模擬物件並進行行為驗證。

新增maven依賴

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

建立模擬物件

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.mockito.Mockito;

public class TestMock2 {

  public static void main(String[] args) {
    List<String> mockList = Mockito.mock(List.class);
    System.out.println(mockList.getClass());
    System.out.println(Arrays.toString(mockList.getClass().getInterfaces()));
    mockList = Mockito.mock(ArrayList.class);
    System.out.println(mockList.getClass());
    System.out.println(Arrays.toString(mockList.getClass().getInterfaces()));
  }

}

既可以對介面建立模擬物件,也可以對具體的實現類建立模擬物件,Mockito底層使用ByteBuddy庫來建立代理類,使用Objenesis庫來例項化物件。
ByteBuddy是一個程式碼生成和操作的類庫,類似於Cglib、javassist,底層也是ASM庫,官網
Objenesis是一個小的java庫,可以讓我們繞過構造器來例項化物件,Spring通過Cglib建立代理物件的過程中就使用到了Objenesis,
更多資訊可以檢視java中Objenesis庫簡單使用

建立部分模擬物件

import java.util.ArrayList;
import java.util.List;
import org.mockito.Mockito;

public class TestMock4 {

  public static void main(String[] args) {
    List<String> mockList = Mockito.mock(ArrayList.class);
    List<String> spyList = Mockito.spy(ArrayList.class);
    mockList.add("hello");
    spyList.add("hello");
    System.out.println(mockList.get(0));//null
    System.out.println(spyList.get(0));//hello
  }

}

spy()方法和mock()的區別在於

  • mock()方法在沒有找到對應的配置行為時,返回預設結果,如int型別返回0
  • spy()方法在沒有找到對應的配置行為時,委託給被代理物件處理,這裡就是ArrayList。如果被代理物件為介面,也是返回預設結果。
    具體可以檢視CallsRealMethods類實現。

配置方法行為

import java.util.List;
import org.mockito.Mockito;

public class TestMock2 {

  public static void main(String[] args) {
    List<String> mockList = Mockito.mock(List.class);
    Mockito.when(mockList.size()).thenReturn(1);
    System.out.println(mockList.size());
    Mockito.when(mockList.get(0)).thenReturn("hello");
    System.out.println(mockList.get(0));
    //丟擲異常
    Mockito.when(mockList.get(0)).thenThrow(new RuntimeException("error"));
    System.out.println(mockList.get(0));
  }

}

為List.size()行為配置一個結果1,下次呼叫此方法時直接返回此結果。
沒有配置行為時,使用預設結果,配置在DefaultMockitoConfiguration的ReturnsEmptyValues類中,如對int值返回0,對Iterable型別,返回一個空的ArrayList。
接下來分析一下原理:

  1. 當我們呼叫List.size()時,Mockito會攔截此方法,將方法呼叫(List.size())的詳細資訊儲存到模擬物件的上下文中
  2. 呼叫Mockito.when()方法時,會從上下文(具體為MockingProgressImpl)中獲取到最後一次方法呼叫(List.size())資訊
  3. 將thenReturn()的引數作為結果儲存起來,下次呼叫時直接獲取。

方法呼叫容器為InvocationContainerImpl,其中包含一系列StubbedInvocationMatcher物件,每一個物件都是一個方法呼叫和具體結果的封裝。

驗證方法呼叫次數

import java.util.List;
import org.mockito.Mockito;

public class TestMock3 {

  public static void main(String[] args) {
    List<String> mockList = Mockito.mock(List.class);
    mockList.size();
    mockList.size();
    Mockito.verify(mockList, Mockito.times(2)).size();
  }

}

如果方法沒有被呼叫2次,就會丟擲異常。

總結

Mockito基本原理就是對介面或具體類建立動態代理物件,在實際進行方法呼叫時,會查詢是否已經配置了結果,沒有就使用預設結果。

參考

Mockito官網
一文讓你快速上手 Mockito 單元測試框架
手把手教你 Mockito 的使用