優雅的模擬測試框架Mockito介紹

前言

本文著重於使用 Mockito 測試框架協助撰寫單元測試(Unit Test),整合測試(Integration Test)也相當重要,關於單元測試及整合測試各自特性並不在本文討論的範圍內,本文期許藉由簡單的範例來認識 Mockito,提高開發者對於這一區塊的關注及討論程度。

Mock 種類

在正式開始進入主題之前,需要先對 Test Double 有些許概念,由於在 Mockito 中將大部分的 Test Doubles 都以 Mock 取代之,而 Test Doubles 並非只有 Mock 一種而已,以下則開始針對 Test Double 做個說明。

• Dummy

不包含實作的物件(包含 NULL),目的為在測試中傳入但是實際不會被使用到的物件,使之成功編譯。

• Stub

當你的 SUT 有依賴 DOC 時,用來替代真實 DOC 的物件,並且指定測試過程的回傳值。

• Mock

建立一個完全模擬的物件,與 Stub 不同的是,Stub 提供你的測試案例回傳值,Mock 則關注『驗證行為』。

• Spy

可以『記錄』並『驗證』與待測對象互動的行為,與 Mock 類似但是 Mockito 中 Spy 物件並不是 Mock 物件,Spy 所創建的是真實的物件。

• Fake

通常為自行實作並且僅用於替代 Production 環境中的輕量化物件,舉個例子:In-memory database。

Mockito ?

很廣泛被使用的測試框架,尤其能夠很容易的處理依賴注入的情境,對於使用 Spring Framework 的開發者來說,用來搭配撰寫 Unit Test 相對有幫助,當開發者遇到依賴注入情境時往往會直接使用『實際物件』來進行測試,而事實上這樣的操作是再進行 Integration Test,並非 Unit Test。另外 Mockito 也扮演著協助開發者能夠更容易地處理並且建構各式 Test Double 來進行 Unit Test。

演示範例

本例關注在 Mockito 的各種測試,在此則不特地引用 Spring 以及任何 ORM 相關框架。

• Project:專案結構。

• Maven Dependency: (在此範例中使用 Junit 5)。

        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>2.23.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-junit-jupiter</artifactId>
            <version>2.23.0</version>
            <scope>test</scope>
        </dependency>

• Service:呼叫 Repository 與資料庫進行交互取得資訊。

package service.jpa;

import model.Custom;
import repository.CustomRepository;
import service.ICustomJpaService;

import java.util.HashSet;
import java.util.Set;

public class CustomJpaService implements ICustomJpaService {
    private CustomRepository customRepository;

    public CustomJpaService(CustomRepository customRepository) {
        this.customRepository = customRepository;
    }

    @Override
    public Set<Custom> findAll() {
        Set<Custom> customs = new HashSet<>();
        customRepository.findAll().forEach(customs::add);
        return customs;
    }

    @Override
    public Custom findById(Long aLong) {
        return customRepository.findById(aLong).orElse(null);
    }

    @Override
    public Custom save(Custom object) {
        return customRepository.save(object);
    }

    @Override
    public void delete(Custom object) {
        customRepository.delete(object);
    }

    @Override
    public void deleteById(Long aLong) {
        customRepository.deleteById(aLong);
    }
}

• Repository:與資料庫溝通取得資料(這裡模擬 Spring Data Jpa 的行為,並無實際引用該框架)。

package repository;

import model.Custom;

public interface CustomRepository extends CrudRepository<Custom, Long> {
}

• Model:即 Entity。

package model;

public class Custom extends BaseEntity{
    private Long id;
    private String name;
    private String email;

    public Custom(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

撰寫 Unit Test By Mockito

針對 CustomJpaService 撰寫測試。

• Inject Mocks

此例中 Service 呼叫其依賴項目 Repository 取得或異動資料庫資訊,這裡關注待測物件 Service 呼叫方法執行時是否符合預期結果,因此我們需要對其依賴(Repository)進行 Mocks。

package service.jpa;

import model.Custom;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import repository.CustomRepository;

@ExtendWith(MockitoExtension.class)
class CustomJpaServiceTest {
    @Mock
    CustomRepository customRepository;

    @InjectMocks
    CustomJpaService customJpaService;

    @Test
    void delete() {
        customJpaService.delete(new Custom(1l, "Adele", "adele@gmail.com"));
    }
}

說明:

line 11 @ExtendWith: JUnit 5 提供拓展點的架構,利用此 Annotation 提供拓展性,讓第三方也能夠實現 JUnit Jupiter API,在此我們則使用 Mockito Extention。

line 13 @Mock: 建立 Mock 物件。

line 16 @InjectMocks: 注入 Mock 物件。

line 19 定義測試

line 23 執行待測物件

在 Debug 模式下進行檢查,可以看到 CustomRepository 成功地被 Mock 並且注入到 CustomJpaService。

測試結果:Pass。

• Verify Mocks

目的為驗證 Mock 物件被執行呼叫的情況是否符合預期結果。

    @Test
    void deleteById() {
        customJpaService.deleteById(1l);
        verify(customRepository, times(1)).deleteById(1l);
    }

說明:

line 4 驗證待測物件 customJpaService 呼叫 deleteById 時,Mock 物件 customRepository 被執行了幾次,times(1)表示被呼叫執行了一次,verify 預設行為是 times(1),在此為了演示所以沒有省略。

測試結果:Pass。

• Mocks 回傳值

目的為依據測試情境預先定義 Mock 回傳值。

    @Test
    void findAll() {
        Set<Custom> customSet = new HashSet<>();

        when(customRepository.findAll()).thenReturn(customSet);

        Set<Custom> returnCustomSet = customJpaService.findAll();

        assertThat(returnCustomSet).isNotNull();

        verify(customRepository).findAll();
    }

說明:

line 3 定義回傳物件。

line 5 指定 Mock 物件回傳值,當 customRepository 呼叫 findAll(),則回傳 line 37 定義的物件。

line 7 執行待測物件的呼叫。

line 9 驗證回傳值結果,這裡使用 assertj 進行斷言。

line 11 如同上一個 topic 所提及之 Verify Mocks 的驗證行為。

測試結果:Pass。

• Argument Machers

目的為驗證 Mock 物件的參數是否符合預期。

    @Test
    void testArgumentMatcherByDelete() {
        Custom custom = new Custom(1l, "Adele", "adele@gmail.com");

        customJpaService.delete(custom);

        verify(customRepository).delete(any(Custom.class));
    }

說明:

此例使用 any(Class type),並指定傳入參數必須要是 Custom 的類型,Mockito 提供了非常多的參數驗證類型,依據各種需求選擇合適的方法即可。

以下列出可使用的方法:

測試結果:Pass。

本篇重點

  1. 認識 Mock 種類

  2. Maven 配置使用 Mockito

  3. 各項 Annotation 使用

• @ExtendWith

• @Mock

• @InjectMocks

  1. Mockito 實際案例

• 如何實現 Dependency Inject

• Verify

• Return Value

• Argument Machers

Mockito Unit Test 的介紹及實作至此,日後有機會再接著分享 Mockito BDD Style。