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