Mockito Unit Test

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

莊宗霖 2019/07/30 09:00:00
12525

Mockito Unit Test

前言

本文著重於使用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<T> type),並指定傳入參數必須要是Custom的類型,Mockito提供了非常多的參數驗證類型,依據各種需求選擇合適的方法即可。
 
以下列出可使用的方法:
 
測試結果:Pass。
 

本篇重點

1. 認識Mock種類
2. Maven配置使用Mockito
3. 各項Annotation使用
    • @ExtendWith
    • @Mock
    • @InjectMocks
4. Mockito實際案例
    • 如何實現Dependency Inject
    • Verify
    • Return Value
    • Argument Machers

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