# 六角鼠年鐵人賽 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`