# 六角鼠年鐵人賽 Week 15 - Spring Boot - Mockito 模擬測試框架
==大家好,我是 "為了拿到金角獎盃而努力著" 的文毅青年 - Kai==
## 名句 Albert Einstein
:::info
The value of a man resides in what he gives and not in what he is capable of receiving.
:::
## 主題
這次繼續介紹 Java 在做測試時候可以使用的框架--Mockito,有別於 JUnit,Mockito 主要是用來 **模擬實際程式運行的狀況**,是適合用來寫測 **Test Double** 的好用框架,這裡可能會有人疑問: 那 JUnit 就無法做到 Mockito 的效果嗎?,坦白說兩者應該分開看待,JUnit 適合用來作模組類程式的測試,而 Mockito 適合用來測試實際的 Service 類程式,而在使用上來說,兩者是完全可以同時使用的,就像豹頭常說的
![](https://i.imgur.com/g7l4IZR.jpg)
## 介紹 Mockito
Mockito 是一個開源的 Java 測試框架套件,由 MIT (麻省理工學院) Szczepan Faber,Brice Dutheil,Rafael Winterhalter,Tim van der Lippe 等人在 2010 年釋出第一版,旨在於打造一個可以讓開發者容易構寫可讀性高、提供完整的錯誤資訊的測試程式。
與 JUnit 相比,Mockito 最適合用來測試包含依賴注入環節的程式,而這也是 JUnit 目前不支援的部分。
因為在 JUnit 單元測試狀況中,大多會直接使用實際物件,然而這無法真正模擬實際狀況在處理依賴注入時是否會與預期一樣,因此會需要 Mockito 做此種狀況的測試。
且 Mockito 強項在於,可以單獨測試 Service 類程式,不需要去注意那些繁瑣的模組類程式,非常適合在作最後整合測試時使用,相比 JUnit 來說更能模擬實際程式執行的狀況。
> 上一篇提過的 TestNG 是有支援依賴注入的測試 (日後有機會可以再寫一篇介紹TestNG)
## 介紹 Test Double
在介紹 Mockito 之前,需要先了解何謂 Test Double?
先給各位介紹兩個接下來會不斷出現的詞:
- **SUT**:
全名可作 **System Under Test** 或 **Software Under Test**。
可簡單想作 SUT 就是一個開發者想拿來作測試的 Function 或 method,在 SpringBoot 專案中就是 Service 類的類別程式。
- **DOC**:
全名可作 **Depended-on Component**,扮演著 **Collaborator** 的角色。是 SUT 執行時會用到的模組類程式。在 SpringBoot 專案中就是各式 DAO 或 Module 類的程式。
作單元測試的時候,Mockito 使用來測試的對象通常是 SUT 本身,畢竟測試模組類程式是較沒有意義的事情,測試本身必須符合專案的業務邏輯,在此前提之下的測試才有意義。
但在 SUT 中必然會存在著使用到諸多的 DOC,這些 DOC 內的方法在測試上也必須加以排除,才能確保 SUT 的測試是沒有受到任何 DOC 影響的。
- **如何單獨驗證 SUT,而不需要真正使用到 DOC 成為了第一個問題**
如果你願意手刻一個新的 DOC,且僅為測試用,就會衍生出第二個問題,測試時候我們不需要真的去跑動那些太複雜的 DOC 邏輯,我們可以假定一些參數或者回應,然而如果每一個測試都需要手動更動剛剛建立的測試用 DOC 程式,又會拉慢整體測試的速度,更遑論一個開發者絕對沒有充足的時間可以進行測試的動作。
- **如何快速驗證 SUT 成為了第二個問題**
為此,Test Double 的方式解決了這兩個問題。
Test Double 主要作為測試替身,盡可能地在 SUT 中替代了那些 DOC 方法,並且能夠更好的被更動和調整,促使測試的速度可以更快更穩定。
Test Double 一共有五種
1. **Dummy Object**
- 不包含實作的物件 (Null也算),在測試中需要傳入但卻不會被用到的參數
> 產生完全沒有實作 DOC 的 Test Double
2. **Test Stub**
- 回傳固定值的實作
> 產生僅回傳特定回傳值的 DOC 實作的 Test Double,多用來作 State Verification (狀態驗證)
3. **Test Spy**
- 類似 Stub,但會記錄自身被呼叫的成員變數,以此確認 SUT 與它 (被 Test Spy 的 DOC 對象) 的互動是否正確
> 類似 Stub,但會記錄成員變數的 DOC 實作的 Test Double,多用來作 Behavior Verification (行為驗證)
4. **Mock Object**
- 由 Mock 程式庫動態建立,提供類似 Dummy、Stub、Spy的功能
- 開發人員看不到實作的程式碼,只能夠設定 Mock 要提供的回傳值、預期要呼叫使用的成員變數等
> 集合 Dummy、Stub 與 Spy 功能於一身的 Test Double,
5. **Fake Object**
提供接近原始物件但較簡單的實作
> 非常接近原始 DOC 方法的 Test Double,其差異在於會採用較簡單的方式處理 DOC 類程式的邏輯
> 資料來源於 [xUnit Test Patterns](https://www.amazon.com/xUnit-Test-Patterns-Refactoring-Code/dp/0131495054/ref=sr_1_1?ie=UTF8&qid=1411372534&sr=8-1&keywords=xunit+test+patterns)
可再參考 [MSDN Magazine] 的架構圖會更了解(https://docs.microsoft.com/zh-tw/archive/msdn-magazine/2007/september/unit-testing-exploring-the-continuum-of-test-doubles)
![](https://i.imgur.com/GnSsID1.png)
上述說了許多關於 Test Double 的東西,而這正是為何使用 Mockito 框架的原因,因為它很好的包覆了 Test Double 所需要使用到的程式類,並提供開發者很方便的 API 可以直接使用於 SUT 和 DOC 的測試處理中。
## Gradle 設定
```xml
repositories {
jcenter()
}
dependencies {
testCompile "org.mockito:mockito-core:3.+"
}
```
## 常用 Annotation 和 Function
### @Mock
說明:
宣告會放入 Service 中的模組類程式
撰寫位置:
如同 Service 中的宣告一樣位置即可
### @InjectMocks
說明:
宣告測試用的 Service 類程式,會將 @Mock 的物件放入
撰寫位置:
隨意,建議放在 @Mock 之後,對於使用上的語意會較清楚
### when()
說明:
測試 function 中,用作指定 SUT 的方法應該進行哪些動作?
- 回傳特定結果 (值 or exception)
- 忽略 SUT 中特定 DOC 的方法
- etc
撰寫位置:
在測試 function 中優先於實際執行 SUT 方法之前
### verify()
說明:
測試 function 中,用作驗證 DOC 方法或 DOC 變數值
- 執行特定次數
- 變數應為特定值
- etc
撰寫位置:
在測試 function 中於實際執行 SUT 方法之後、assert() 之前
### assert()
說明:
測試 function 中,用作檢核最終結果,也可看作是該支 TEST 程式最終要檢核的項目
撰寫位置:
在測試 function 中最後的部分
## 實作
Kai 這邊會用之前做過的 API 的 Project 繼續延伸作為 Mockito 的學習範例,其中藍色部分就是這一次會添加的程式部分。
![](https://i.imgur.com/F9o208I.png)
### IArea.java
```java=
package kai.com.mockito.modules;
public interface IArea {
public double count();
public void setParameter(double width,double length,double height);
public double getWidth();
public double getLength();
public double getHeight();
}
```
### SquareArea
```java=
package kai.com.mockito.modules;
public class SquareArea implements IArea{
private double width,length,height;
@Override
public double count() {
return width * length;
}
@Override
public void setParameter(double width, double length, double height) {
this.width = width;
this.length = length;
this.height = height;
}
@Override
public double getWidth() {
return this.width;
}
@Override
public double getLength() {
return this.length;
}
@Override
public double getHeight() {
return this.height;
}
}
```
### TriangleArea
```java=
package kai.com.mockito.modules;
public class TriangleArea implements IArea {
private double width,length,height;
@Override
public double count() {
return width * height / 2;
}
@Override
public void setParameter(double width, double length, double height) {
this.width = width;
this.length = length;
this.height = height;
}
@Override
public double getWidth() {
return this.width;
}
@Override
public double getLength() {
return this.length;
}
@Override
public double getHeight() {
return this.height;
}
}
```
### AreaService
```java=
package kai.com.mockito.service;
import kai.com.mockito.modules.IArea;
import org.springframework.beans.factory.annotation.Autowired;
public class AreaService {
@Autowired
IArea iArea;
public AreaService(IArea iArea){
this.iArea = iArea;
}
public double getArea(double width,double length,double height) throws Exception{
iArea.setParameter(width,length,height);
return iArea.count();
}
public double getWidth(){
return iArea.getWidth();
}
public double getLength(){
return iArea.getLength();
}
public double getHeight(){
return iArea.getHeight();
}
}
```
### AreaServiceTest
```java=
package mockitoTest;
import kai.com.mockito.modules.IArea;
import kai.com.mockito.service.AreaService;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
public class AreaServiceTest {
@Mock
IArea iArea;
@InjectMocks
AreaService areaService;
@BeforeAll
public static void BeforeAll(){
System.out.println("開始測試");
}
@Test
public void testGetArea_whenReturnOK() throws Exception {
// 建立預期回傳值
double answer = 10.0;
double answer_error = 11.0;
// 設立當 Mock 物件執行有回傳值的方法時,會回傳的值
// IArea.setParameter(double,double,double) 由於是無回傳值的方法,Mockito 會自動略過
when(iArea.count()).thenReturn(10.0);
when(iArea.getWidth()).thenReturn(1.0);
when(iArea.getLength()).thenReturn(2.0);
when(iArea.getHeight()).thenReturn(3.0);
// 實際執行並取得值
// anyDouble() 是 UnitTest 中會自動放入對應型態的參數使其通過的方法之一
double getArea = areaService.getArea(anyDouble(),anyDouble(),anyDouble());
double getWidth = areaService.getWidth();
double getLength = areaService.getLength();
double getHeight = areaService.getHeight();
System.out.println("得到的面積為: " + getArea);
System.out.println("得到的寬為: " + getWidth);
System.out.println("得到的長為: " + getLength);
System.out.println("得到的高為: " + getHeight);
// 執行 Verify 驗證
verify(iArea,times(1)).setParameter(anyDouble(),anyDouble(),anyDouble());
// 當檢查需要使用兩次function,但只有使用1次時,會出錯
//verify(iArea,times(2)).setParameter(anyDouble(),anyDouble(),anyDouble());
// 執行 Assert 檢核
assertEquals(getArea, answer);
// 當期望是 11 卻出現 10 就會出錯
//assertEquals(a,answer_error);
System.out.println("執行到本行,代表通過測試");
}
@Test
public void testGetArea_whenNullPointException() throws Exception {
// 建立預期回傳值
String answer = "Null Parameter";
String answer_error = "Null Parameter1";
// 設立當 Mock 物件執行有回傳值的方法時,會回傳的值
when(iArea.count()).thenThrow(new NullPointerException("Null Parameter"));
// 設立並執行出現 Exception 時,進行 Exception 參數的抓取
Exception exception = assertThrows(NullPointerException.class, () -> areaService.getArea(anyDouble(), anyDouble(), anyDouble()));
assertEquals(exception.getMessage(),answer);
// 當期望錯誤訊息是 Null Parameter1 卻得到 Null Parameter 會出錯
//assertEquals(exception.getMessage(),answer_error);
System.out.println("執行到本行,代表通過測試");
}
@AfterAll
public static void AfterAll(){
System.out.println("結束測試");
}
}
```
## 結語
:::danger
搭配著例子,簡單的也把 Mockito 介紹完了,大家可能會發現到範例中使用到 JUnit 的流程,這也是我提到的兩者其實是需要搭配使用才能得到最好的效益。
希望這兩篇可以幫助到想學單元測試、又不知從何學起的人。
至少能理解 JUnit 和 Mockito 在做什麼的話,就已經是一個長足的進步了!只要持之以恆地繼續學習下去即可。
兩篇內容中,Kai 都沒有講到太深入的地方,還請多加見諒,Kai 這邊文章多半是分享個入門,技術面的部份就須依賴官方文件了~
下一篇稍微插題介紹 Java 8 的 feature - Lambda 表示式
[六角鼠年鐵人賽 Week 16 - Spring Boot - 番外篇 Java 8 Lambda Tutorial](/bEfwwBNCRCa3gNXDsAO3aQ)
:::
首頁 [Kai 個人技術 Hackmd](/2G-RoB0QTrKzkftH2uLueA)
###### tags: `Spring Boot`,`w3HexSchool`