Mockito 用於為 Java 介面與類別建立測試替身。其核心 API 不依賴 JUnit:程式可以在一般的 main 方法中建立 mock、設定行為、呼叫程式碼並驗證互動。JUnit 整合增加的是測試探索與 mock 生命週期的自動管理,而不是基本的 mock 能力。
不使用 JUnit 的最小 Maven 專案 #
由於 Mockito 使用 test 範圍,請將示範放在 src/test/java 下:
mockito-hello/
├── pom.xml
└── src/test/java/com/example/MockitoHello.java<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>mockito-hello</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<mockito.version>5.23.0</mockito.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>package com.example;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
interface Greeter {
String greet(String name);
}
public final class MockitoHello {
public static void main(String[] args) {
Greeter greeter = mock(Greeter.class);
when(greeter.greet("Ada")).thenReturn("Hello, Ada");
String actual = greeter.greet("Ada");
if (!"Hello, Ada".equals(actual)) {
throw new AssertionError("unexpected result: " + actual);
}
verify(greeter).greet("Ada");
}
}編譯測試類別,並使用 Maven Exec Plugin 執行:
mvn test-compile \
org.codehaus.mojo:exec-maven-plugin:3.6.1:java \
-Dexec.mainClass=com.example.MockitoHello \
-Dexec.classpathScope=test不會涉及測試引擎或 @Test 註解。程序會透過 AssertionError 或 Mockito 的驗證例外失敗。
Mock 與 Stub #
mock(Greeter.class) 會回傳一個記錄方法呼叫的物件。未進行 stub 的方法會回傳 Mockito 的預設值,例如 null、零、false 或受支援的空集合。
一般 stub 會將呼叫放在 when 中:
when(repository.findById(7L)).thenReturn(user);
when(repository.findById(-1L))
.thenThrow(new IllegalArgumentException("negative ID"));
when(sequence.next()).thenReturn(10, 20, 30);連續回傳值可支援具狀態的協定,但 fake 物件通常比長串的 stub 鏈更能清楚表達複雜狀態。
do... 系列用於 void 方法,以及在設定期間評估方法並不安全的情況:
doNothing().when(queue).acknowledge("message-1");
doThrow(new IOException("offline")).when(client).close();
doAnswer(invocation -> invocation.getArgument(0))
.when(normalizer).normalize(anyString());可用形式包括 doReturn、doThrow、doAnswer、doNothing 與 doCallRealMethod。
驗證 #
驗證會在受測程式碼完成執行後檢查協作行為:
verify(repository).save(order);
verify(repository, times(2)).findById(7L);
verify(repository, never()).delete(any());
verifyNoInteractions(auditLog);
verifyNoMoreInteractions(repository);verify(mock) 表示 times(1)。只驗證屬於單元契約的互動,因為檢查每一個實作呼叫會使測試在保持行為不變的重構後失敗。
InOrder 可驗證跨越一個或多個 mock 的必要順序:
InOrder order = inOrder(transaction, publisher);
order.verify(transaction).commit();
order.verify(publisher).publish(event);此範例要求在 commit 後進行 publication;無關的互動不需要進行順序驗證。
引數匹配與擷取 #
Mockito 使用 equals 比較字面引數。Matcher 可放寬或描述這項比較:
when(catalog.find(eq("book"), anyInt())).thenReturn(results);
verify(catalog).find(eq("book"), intThat(limit -> limit > 0));如果一個引數使用 matcher,同一個呼叫中的每個引數都必須使用 matcher。請使用 eq 包裝字面值,而不要混用 matcher 與字面值形式。
ArgumentCaptor 會檢查受測單元建立的值:
ArgumentCaptor<Email> captor = ArgumentCaptor.forClass(Email.class);
verify(sender).send(captor.capture());
assert "ada@example.com".equals(captor.getValue().recipient());請在驗證期間進行擷取。在 stub 中使用 captor 會隱藏輸入條件,並可能在方法從未被呼叫時產生令人困惑的失敗。
Answer 與預設行為 #
Answer 會根據呼叫中繼資料計算結果:
when(cache.compute(anyString())).thenAnswer(invocation -> {
String key = invocation.getArgument(0);
return key.toUpperCase(Locale.ROOT);
});只有當每個未進行 stub 的呼叫都需要特殊語意時,才將預設 answer 傳給 mock:
Service service = mock(Service.class, invocation -> {
throw new AssertionError("unstubbed call: " + invocation.getMethod());
});嚴格的預設行為可以暴露意外呼叫,但也會取代 Mockito 的標準預設值,因此需要刻意進行 stub。
Spy 與部分 Mock #
除非方法已進行 stub,否則 spy 會呼叫實際方法:
List<String> names = spy(new ArrayList<>());
doReturn(100).when(names).size();建議使用 doReturn(...).when(spy)...,因為 when(spy.method()).thenReturn(...) 會在設定期間呼叫實際方法。建立 spy 時會複製所提供物件的狀態;它不是持續委派給原始參照的包裝器。
部分 mock 會使測試與實作細節耦合。如果可以變更正式環境的設計,較窄的介面或抽出的協作者可以提供更強的邊界。
註解生命週期 #
Mockito 註解可減少 fixture 宣告:
class ReportFixture {
@Mock ReportRepository repository;
@Spy Formatter formatter = new Formatter();
@Captor ArgumentCaptor<Report> reportCaptor;
@InjectMocks ReportService service;
}沒有測試框架整合時,請明確初始化並關閉帶註解的 mock:
var fixture = new ReportFixture();
try (AutoCloseable mocks = MockitoAnnotations.openMocks(fixture)) {
// Exercise fixture.service.
}MockitoAnnotations.initMocks 會立即關閉 mock context,因此已被棄用。過早關閉會破壞依賴生命週期的 mock maker 與具範圍的 mock。@InjectMocks 會為 fixture 便利性執行建構函式、setter 或欄位注入;它不會取代相依性注入設定。
當自訂測試 harness 需要更多控制時,MockitoSession 提供明確的生命週期與嚴格性:
MockitoSession session = Mockito.mockitoSession()
.initMocks(fixture)
.strictness(Strictness.STRICT_STUBS)
.startMocking();
try {
// Exercise the fixture.
} finally {
session.finishMocking();
}嚴格 stub 會回報未使用的 stub 與引數不匹配,因而揭示不再符合實際執行行為的 fixture 設定。
Final、Static 與建構函式 Mock #
Mockito 5 預設使用 inline mock maker,因此 mockito-core 可以 mock final 類別、final 方法與列舉。較舊的 Mockito 版本可能需要之前的 mockito-inline artifact 或明確的 mock-maker 設定。
Static mock 與建構函式 mock 是執行緒區域的具範圍資源,必須關閉:
try (MockedStatic<ClockSource> clock = mockStatic(ClockSource.class)) {
clock.when(ClockSource::now).thenReturn(fixedInstant);
// Exercise code that calls ClockSource.now().
}
try (MockedConstruction<SocketClient> clients =
mockConstruction(SocketClient.class)) {
// Exercise code that constructs SocketClient.
}無法 mock private 方法與 native 方法。JVM 與 mock maker 的限制也適用於選定的 JDK 型別和方法。Static 或建構函式 mock 可以為舊有程式碼提供遷移接縫,但相依性注入可避免全域呼叫位置耦合。
現代 JVM 限制動態 agent 附加。使用 Mockito inline mock maker 的建置應遵循 Mockito 文件所述的 -javaagent 設定,而不要依賴執行時期的自我附加。
JUnit 5 整合 #
JUnit Jupiter 提供測試探索與生命週期回呼。請將 Jupiter 整合 artifact 與 JUnit 一起加入:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.13.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>mockito-junit-jupiter 會以傳遞相依性包含 mockito-core。MockitoExtension 會初始化註解欄位與參數,套用嚴格 stub 的生命週期處理,並在每個測試後關閉 mock:
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
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;
@ExtendWith(MockitoExtension.class)
class GreetingServiceTest {
@Mock
GreetingRepository repository;
@InjectMocks
GreetingService service;
@Test
void returnsTheStoredGreeting() {
when(repository.findFor("Ada")).thenReturn("Welcome, Ada");
assertEquals("Welcome, Ada", service.greet("Ada"));
verify(repository).findFor("Ada");
}
}方法參數也可以接收 mock:
@Test
void retriesAfterFailure(@Mock RemoteClient client) {
// The parameter mock exists only for this invocation.
}對於與 fixture 設定共用的協作者,優先使用欄位 mock;對於僅屬於單一測試的相依性,使用參數 mock。
評論
在 GitHub 上閱讀或評論(發表評論需要登入)