不使用與使用 JUnit 5 的 Mockito

最後更新: 2026-08-17 10:18:23 +0800 +0800
原文以英文撰寫。您正在閱讀AI翻譯版本。

Mockito 用於為 Java 介面與類別建立測試替身。其核心 API 不依賴 JUnit:程式可以在一般的 main 方法中建立 mock、設定行為、呼叫程式碼並驗證互動。JUnit 整合增加的是測試探索與 mock 生命週期的自動管理,而不是基本的 mock 能力。

不使用 JUnit 的最小 Maven 專案 #

由於 Mockito 使用 test 範圍,請將示範放在 src/test/java 下:

text
mockito-hello/
├── pom.xml
└── src/test/java/com/example/MockitoHello.java
xml
<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>
java
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 執行:

sh
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 中:

java
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 方法,以及在設定期間評估方法並不安全的情況:

java
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。

驗證 #

驗證會在受測程式碼完成執行後檢查協作行為:

java
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 的必要順序:

java
InOrder order = inOrder(transaction, publisher);
order.verify(transaction).commit();
order.verify(publisher).publish(event);

此範例要求在 commit 後進行 publication;無關的互動不需要進行順序驗證。

引數匹配與擷取 #

Mockito 使用 equals 比較字面引數。Matcher 可放寬或描述這項比較:

java
when(catalog.find(eq("book"), anyInt())).thenReturn(results);
verify(catalog).find(eq("book"), intThat(limit -> limit > 0));

如果一個引數使用 matcher,同一個呼叫中的每個引數都必須使用 matcher。請使用 eq 包裝字面值,而不要混用 matcher 與字面值形式。

ArgumentCaptor 會檢查受測單元建立的值:

java
ArgumentCaptor<Email> captor = ArgumentCaptor.forClass(Email.class);

verify(sender).send(captor.capture());
assert "ada@example.com".equals(captor.getValue().recipient());

請在驗證期間進行擷取。在 stub 中使用 captor 會隱藏輸入條件,並可能在方法從未被呼叫時產生令人困惑的失敗。

Answer 與預設行為 #

Answer 會根據呼叫中繼資料計算結果:

java
when(cache.compute(anyString())).thenAnswer(invocation -> {
    String key = invocation.getArgument(0);
    return key.toUpperCase(Locale.ROOT);
});

只有當每個未進行 stub 的呼叫都需要特殊語意時,才將預設 answer 傳給 mock:

java
Service service = mock(Service.class, invocation -> {
    throw new AssertionError("unstubbed call: " + invocation.getMethod());
});

嚴格的預設行為可以暴露意外呼叫,但也會取代 Mockito 的標準預設值,因此需要刻意進行 stub。

Spy 與部分 Mock #

除非方法已進行 stub,否則 spy 會呼叫實際方法:

java
List<String> names = spy(new ArrayList<>());
doReturn(100).when(names).size();

建議使用 doReturn(...).when(spy)...,因為 when(spy.method()).thenReturn(...) 會在設定期間呼叫實際方法。建立 spy 時會複製所提供物件的狀態;它不是持續委派給原始參照的包裝器。

部分 mock 會使測試與實作細節耦合。如果可以變更正式環境的設計,較窄的介面或抽出的協作者可以提供更強的邊界。

註解生命週期 #

Mockito 註解可減少 fixture 宣告:

java
class ReportFixture {
    @Mock ReportRepository repository;
    @Spy Formatter formatter = new Formatter();
    @Captor ArgumentCaptor<Report> reportCaptor;
    @InjectMocks ReportService service;
}

沒有測試框架整合時,請明確初始化並關閉帶註解的 mock:

java
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 提供明確的生命週期與嚴格性:

java
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 是執行緒區域的具範圍資源,必須關閉:

java
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 一起加入:

xml
<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:

java
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:

java
@Test
void retriesAfterFailure(@Mock RemoteClient client) {
    // The parameter mock exists only for this invocation.
}

對於與 fixture 設定共用的協作者,優先使用欄位 mock;對於僅屬於單一測試的相依性,使用參數 mock。

參考資料 #

評論

在 GitHub 上閱讀或評論(發表評論需要登入)