1. 概述

如今,大多数服务都需要调用 REST 接口。Spring 提供了多种方式来构建 REST 客户端,其中 WebClient 是官方推荐的选择

本文将重点讲解如何对使用 WebClient 调用外部接口的服务进行单元测试,避免依赖真实网络请求,提升测试稳定性和效率。

2. Mock 的两种思路

测试中模拟 WebClient 行为,主要有两种策略:

  • ✅ **使用 Mockito**:直接 mock WebClient 及其链式调用
  • 使用 MockWebServer(来自 OkHttp):启动一个本地轻量 HTTP 服务,让真实的 WebClient 发起请求,实现“伪集成测试”

前者适合简单场景,后者更贴近真实调用流程,推荐在大多数场景下使用。

3. 使用 Mockito(不推荐)

Mockito 是 Java 最主流的 mock 框架,但在处理链式调用(fluent API)时非常痛苦。WebClient 正是典型的 fluent API,每一步返回不同对象,导致 mock 成本极高。

来看一个例子:

public class EmployeeService {

    private final WebClient webClient;

    public EmployeeService(String baseUrl) {
        this.webClient = WebClient.create(baseUrl);
    }

    public Mono<Employee> getEmployeeById(Integer employeeId) {
        return webClient
                .get()
                .uri("/employee/{id}", employeeId)
                .retrieve()
                .bodyToMono(Employee.class);
    }
}

如果用 Mockito 来测试这个方法,你得 mock 整个调用链:

@ExtendWith(MockitoExtension.class)
public class EmployeeServiceUnitTest {

    @Mock
    private WebClient webClientMock;

    @Mock
    private WebClient.RequestHeadersUriSpec requestHeadersUriSpecMock;

    @Mock
    private WebClient.RequestHeadersSpec requestHeadersSpecMock;

    @Mock
    private WebClient.ResponseSpec responseSpecMock;

    @InjectMocks
    private EmployeeService employeeService;

    @Test
    void givenEmployeeId_whenGetEmployeeById_thenReturnEmployee() {
        Integer employeeId = 100;
        Employee mockEmployee = new Employee(100, "Adam", "Sandler", 
          32, Role.LEAD_ENGINEER);

        when(webClientMock.get())
          .thenReturn(requestHeadersUriSpecMock);
        when(requestHeadersUriSpecMock.uri("/employee/{id}", employeeId))
          .thenReturn(requestHeadersSpecMock);
        when(requestHeadersSpecMock.retrieve())
          .thenReturn(responseSpecMock);
        when(responseSpecMock.bodyToMono(Employee.class))
          .thenReturn(Mono.just(mockEmployee));

        Mono<Employee> employeeMono = employeeService.getEmployeeById(employeeId);

        StepVerifier.create(employeeMono)
          .expectNextMatches(employee -> employee.getRole()
            .equals(Role.LEAD_ENGINEER))
          .verifyComplete();
    }
}

⚠️ 踩坑点:

  • 需要 mock 至少 4 个中间对象,代码冗长且难以维护
  • 测试强依赖实现细节,一旦 WebClient 调用链改动,测试就挂
  • 可读性差,后续接手的人会一脸懵 ❌

✅ 结论:虽然能跑通,但属于“自找麻烦”,不推荐用于生产级测试

4. 使用 MockWebServer(推荐)

MockWebServer 是 Square 提供的一个轻量级 HTTP 服务,专为测试设计。它能接收真实 HTTP 请求并返回预设响应。

优势

  • ✅ 让 WebClient 发起真实 HTTP 请求,但目标是本地 MockWebServer
  • ✅ 无需 mock 链式调用,测试更贴近真实场景
  • ✅ 可验证请求方法、路径、头、体等细节
  • ✅ 被 Spring 团队官方推荐 用于集成测试

4.1 添加依赖

需要引入 OkHttp 和 MockWebServer 的测试依赖:

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>5.0.0-alpha.12</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>mockwebserver</artifactId>
    <version>5.0.0-alpha.12</version>
    <scope>test</scope>
</dependency>

⚠️ 注意版本兼容性,建议使用与项目 OkHttp 版本一致的 mockwebserver

4.2 初始化 MockWebServer

使用 JUnit 5 的 @BeforeAll@AfterAll 管理生命周期:

public class EmployeeServiceIntegrationTest {

    public static MockWebServer mockBackEnd;

    @BeforeAll
    static void setUp() throws IOException {
        mockBackEnd = new MockWebServer();
        mockBackEnd.start();
    }

    @AfterAll
    static void tearDown() throws IOException {
        mockBackEnd.shutdown();
    }

    private EmployeeService employeeService;
    private ObjectMapper objectMapper = new ObjectMapper();

    @BeforeEach
    void initialize() {
        String baseUrl = String.format("http://localhost:%s", 
          mockBackEnd.getPort());
        employeeService = new EmployeeService(baseUrl);
    }
}

关键点:

  • 启动 MockWebServer 并获取动态端口
  • EmployeeService 的 base URL 指向 http://localhost:动态端口
  • 每次测试前重新初始化 service,避免状态污染

4.3 模拟响应(Stubbing)

通过 enqueue 方法预设响应,MockWebServer 会按 FIFO 返回:

@Test
void getEmployeeById() throws Exception {
    Employee mockEmployee = new Employee(100, "Adam", "Sandler", 
      32, Role.LEAD_ENGINEER);

    mockBackEnd.enqueue(new MockResponse()
      .setBody(objectMapper.writeValueAsString(mockEmployee))
      .addHeader("Content-Type", "application/json"));

    Mono<Employee> employeeMono = employeeService.getEmployeeById(100);

    StepVerifier.create(employeeMono)
      .expectNextMatches(employee -> employee.getRole()
        .equals(Role.LEAD_ENGINEER))
      .verifyComplete();
}

✅ 简单粗暴:

  • enqueue 一次,后续请求自动返回预设响应
  • 使用 StepVerifier 验证响应数据,完全模拟 Reactor 流程

4.4 验证请求细节

除了响应,我们还可以检查 WebClient 是否发出了正确的请求:

@Test
void getEmployeeById_shouldSendCorrectRequest() throws Exception {
    mockBackEnd.enqueue(new MockResponse()
      .setBody("{}")
      .addHeader("Content-Type", "application/json"));

    employeeService.getEmployeeById(100).block();

    RecordedRequest recordedRequest = mockBackEnd.takeRequest();

    assertEquals("GET", recordedRequest.getMethod());
    assertEquals("/employee/100", recordedRequest.getPath());
    assertTrue(recordedRequest.getHeader("Accept").contains("application/json"));
}

RecordedRequest 提供了丰富的断言能力:

  • getMethod():验证 HTTP 方法
  • getPath():验证请求路径(含路径参数)
  • getBody():验证请求体(POST/PUT)
  • getHeader():验证头信息

⚠️ 注意:takeRequest() 是阻塞调用,会等待下一个请求到达,适合同步或 block() 场景。

5. 总结

方案 优点 缺点 推荐度
Mockito 无需网络,速度快 难写难维护,脆性高 ⚠️ 仅简单场景
MockWebServer 真实 HTTP,可验证请求 启动开销略大 ✅ 强烈推荐

核心建议

  • ❌ 避免 mock WebClient 链式调用,自讨苦吃
  • ✅ 优先使用 MockWebServer,测试更可靠、更贴近生产
  • ✅ 结合 StepVerifier 验证响应流,覆盖 Reactor 场景

所有示例代码已上传至 GitHub:https://github.com/baeldung/spring-reactive-client


原始标题:Mocking a WebClient in Spring