前言
本文著重於使用 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
以下列出可使用的方法:
測試結果:Pass。
本篇重點
-
認識 Mock 種類
-
Maven 配置使用 Mockito
-
各項 Annotation 使用
• @ExtendWith
• @Mock
• @InjectMocks
- Mockito 實際案例
• 如何實現 Dependency Inject
• Verify
• Return Value
• Argument Machers
Mockito Unit Test 的介紹及實作至此,日後有機會再接著分享 Mockito BDD Style。