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 參考
github mockito/mockito: Most popular Mocking framework for unit tests written in Java (github.com)