一文讓你快速上手 Mockito 單元測試框架

前言spring

在計算機編程中,單元測試是一種軟件測試方法,經過該方法能夠測試源代碼的各個單元功能是否適合使用。爲代碼編寫單元測試有不少好處,包括能夠及早的發現代碼錯誤,促進更改,簡化集成,方便代碼重構以及許多其它功能。使用 Java 語言的朋友應該用過或者聽過 Junit 就是用來作單元測試的,那麼爲何咱們還須要 Mockito 測試框架呢?想象一下這樣的一個常見的場景,當前要測試的類依賴於其它一些類對象時,若是用 Junit 來進行單元測試的話,咱們就必須手動建立出這些依賴的對象,這實際上是個比較麻煩的工做,此時就可使用 Mockito 測試框架來模擬那些依賴的類,這些被模擬的對象在測試中充當真實對象的虛擬對象或克隆對象,並且 Mockito 同時也提供了方便的測試行爲驗證。這樣就可讓咱們更多地去關注當前測試類的邏輯,而不是它所依賴的對象。數據庫

1編程

生成 Mock 對象方式框架

要使用 Mockito,首先須要在咱們的項目中引入 Mockito 測試框架依賴,基於 Maven 構建的項目引入以下依賴便可:ide

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

若是是基於 Gradle 構建的項目,則引入以下依賴:工具

testCompile group: 'org.mockito', name: 'mockito-core', version: '3.3.3'

使用 Mockito 一般有兩種常見的方式來建立 Mock 對象。單元測試

1.1學習

使用 Mockito.mock(clazz) 方式測試

經過 Mockito 類的靜態方法 mock 來建立 Mock 對象,例如如下建立了一個 List 類型的 Mock 對象:this

List<String> mockList = Mockito.mock(ArrayList.class);

因爲 mock 方法是一個靜態方法,因此一般會寫成靜態導入方法的方式,即 ListmockList = mock(ArrayList.class)。

1.2

使用 @Mock 註解方式

第二種方式就是使用 @Mock 註解方式來建立 Mock 對象,使用該方式創須要注意的是要在運行測試方法前使用 MockitoAnnotations.initMocks(this) 或者單元測試類上加上 @ExtendWith(MockitoExtension.class) 註解,以下所示代碼建立了一個 List 類型的 Mock 對象(PS: @BeforeEach 是 Junit 5 的註解,功能相似於 Junit 4 的 @Before 註解。):

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
//@ExtendWith(MockitoExtension.class)
public class MockitoTest {

  @Mock
  private List<String> mockList;

  @BeforeEach
  public void beforeEach() {
    MockitoAnnotations.initMocks(this);
  }
}

2

驗證性測試

Mockito 測試框架中提供了 Mockito.verify 靜態方法讓咱們能夠方便的進行驗證性測試,好比方法調用驗證、方法調用次數驗證、方法調用順序驗證等,下面看看具體的代碼。

2.1

驗證方法單次調用

驗證方法單次調用的話直接 verify 方法後加上待驗證調用方法便可,如下代碼的功能就是驗證 mockList 對象的 size 方法被調用一次。

/**
 * @author mghio
 * @date: 2020-05-28
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {

  @Mock
  List<String> mockList;

  @Test
  void verify_SimpleInvocationOnMock() {
    mockList.size();
    verify(mockList).size();
  }
}

2.2

驗證方法調用指定次數

除了驗證單次調用,咱們有時候還須要驗證一些方法被調用屢次或者指定的次數,那麼此時就可使用 verify + times 方法來驗證方法調用指定次數,同時還能夠結合 atLeast + atMost 方法來提供調用次數範圍,同時還有 never 等方法驗證不被調用等。

/**
 * @author mghio
 * @date: 2020-05-28
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {

  @Mock
  List<String> mockList;

  @Test
  void verify_NumberOfInteractionsWithMock() {
    mockList.size();
    mockList.size();

    verify(mockList, times(2)).size();
    verify(mockList, atLeast(1)).size();
    verify(mockList, atMost(10)).size();
  }
}

2.3

驗證方法調用順序

同時還可使用 inOrder 方法來驗證方法的調用順序,下面示例驗證 mockList 對象的 size、add 和 clear 方法的調用順序。

/**
 * @author mghio
 * @date: 2020-05-28
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {

  @Mock
  List<String> mockList;

  @Test
  void verify_OrderedInvocationsOnMock() {
    mockList.size();
    mockList.add("add a parameter");
    mockList.clear();

    InOrder inOrder = inOrder(mockList);

    inOrder.verify(mockList).size();
    inOrder.verify(mockList).add("add a parameter");
    inOrder.verify(mockList).clear();
  }
}

以上只是列舉了一些簡單的驗證性測試,還有驗證測試方法調用超時以及更多的驗證測試能夠經過相關官方文檔探索學習。

3

驗證方法異常

異常測試咱們須要使用 Mockito 框架提供的一些調用行爲定義,Mockito 提供了 when(...).thenXXX(...) 來讓咱們定義方法調用行爲,如下代碼定義了當調用 mockMap 的 get 方法不管傳入任何參數都會拋出一個空指針 NullPointerException 異常,而後經過 Assertions.assertThrows 來驗證調用結果。

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@ExtendWith(MockitoExtension.class)
public class MockitoExceptionTest {

  @Mock
  public Map<String, Integer> mockMap;

  @Test
  public void whenConfigNonVoidReturnMethodToThrowEx_thenExIsThrown() {
    when(mockMap.get(anyString())).thenThrow(NullPointerException.class);

    assertThrows(NullPointerException.class, () -> mockMap.get("mghio"));
  }
}

同時 when(...).thenXXX(...) 不只能夠定義方法調用拋出異常,還能夠定義調用方法後的返回結果,好比 when(mockMap.get("mghio")).thenReturn(21); 定義了當咱們調用 mockMap 的 get 方法並傳入參數 mghio 時的返回結果是 21。這裏有一點須要注意,使用以上這種方式定義的 mock 對象測試實際並不會影響到對象的內部狀態,以下圖所示:

一文讓你快速上手 Mockito 單元測試框架

雖然咱們已經在 mockList 對象上調用了 add 方法,可是實際上 mockList 集合中並無加入 mghio,這時候若是須要對 mock 對象有影響,那麼須要使用 spy 方式來生成 mock 對象。

public class MockitoTest {

  private List<String> mockList = spy(ArrayList.class);

  @Test
  public void add_spyMockList_thenAffect() {
    mockList.add("mghio");

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

斷點後能夠發現當使用 spy 方法建立出來的 mock 對象調用 add 方法後,mghio 被成功的加入到 mockList 集合當中。

4


與 Spring 框架集成

Mockito 框架提供了 @MockBean 註解用來將 mock 對象注入到 Spring 容器中,該對象會替換容器中任何現有的相同類型的 bean,該註解在須要模擬特定 bean(例如外部服務)的測試場景中頗有用。若是使用的是 Spring Boot 2.0+ 而且當前容器中已有相同類型的 bean 的時候,須要設置 spring.main.allow-bean-definition-overriding 爲 true(默認爲 false)容許 bean 定義覆蓋。下面假設要測試經過用戶編碼查詢用戶的信息,有一個數據庫操做層的 UserRepository,也就是咱們等下要 mock 的對象,定義以下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@Repository
public interface UserRepository {

  User findUserById(Long id);

}

還有用戶操做的相關服務 UserService 類,其定義以下所示:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@Service
public class UserService {

  private UserRepository userRepository;

  public UserService(UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  public User findUserById(Long id) {
    return userRepository.findUserById(id);
  }
}

在測試類中使用 @MockBean 來標註 UserRepository 屬性表示這個類型的 bean 使用的是 mock 對象,使用 @Autowired 標註表示 UserService 屬性使用的是 Spring 容器中的對象,而後使用 @SpringBootTest 啓用 Spring 環境便可。

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@SpringBootTest
public class UserServiceUnitTest {

  @Autowired
  private UserService userService;

  @MockBean
  private UserRepository userRepository;

  @Test
  public void whenUserIdIsProvided_thenRetrievedNameIsCorrect() {
    User expectedUser = new User(9527L, "mghio", "18288888880");
    when(userRepository.findUserById(9527L)).thenReturn(expectedUser);
    User actualUser = userService.findUserById(9527L);
    assertEquals(expectedUser, actualUser);
  }
}

5

Mockito 框架的工做原理

經過以上介紹能夠發現, Mockito 很是容易使用而且能夠方便的驗證一些方法的行爲,相信你已經看出來了,使用的步驟是先建立一個須要 mock 的對象 Target ,該對象以下:

public class Target {

  public String foo(String name) {
    return String.format("Hello, %s", name);
  }

}

而後咱們直接使用 Mockito.mock 方法和 when(...).thenReturn(...) 來生成 mock 對象並指定方法調用時的行爲,代碼以下:

@Test
public void test_foo() {
  String expectedResult = "Mocked mghio";
  when(mockTarget.foo("mghio")).thenReturn(expectedResult);
  String actualResult = mockTarget.foo("mghio");
  assertEquals(expectedResult, actualResult);
}

仔細觀察以上 when(mockTarget.foo("mghio")).thenReturn(expectedResult) 這行代碼,首次使用我也以爲很奇怪,when 方法的入參居然是方法的返回值 mockTarget.foo("mghio"),以爲正確的代碼應該是這樣 when(mockTarget).foo("mghio"),可是這個寫法實際上沒法進行編譯。既然 Target.foo 方法的返回值是 String 類型,那是否是可使用以下方式呢?

Mockito.when("Hello, I am mghio").thenReturn("Mocked mghio");

結果是編譯經過,可是在運行時報錯:

一文讓你快速上手 Mockito 單元測試框架

從錯誤提示能夠看出,when 方法須要一個方法調用的參數,實際上它只須要 more 對象方法調用在 when 方法以前就行,咱們看看下面這個測試代碼:

@Test
public void test_mockitoWhenMethod() {
  String expectedResult = "Mocked mghio";
  mockTarget.foo("mghio");
  when("Hello, I am mghio").thenReturn(expectedResult);
  String actualResult = mockTarget.foo("mghio");
  assertEquals(expectedResult, actualResult);
}

以上代碼能夠正常測試經過,結果以下:

一文讓你快速上手 Mockito 單元測試框架

爲何這樣就能夠正常測試經過?是由於當咱們調用 mock 對象的 foo 方法時,Mockito 會攔截方法的調用而後將方法調用的詳細信息保存到 mock 對象的上下文中,當調用到 Mockito.when 方法時,其實是從該上下文中獲取最後一個註冊的方法調用,而後把 thenReturn 的參數做爲其返回值保存,而後當咱們的再次調用 mock 對象的該方法時,以前已經記錄的方法行爲將被再次回放,該方法觸發攔截器從新調用而且返回咱們在 thenReturn 方法指定的返回值。如下是 Mockito.when 方法的源碼:

一文讓你快速上手 Mockito 單元測試框架

該方法裏面直接使用了 MockitoCore.when 方法,繼續跟進,該方法源碼以下:

一文讓你快速上手 Mockito 單元測試框架

仔細觀察能夠發現,在源碼中並無用到參數 methodCall,而是從 MockingProgress 實例中獲取 OngoingStubbing 對象,這個 OngoingStubbing 對象就是前文所提到的上下文對象。我的感受是 Mockito 爲了提供簡潔易用的 API 而後才製造了 when 方法調用的這種「幻象」,簡而言之,Mockito 框架經過方法攔截在上下文中存儲和檢索方法調用詳細信息來工做的。

6


如何實現一個微型的 Mock 框架***

知道了 Mockito 的運行原理以後,接下來看看要如何本身去實現一個相似功能的 mock 框架出來,看到方法攔截這裏我相信你已經知道了,其實這就是 AOP 啊,可是經過閱讀其源碼發現 Mockito 其實並無使用咱們熟悉的 Spring AOP 或者 AspectJ 作的方法攔截,而是經過運行時加強庫 Byte Buddy 和反射工具庫 Objenesis 生成和初始化 mock 對象的。如今,經過以上分析和源碼閱讀能夠定義出一個簡單版本的 mock 框架了,將自定義的 mock 框架命名爲 imock。這裏有一點須要注意的是,Mockito 有一個好處是,它不須要進行初始化,能夠直接經過其提供的靜態方法來當即使用它。在這裏咱們也使用相同名稱的靜態方法,經過 Mockito 源碼:
一文讓你快速上手 Mockito 單元測試框架
很容易看出 Mockito 類最終都是委託給 MockitoCore 去實現的功能,而其只提供了一些面向使用者易用的靜態方法,在這裏咱們也定義一個這樣的代理對象 IMockCore,這個類中須要一個建立 mock 對象的方法 mock 和一個給方法設定返回值的 thenReturn 方法,同時該類中持有一個方法調用詳情 InvocationDetail 集合列表,這個類是用來記錄方法調用詳細信息的,而後 when 方法僅返回列表中的最後一個 InvocationDetail,這裏列表能夠直接使用 Java 中經常使用的 ArrayList 便可,這裏的 ArrayList 集合列表就實現了 Mockito 中的 OngoingStubbing 的功能。根據方法的三要素方法名、方法參數和方法返回值很容易就能夠寫出 InvocationDetail 類的代碼,爲了對方法在不一樣類有同名的狀況區分,還須要加上類全稱字段和重寫該類的 equals 和 hashCode 方法(判斷是否在調用方法集合列表時須要根據該方法判斷),代碼以下所示:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class InvocationDetail<T> {

  private String attachedClassName;

  private String methodName;

  private Object[] arguments;

  private T result;

  public InvocationDetail(String attachedClassName, String methodName, Object[] arguments) {
    this.attachedClassName = attachedClassName;
    this.methodName = methodName;
    this.arguments = arguments;
  }

  public void thenReturn(T t) {
    this.result = t;
  }

  public T getResult() {
    return result;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    InvocationDetail<?> behaviour = (InvocationDetail<?>) o;
    return Objects.equals(attachedClassName, behaviour.attachedClassName) &&
        Objects.equals(methodName, behaviour.methodName) &&
        Arrays.equals(arguments, behaviour.arguments);
  }

  @Override
  public int hashCode() {
    int result = Objects.hash(attachedClassName, methodName);
    result = 31 * result + Arrays.hashCode(arguments);
    return result;
  }
}

接下來就是如何去建立咱們的 mock 對象了,在這裏咱們也使用 Byte Buddy 和 Objenesis 庫來建立 mock 對象,IMockCreator 接口定義以下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public interface IMockCreator {

  <T> T createMock(Class<T> mockTargetClass, List<InvocationDetail> behaviorList);

}

實現類 ByteBuddyIMockCreator 使用 Byte Buddy 庫在運行時動態生成 mock 類對象代碼而後使用 Objenesis 去實例化該對象。代碼以下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class ByteBuddyIMockCreator implements IMockCreator {

  private final ObjenesisStd objenesisStd = new ObjenesisStd();

  @Override
  public <T> T createMock(Class<T> mockTargetClass, List<InvocationDetail> behaviorList) {
    ByteBuddy byteBuddy = new ByteBuddy();

    Class<? extends T> classWithInterceptor = byteBuddy.subclass(mockTargetClass)
        .method(ElementMatchers.any())
        .intercept(MethodDelegation.to(InterceptorDelegate.class))
        .defineField("interceptor", IMockInterceptor.class, Modifier.PRIVATE)
        .implement(IMockIntercepable.class)
        .intercept(FieldAccessor.ofBeanProperty())
        .make()
        .load(getClass().getClassLoader(), Default.WRAPPER).getLoaded();

    T mockTargetInstance = objenesisStd.newInstance(classWithInterceptor);
    ((IMockIntercepable) mockTargetInstance).setInterceptor(new IMockInterceptor(behaviorList));

    return mockTargetInstance;
  }
}

基於以上分析咱們能夠很容易寫出建立 mock 對象的 IMockCore 類的代碼以下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class IMockCore {

  private final List<InvocationDetail> invocationDetailList = new ArrayList<>(8);

  private final IMockCreator mockCreator = new ByteBuddyIMockCreator();

  public <T> T mock(Class<T> mockTargetClass) {
    T result = mockCreator.createMock(mockTargetClass, invocationDetailList);
    return result;
  }

  @SuppressWarnings("unchecked")
  public <T> InvocationDetail<T> when(T methodCall) {
    int currentSize = invocationDetailList.size();
    return (InvocationDetail<T>) invocationDetailList.get(currentSize - 1);
  }
}

提供給使用者的類 IMock 只是對 IMockCore 進行的簡單調用而已,代碼以下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class IMock {

  private static final IMockCore IMOCK_CORE = new IMockCore();

  public static <T> T mock(Class<T> clazz) {
    return IMOCK_CORE.mock(clazz);
  }

  public static <T> InvocationDetail when(T methodCall) {
    return IMOCK_CORE.when(methodCall);
  }
}

經過以上步驟,咱們就已經實現了一個微型的 mock 框架了,下面來個實際例子測試一下,首先建立一個 Target 對象:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class Target {

  public String foo(String name) {
    return String.format("Hello, %s", name);
  }

}

而後編寫其對應的測試類 IMockTest 類以下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class IMockTest {

  @Test
  public void test_foo_method() {
    String exceptedResult = "Mocked mghio";
    Target mockTarget = IMock.mock(Target.class);

    IMock.when(mockTarget.foo("mghio")).thenReturn(exceptedResult);

    String actualResult = mockTarget.foo("mghio");

    assertEquals(exceptedResult, actualResult);
  }

}

以上測試的能夠正常運行,達到了和 Mockito 測試框架同樣的效果,運行結果以下:

一文讓你快速上手 Mockito 單元測試框架
上面只是列出了一些關鍵類的源碼,自定義 IMock 框架的全部代碼已上傳至 Github 倉庫 imock,感興趣的朋友能夠去看看。


7

總結

本文只是介紹了 Mockito 的一些使用方法,這只是該框架提供的最基礎功能,更多高級的用法能夠去官網閱讀相關的文檔,而後介紹了框架中 when(...).thenReturn(...) 定義行爲方法的實現方式並按照其源碼思路實現了一個相同功能的簡易版的 imock。雖然進行單元測試有不少優勢,可是也不可盲目的進行單元測試,在大部分狀況下咱們只要作好對項目中邏輯比較複雜、不容易理解的核心業務模塊以及項目中公共依賴的模塊的單元測試就能夠了。

相關文章
相關標籤/搜索