
本文详解 Spring Boot 中使用 MockWebServer 测试 WebClient 时仍发起真实 HTTP 调用的根本原因——@LoadBalanced + 固定 baseUrl 导致 mock 配置被覆盖,并提供可复用的解耦方案。
本文详解 spring boot 中使用 mockwebserver 测试 webclient 时仍发起真实 http 调用的根本原因——`@loadbalanced` + 固定 `baseurl` 导致 mock 配置被覆盖,并提供可复用的解耦方案。
在基于 Spring Cloud 的微服务架构中,MockWebServer 是测试 HTTP 客户端(如 WebClient)的理想工具。但许多开发者会遇到一个典型陷阱:测试中明明启用了 MockWebServer,却依然向真实服务(如 http://user-service)发起请求,甚至触发 Eureka 服务发现逻辑,导致测试失败或超时。
根本原因在于 UserClient 构造过程中对 WebClient.Builder 的误用:
@Component
public class UserClient {
private static final String BASE_URL = "http://user-service"; // ⚠️ 硬编码,强制覆盖
private final WebClient webClient;
public UserClient(@Autowired WebClient.Builder webClient) {
// ❌ 错误:此处调用 baseUrl() 会丢弃测试中设置的 mock 地址
this.webClient = webClient.baseUrl(BASE_URL).build();
}
}
即使你在测试中通过 WebClient.builder().baseUrl(mockWebServer.url("/users").toString()) 创建了指向 mock server 的 builder,UserClient 构造器仍会强行将其重置为 http://user-service —— 这正是 Eureka 客户端解析后的真实地址,最终绕过 mock 直连生产服务。
✅ 正确做法是:将 baseUrl 的绑定职责从客户端类移出,交由配置层统一管理,并确保测试可完全接管 WebClient.Builder 实例。
✅ 推荐重构方案
1. 修改 UserClient:不干预 builder,仅构建 client
@Component
public class UserClient {
private final WebClient webClient;
// ✅ 不再调用 baseUrl() —— 让外部决定 base URL
public UserClient(@Autowired WebClient.Builder webClient) {
this.webClient = webClient.build(); // 仅 build,不设 baseUrl
}
public Mono<userresponse> createUser(UserRequest userRequest) {
return webClient.post()
.uri("/users") // 使用相对路径(关键!)
.bodyValue(userRequest)
.retrieve()
.bodyToMono(UserResponse.class);
}
}</userresponse>
2. 将 baseUrl 移至配置类(生产环境)
@Configuration
public class WebClientConfig {
@Bean
@LoadBalanced
public WebClient.Builder webClientBuilder() {
// ✅ 生产环境:启用负载均衡 + 设置服务名
return WebClient.builder()
.baseUrl("http://user-service"); // 仅在生产配置中指定
}
}
3. 测试中完全接管 builder(无 @LoadBalanced 干扰)
@SpringBootTest
@Import(TestWebClientConfig.class) // 避免加载生产配置
class UserClientTest {
@Autowired
private WebClient.Builder webClientBuilder; // 来自测试专用配置
private MockWebServer mockWebServer;
private UserClient userClient;
@BeforeEach
void setUp() throws IOException {
mockWebServer = new MockWebServer();
mockWebServer.start();
// ✅ 测试专用:builder 已预设 mock 地址,UserClient 不再覆盖
webClientBuilder = WebClient.builder()
.baseUrl(mockWebServer.url("/").toString()); // 注意:/ 结尾,保证 /users 解析正确
userClient = new UserClient(webClientBuilder);
}
@AfterEach
void tearDown() throws IOException {
mockWebServer.shutdown();
}
@Test
void shouldCreateANewUser() {
UserResponse mockResponse = new UserResponse(1L, "john_doe", "<a class="__cf_email__" data-cfemail="f09a9f989eb09588919d809c95de939f9d" href="/cdn-cgi/l/email-protection">[email protected]</a>");
mockWebServer.enqueue(
new MockResponse()
.setResponseCode(201)
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.setBody(new Gson().toJson(mockResponse))
);
UserRequest request = new UserRequest("john_doe", "password", "<a class="__cf_email__" data-cfemail="1a707572745a7f627b776a767f34797577" href="/cdn-cgi/l/email-protection">[email protected]</a>");
StepVerifier.create(userClient.createUser(request))
.expectNext(mockResponse)
.verifyComplete();
// ✅ 可选验证:确保只收到一次请求
assertThat(mockWebServer.getRequestCount()).isEqualTo(1);
}
}
// 测试专用配置:禁用 @LoadBalanced,避免 Ribbon/Eureka 干预
@Configuration
class TestWebClientConfig {
@Bean
WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}
⚠️ 关键注意事项
- URI 必须用相对路径:webClient.post().uri("/users") 才能被 baseUrl 正确拼接;若写成 .uri("http://.../users"),则完全忽略 baseUrl。
- @LoadBalanced 与 Mock 冲突:测试中务必避免 @LoadBalanced,它会注入 LoadBalancerExchangeFilterFunction,强制走服务发现逻辑。
- MockWebServer.url("/") 而非 "/users":baseUrl 应指向根路径,否则 /users 会被拼接为 http://mock:port//users(双斜杠),导致 404。
- 依赖隔离:使用 @Import(TestWebClientConfig.class) 或 @ContextConfiguration 显式加载测试配置,防止生产 @LoadBalanced Bean 被意外注入。
通过以上解耦设计,UserClient 不再耦合具体地址,WebClient.Builder 成为真正的可测试契约——既满足生产环境的服务发现需求,又保障单元测试的可控性与可靠性。











