Kai Chen
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    2
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    # 六角鼠年鐵人賽 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`

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully