不使用和使用 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 与打桩 #

mock(Greeter.class) 返回一个会记录方法调用的对象。未进行打桩的方法会返回 Mockito 的默认值,例如 null、零、false 或受支持的空集合。

普通打桩会将调用放在 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 对象通常能更清晰地表达复杂状态。

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 比较字面参数。匹配器可以放宽或描述这种比较:

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

如果一个参数使用匹配器,则同一调用中的每个参数都必须使用匹配器。使用 eq 包装字面值,不要混用匹配器和字面值形式。

ArgumentCaptor 用于检查被测单元创建的值:

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

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

应在验证期间进行捕获。在打桩中使用 captor 会隐藏输入条件,并且当方法从未被调用时,可能产生令人困惑的失败。

Answer 与默认行为 #

Answer 根据调用元数据计算结果:

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

只有在每个未打桩调用都需要特殊语义时,才向 mock 传入默认 answer:

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

严格的默认行为可以暴露意外调用,但也会替换 Mockito 的标准默认值,因此需要有意地进行打桩。

Spy 与部分 Mock #

除非方法已被打桩,否则 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 上下文,因此已被弃用。过早关闭会破坏依赖生命周期的 mock maker 和作用域 mock。@InjectMocks 为 fixture 提供便利,会执行构造函数、setter 或字段注入;它不会替代依赖注入配置。

当自定义测试框架需要更多控制时,MockitoSession 提供显式生命周期和严格性:

java
MockitoSession session = Mockito.mockitoSession()
        .initMocks(fixture)
        .strictness(Strictness.STRICT_STUBS)
        .startMocking();
try {
    // Exercise the fixture.
} finally {
    session.finishMocking();
}

严格 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 上阅读或评论(发表评论需要登录)