Mockito 學習筆記

發表於2024-02-11

1 關於 Mockito

1.1 簡介

Mockito 是一個 java mock 框架,主要用於程式碼的 mock 測試。

在真實的開發環境裡,Mockito 可以阻斷依賴鏈條,達到只測試某個方法內程式碼的目的。

舉個例子:
AService.someMethod1(...) 裡使用了 BService.someMethod(...) 和 AService.someMethod2(...) 這兩個方法。
當開發者只想要測試 AService.someMethod1(...) 的時候,
就可以透過 mock 框架模擬 BService.someMethod(...) 和 AService.someMethod2(...),
以此來達到只測試 AService.someMethod1(...) 的目的。

Mockito 除了服務端程式碼的 mock,還可以 mock 安卓程式碼。

本文只考慮 java 服務端開發部分,暫不涉及安卓開發。

1.2 Mockito 版本

Mockito 到目前為止一共 5 個大版本更新

  • Mockito 1.x -- 維護時間 2008 - 2014 年
  • Mockito 2.x -- 重構了 api,當前主流版本之一,維護時間 2015 - 2019 年
  • Mockito 3.x -- 在 2 的基礎上沒有大改 api,但是相容了 jdk8,維護時間 2019 - 2021 年
  • Mockito 4.x -- 刪除了一些過時的 api,維護時間 2021 - 2022 年
  • Mockito 5.x -- 需要 jdk11 及以上的專案使用,維護時間 2023 年以後

1.3 PowerMock

PowerMock 是一個 mock 門面框架,它可以補充 EasyMock 或者 Mockito 的功能。

Mockito 的原理是對物件進行代理,這種方式的最大問題是沒辦法對 private 和 static 方法進行處理。

PowerMock 則透過位元組碼編輯的方式更徹底的處理了物件,使得修改 private 和 static 方法變成了可能。

Mockito3 之後的版本里補全了相關功能,也就用不到 PowerMock 了。

PowerMock 在 2020 年以後就不再維護了。

PowerMock 分為 1.x 和 2.x 版本,一般使用 2.x。

如果專案中使用 Mockito 2.x 或者 3.x 的話,一般需要配置 PowerMock 使用。

注意:

1.4 私有方法的 mock

Mockito 截止 5.10.0 還沒有支援 private 方法的 mock,但是 PowerMock 是支援的。

Mockito 的團隊認為,private 方法是不需要 mock 的,因為那是需要 mock 的方法的一部分,而不是外部依賴。

當 private 方法需要 mock 的時候,說明程式碼的編碼是有問題的,建議重新進行編碼。

如果真的需要 mock private 方法,可以使用 PowerMock 2.x,一般搭配 Mockito 3.x 使用。

靜態方法的

1.5 相關包的 Maven 依賴

1.5.1 Mockito 5.x

Mockito 5.10.0 是截止到 2024 年 2 月的最新版本。

Mockito 5.x 需要配合 jdk11 及以上的 jdk 版本使用。

如果使用 Mockito 5.x,則最好使用 5.2.0 以後的版本,不需要單獨引入 mockito-inline 包了。

<!-- Mockito 核心包 -->
<!-- 當需要單獨使用 Mockito 的時候就使用這個包 -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>

<!-- Mockito 5.x + JUnit 5.x 的整合包 -->
<!-- 一般的 SpringBoot 專案就用這個包 -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>

1.5.2 Mockito 4.x

jdk8 下一般使用 Mockito 4.x,api 和 5.x 一致。

<!-- Mockito 核心包 -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>4.11.0</version>
    <scope>test</scope>
</dependency>

<!-- Mockito 擴充套件包,當需要 mock 靜態方法的時候需要用此包 -->
<!-- 在 Mockito 5.2.0 之後的版本里,inline 已經融合進了 core 裡,但是 4.x 裡還是需要單獨引用 -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <version>4.11.0</version>
    <scope>test</scope>
</dependency>

<!-- Mockito 4.x + JUnit 5.x 的整合包 -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>4.11.0</version>
    <scope>test</scope>
</dependency>

1.5.3 Mockito 3.x

Mockito 3.x 一般配合 PowerMock 2.x 和 JUnit 4.x 使用。

mockito-all 包已經不再更新了。

<!-- PowerMock 2.x + JUnit 4.x 的整合包 -->
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>2.0.9</version>
    <scope>test</scope>
</dependency>

<!-- PowerMock 2.x + Mockito 3.x 的整合包 -->
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito2</artifactId>
    <version>2.0.9</version>
    <scope>test</scope>
</dependency>

<!-- Mockito 3.x 的依賴整合包 -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
    <version>1.10.19</version>
    <scope>test</scope>
</dependency>

1.5.4 SpringBoot 依賴

spring-boot-starter-test 裡自帶了 mockito 相關依賴,可以剔除掉,然後在 pom 裡引入想要的版本。

在 SpringBoot 2.x 的版本里,一般引用的 Mockito 4.x,在真實的開發中需要額外引入 mockito-inline 來補充靜態方法的 mock 能力。

SpringBoot 2.2.0 之前的版本里使用 JUnit 4.x,之後的版本里使用 JUnit 5.x。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.mockito</groupId>
             <artifactId>mockito-junit-jupiter</artifactId>
        </exclusion>
    </exclusions>
</dependency>

2 最佳實踐

2.0 Demo

建立 Demo 介面類:

public interface MockClass {

    /**
     * 測試方法 1 - 無入參,有出參
     */
    String test1();

    /**
     * 測試方法 2 - 有入參,有出參
     */
    String test2(String re);

    /**
     * 測試方法 3 - 無入參,有出參
     */
    void test3();

    /**
     * 測試方法 4 - 無入參,無出參
     */
    void test4();
  
    /**
     * 測試靜態方法 - 需要一個入參
     */
    static String staticTestMethod(String printData) {
        return "print private test method return " + printData;
    }
}

介面的實現:

public class MockClassImpl implements MockClass {

    private final MockInnerClass innerClass;

    /**
     *     存入 inner class
     */
    public void setInnerClass(MockInnerClass innerClass) {
        this.innerClass = innerClass;
    }

    /**
     * 有參構造器,注入一個 inner 物件
     */
    public MockClassImpl(MockInnerClass innerClass) {
        this.innerClass = innerClass;
    }

    /**
     * 測試方法 1 - 無入參,有出參
     */
    public String test1() {
        return "test1";
    }

    /**
     * 測試方法 2 - 有入參,有出參
     */
    public String test2(String re) {
        return re;
    }

    /**
     * 測試方法 3 - 無入參,有出參
     */
    public void test3() {
        System.out.println("print test3");
    }


    public void test4() {
        System.out.println(MockClass.staticTestMethod("test4"));
    }

    public void testForInner() {
        System.out.println(innerClass.innerTest());
    }
}

inner 物件的實現:

public class MockInnerClass {

    public String innerTest() {
        return "innerTest";
    }
}

2.1 Mockito 5.x Simple Demo

2.1.1 pom

jdk 版本為 11,在 maven pom 裡引入 mockito-core 就可以使用。

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>

2.1.2 main

import org.mockito.MockedStatic;
import org.mockito.Mockito;

public class MockTest {

    public static void main(String[] args) {
        // 1 mock 建立一個 MockClassImpl 物件,這個物件是一個代理出來的物件
        // 以下兩種方式的效果是一樣的:

        // 1.1 使用 mock class 方式建立物件,當 class 有無參構造器的時候可以使用
        // MockClassImpl mockObj = Mockito.mock(MockClassImpl.class);

        // 1.2 使用 spy 方式建立物件
        // 先建立 inner 物件
        MockInnerClass mockOriginInnerObj = new MockInnerClass();
        // 對 inner 物件進行代理
        MockInnerClass mockInnerObj = Mockito.spy(mockOriginInnerObj);
        // 建立主物件
        MockClassImpl mockOriginObj = new MockClassImpl();
        // 存入 inner class
        mockOriginObj.setInnerClass(mockInnerObj);
         // 對主物件進行代理
        MockClassImpl mockObj = Mockito.spy(mockOriginObj);

        // 2 mock 出來的物件所有方法都是空實現的,直接呼叫不會有任何效果,但是也不會報錯
        // 使用 spy 建立出來的物件,不會影響原來的物件的功能
        System.out.println(mockObj.test1()); // 輸出 "null"
        System.out.println(mockOriginObj.test1()); // 輸出 "test1",說明原來的物件並沒有被影響

        // 3 具體 mock 一個方法,此時該物件內的這個方法有實現了
        Mockito.when(mockObj.test1()).thenReturn("mock-test-1");
        System.out.println(mockObj.test1()); // 輸出 "mock-test-1"

        // 4 mock 方法的入參可以選擇任意字串都輸出同一個結果,也可以選擇特定字串輸出某個結果
        // Mockito.any() -- 任意物件
        // Mockito.anyString() -- 任意字串
        // Mockito.anyInt() / Mockito.anyDouble() / Mockito.anyLong() -- 任意對應型別的數字
        // 其它相關方法不一一列舉
        Mockito.when(mockObj.test2(Mockito.anyString())).thenReturn("mock-test-2");
        System.out.println(mockObj.test2("someString")); // 輸出 "mock-test-2"
        // 這裡確定如果入參是 "someString" 的情況下,就會輸出一個不一樣的值
        // 如果入參是多個 object 的話,就會都需要用 equals 比較,都符合才會輸出這個值
        Mockito.when(mockObj.test2("someString")).thenReturn("mock-test-3");
        System.out.println(mockObj.test2("someString")); // 輸出 "mock-test-3"
        // 使用 thenThrow(...) 去 mock 物件的方法後,呼叫會報錯
        // Mockito.when(mockObj.test2("someString2")).thenThrow(new RuntimeException());
        // System.out.println(mockObj.test2("someString2")); // 會報錯

        // 5 mock 一個不需要出參的方法
        // test3() 本身會列印一行字串,但是在呼叫 doNothing() 之後,就不會有輸出了
        Mockito.doNothing().when(mockObj).test3();
        mockObj.test3();
        // 使用 doThrow(...) 去 mock 物件的方法後,呼叫會報錯
        // Mockito.doThrow(new RuntimeException()).when(mockObj).test3();
        // mockObj.test3();

        // 6 mock 一個 private 方法
        try (MockedStatic<MockClass> mockStatic = Mockito.mockStatic(MockClass.class)) {
            mockStatic.when(() -> MockClass.staticTestMethod(Mockito.anyString()))
                    .thenReturn("mock-test-4");
            System.out.println(MockClass.staticTestMethod("test")); // 輸出 "mock-test-4"
            mockObj.test4(); // 由於 test4() 方法內呼叫了這個靜態方法,所以此處輸出的也是 "mock-test-4"
            mockOriginObj.test4(); // 輸出 "test1",說明原來的物件並沒有被影響
        }

        mockObj.test4(); // 上述的靜態方法的代理已經被關掉了,所以此處輸出 "print private test method return mock-test-4"

        // 7 在特定入參下呼叫原方法
        Mockito.doCallRealMethod().when(mockObj).test3();
        Mockito.when(mockObj.test2(Mockito.anyString())).thenCallRealMethod();
        mockObj.test3(); // 輸出 "print test3"
        System.out.println(mockObj.test2("123")); // 輸出 "123"

        // 8 內部依賴
        Mockito.when(mockInnerObj.innerTest()).thenReturn("mock-test-5");
        mockObj.testForInner(); // 輸出 "mock-test-5"
    }
}

2.2 Mockito 3.x + PowerMock 2.x + JUnit 4.x

2.2.1 pom

jdk 版本為 8,在 maven pom 裡引入 Mockito、PowerMock、JUnit 等。

<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>2.0.9</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito2</artifactId>
    <version>2.0.9</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
    <version>1.10.19</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>compile</scope>
</dependency>

2.2.2 main

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

/**
 * @RunWith(PowerMockRunner.class) 使用 PowerMock 自帶的 Runner 跑程式碼,會自動 spy 代理一些類
 * @PrepareForTest({MockClass.class}) 所有需要代理的靜態物件
 */
@RunWith(PowerMockRunner.class)
@PrepareForTest({MockClass.class})
public class MockTest {

    /**
     * @InjectMocks 註解和使用 new 沒有本質區別
     */
    @InjectMocks
    private MockClassImpl mockObj = new MockClassImpl();

    /**
     * @Mock 註解會自動使用 spy 方法代理這個物件
     * 所以此處的 MockClass 物件已經是代理後的物件了
     */
    @Mock
    private MockInnerClass mockInnerObj;

    @Test
    public void testMainMethod() throws Exception {

        // 1 在不進行任何 mock 的情況下,是可以正常輸出原來的結果的
        System.out.println(mockObj.test1()); // 輸出 "test1"

        // 2 如果要 mock 整體方法,需要在程式碼中 spy 物件
        mockObj = PowerMockito.spy(mockObj);
        Mockito.when(mockObj.test1()).thenReturn("mock-test-1");
        System.out.println(mockObj.test1()); // 輸出 "mock-test-1"

        // 3 mock 靜態方法
        PowerMockito.mockStatic(MockClass.class);
        PowerMockito.when(MockClass.staticTestMethod(Mockito.anyString())).thenReturn("mock-test-4");
        System.out.println(MockClass.staticTestMethod("test")); // 輸出 "mock-test-4"
        mockObj.test4(); // 輸出 "mock-test-4"

        // 4 mock 內部依賴
        Mockito.when(mockInnerObj.innerTest()).thenReturn("mock-test-5");
        mockObj.testForInner(); // 輸出 "mock-test-5"

        // 5 mock 私有方法
        PowerMockito.when(mockObj, "privateMethod", Mockito.anyString()).thenReturn("mock-test-6");
        mockObj.testForPrivate("test"); // 輸出 "mock-test-6"
    }
}

2.3 Mockito 5.x + JUnit 5.x

2.3.1 pom

在 maven pom 裡引入 Mockito、JUnit 等。

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.8.2</version>
    <scope>compile</scope>
</dependency>

2.3.2 main

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

/**
 * @ExtendWith 在 JUnit 5 裡取代了 @RunWith 註解,用於做專案啟動的時候的擴充套件
 */
@ExtendWith(value = MockitoExtension.class)
public class MockTest {

    /**
     * @InjectMocks 註解和使用 new 沒有本質區別
     */
    @InjectMocks
    private MockClassImpl mockObj = new MockClassImpl();

    /**
     * @Mock 註解會自動使用 spy 方法代理這個物件
     * 所以此處的 MockClass 物件已經是代理後的物件了
     */
    @Mock
    private MockInnerClass mockInnerObj;

    @Test
    public void testMainMethod() throws Exception {

        // 1 在不進行任何 mock 的情況下,是可以正常輸出原來的結果的
        System.out.println(mockObj.test1()); // 輸出 "test1"

        // 2 如果要 mock 整體方法,需要在程式碼中 spy 物件
        mockObj = Mockito.spy(mockObj);
        Mockito.when(mockObj.test1()).thenReturn("mock-test-1");
        System.out.println(mockObj.test1()); // 輸出 "mock-test-1"

        // 3 mock 靜態方法
        try (MockedStatic<MockClass> mockStatic = Mockito.mockStatic(MockClass.class)) {
            mockStatic.when(() -> MockClass.staticTestMethod(Mockito.anyString()))
                    .thenReturn("mock-test-2");
            System.out.println(MockClass.staticTestMethod("test")); // 輸出 "mock-test-2"
            mockObj.test4(); // 由於 test4() 方法內呼叫了這個靜態方法,所以此處輸出的也是 "mock-test-2"
        }

        // 4 mock 內部依賴
        Mockito.when(mockInnerObj.innerTest()).thenReturn("mock-test-3");
        mockObj.testForInner(); // 輸出 "mock-test-3"
    }
}

3 參考

官方網站 Mockito framework site

github mockito/mockito: Most popular Mocking framework for unit tests written in Java (github.com)

文件 Mockito (Mockito 5.10.0 API) (javadoc.io)