Mockito
Unit Test
優雅的模擬測試框架Mockito介紹
2019/07/30 09:00:00
12
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。