news 2026/9/9 23:47:50

Spring Boot RestClient单元测试实战:MockRestServiceServer与WireMock方案解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot RestClient单元测试实战:MockRestServiceServer与WireMock方案解析

1. 先别急着写测试,搞清楚 RestClient 到底是什么

1.1 版本号这件事:6.1 其实是 Spring Framework 的版本

看到标题里写着“Spring Boot 6.1”,估计不少人和我一开始一样愣了一下,因为 Spring Boot 的版本号目前主线是 3.x,而 6.1 是 Spring Framework 的版本号,Spring Boot 3.2 开始就是基于 Spring Framework 6.1 的。所以“Spring Boot 6.1”这个说法如果理解成“Spring Framework 6.1 + Spring Boot 3.2+”,在技术上是完全成立的,RestClient 正是 Spring Framework 6.1 新增的同步 HTTP 客户端。

这个细节不是抠字眼,它直接影响你查文档、搜资料、引依赖时的准确性。我在工作中遇到过好几个同事在 Maven 里找spring-boot-starter-restclient,找了半天发现没有这个 starter,因为 RestClient 不在单独的 starter 里,它跟着spring-web模块走。Spring Boot 3.2 的spring-boot-starter-webspring-boot-starter-webflux,甚至spring-boot-starter-web-services里都有它,但如果你用的是 WebFlux 的 starter,又想要同步的 RestClient 行为,那就得额外引spring-web的依赖,这里面的细微差别踩坑的人不少。

RestClient 的定位非常清晰:它要替代的是 RestTemplate。RestTemplate 从 Spring 5.0 开始就被官方标记为维护模式,不会再加新功能了,但团队里用它的老项目又特别多,因为大家习惯了它那种同步、直接、写起来不绕弯的风格。WebClient 虽然功能强大、异步非阻塞,但学习成本确实高,对于只想要“发一个 GET 请求拿个 JSON 回来”这种场景,WebClient 显得有点杀鸡用牛刀。RestClient 就是在这个背景下出现的:保留 RestTemplate 那种直观的同步编码体验,同时把 API 设计得更加链式、流畅,把 UriBuilder、ExchangeFilterFunction、消息转换器这些 WebClient 的优点也吸收了过来。

1.2 RestClient 与 RestTemplate、WebClient 怎么选

很多朋友问我一个问题:我现在要新写一个服务间调用,到底选哪个?我给团队定的参考思路是这样的:

维度RestTemplateWebClientRestClient
编码风格命令式,简单直接响应式链式,回调/流式命令式 + 链式,最接近现代写法
异步支持不支持,传统同步阻塞完美支持,非阻塞暂不支持异步,纯同步
学习成本中高
与 Spring 生态契合度低,进入维护模式高,官方主推
适合场景老项目维护高并发、网关、流式调用绝大多数服务间同步调用

这里多说一句异步的事。RestClient 本身是同步设计,但 Spring 官方在 6.1 里同时提供了RestClient的响应式兄弟RestClient的 WebClient 变体(即用 RestClient 的链式语法包着 WebClient 实现),不过日常用不到,咱不展开。我的判断标准很朴素:如果你的接口调用方本身不是高性能网关,也没有流式推送需求,就用 RestClient,它写出来的代码最好读、最好维护

1.3 为什么单元测试这个主题值得单拎出来写

说实话,我见过太多团队使用 RestTemplate 或 RestClient 时,测试只有两种状态:要么不写,要么写一个 mock 掉整个 Service 层的“假单元测试”,真正到了 HTTP 客户端这一层就裸奔了。等哪天第三方接口联调出问题,线上日志打出来的错误是ConnectTimeoutException还是ResourceAccessException,好多人分不清楚,更别提怎么在测试里模拟出这两种不同的超时场景。

RestClient 引入的新 API 刚出来一年左右,网上关于它的单元测试文章不少,但质量参差不齐,有些直接照搬 WebClient 的测试方案,有些用的还是旧版RestTemplate的过时 API,照着抄很容易掉坑里。这篇文章我会从方案选型、代码实现、常见报错三个层面,把 RestClient 单元测试这件事掰开揉碎讲清楚,项目代码也是我自己在 Spring Boot 3.2.4 基础上跑通的,可以放心抄。

2. 设计方案:单测 RestClient 的 3 种主流思路

2.1 思路一:MockRestServiceServer 轻量模拟

MockRestServiceServer是 Spring 官方专门为 RestTemplate 提供的测试工具,在 Spring Framework 6.1 之后,它同步支持对 RestClient 的 mock。这个方案的核心思路非常朴素:你家的服务要去请求第三方接口,测试里我不想真去请求,那我就给这个“网络请求”套一个代理,测试代码里预先定义好“如果收到某个 URL 的 GET 请求,就返回指定的 JSON 字符串”

它最大的优点是简单、快速、没有额外依赖,也不需要起一个真实的 socket 端口。缺点也明显:它 mock 的是底层 request factory,而不是真实网络栈,所以它测不到 DNS 解析、连接池、真实超时这些网络层面的问题。但作为单元测试,这完全够用了,因为你本来就不想在单测里真的发起外部调用。

实现方式上,用MockRestServiceServer.bindTo(RestClient.Builder)把测试服务绑定到 builder 上,然后在测试中server.expect(...)写期望的请求和返回。这里有一个从 RestTemplate 迁移过来的注意点:RestTemplate 时代调用MockRestServiceServer.createServer(restTemplate)就够了,但 RestClient 因为采取了 builder 模式,你需要把 server 绑定到 builder 再让 builder 构建出 RestClient,否则测试里 mock 不生效。

2.2 思路二:WireMock 模拟真实 HTTP 服务

如果你希望测试尽可能接近真实 HTTP 调用,比如要验证 header 里的认证信息、要模拟第三方服务返回 500/超时等异常场景,WireMock 是更好的选择。它本质上是在测试进程里启动一个真正的 HTTP 服务器,然后你的 RestClient 像访问真实服务一样访问这个本地端口。

WireMock 的优势是“真”,RestClient 从连接建立、发送请求、读取响应到解析 JSON 的完整链路都会执行一遍,如果底层配置有问题(比如连接池设置、超时逻辑),用 WireMock 能暴露出来。缺点是测试启动稍慢、需要管理端口和生命周期,如果项目里所有单测都用它,CI 时长会明显增加。

这两套方案怎么取舍?我的经验是:纯 Service 层的单测用 MockRestServiceServer,涉及 HTTP 协议细节的集成测试用 WireMock。还有一种更“重”的方案是用 Testcontainers 起一个容器化的 MockServer,但那种一般留给端到端测试,单测阶段引入容器反而增加了不稳定性,不建议一上来就这么豪横。

2.3 要不要拉起来 Spring 容器

关于这个问题,我见过两种极端:一种人写单测动不动@SpringBootTest,把所有 Bean 全部加载,测一个小 Client 要等 30 秒;另一种人连容器都不碰,纯手动 new 对象,但遇到 RestClient.Builder 注入、依赖别的配置类时又无从下手。

我的建议是分场景:如果只是测一个简单的 Client 方法、验证 URL/header/body,完全不需要启动 Spring 容器,手动RestClient.builder()构建,然后绑定 MockRestServiceServer 即可。但如果你配置了自定义的ClientHttpRequestInterceptorJackson消息转换器,或者依赖了配置文件的某些属性,那就用@SpringBootTest+@AutoConfigureMockRestServiceServer让 Spring 帮你把 builder 注入好,测试只关注业务断言,这更符合“集成测试”的定位。

2.4 先写测试还是先写功能代码

国内团队对这个问题的态度两极分化:要么严格执行 TDD,要么完全跳过测试。我自己的体会是,RestClient 这种外部调用代码,先写测试的价值比普通 CRUD 代码更大,因为它要对接的接口是你不可控的第三方,你必须在写业务代码前就确定好请求格式、响应结构、异常处理逻辑。你先写一个“我想让某个请求返回什么数据”的测试,相当于先把协议定下来,再写实现就不容易跑偏。

我常用的节奏是:先定义接口 DTO,再写一个空实现,然后写测试用例(正常返回、404、500、超时四种情况),最后补实现代码让测试变绿。这样做的好处是,你会在写测试的过程中发现很多设计上的问题,比如响应的 JSON 有嵌套字段你没考虑到、有些 header 是动态生成的、第三方接口分页结构和你预期不一致,这些问题提前暴露比联调时暴露成本低得多。

3. 实操过程:MockRestServiceServer 方案一步步落地

3.1 准备环境和依赖

先交代一下工程环境:JDK 17(Spring Boot 3.x 的最低要求)、Spring Boot 3.2.4、Spring Framework 6.1.5、Maven 3.9+。JDK 8 的老项目想用 RestClient 就不用想了,升 JDK 17 是硬前提。

Maven 依赖不需要额外的测试库,直接用 spring-boot-starter-test:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>

注意如果项目里没引spring-boot-starter-web,只引了spring-boot-starter-webflux,那么 RestClient 类在spring-web模块中已经有了,但有些 HTTP 客户端工厂(比如 JDK HttpURLConnection 的默认实现)在 WebFlux 环境下的测试行为会有差异,建议还是统一用 web starter,省得排查半天。

3.2 先写一个待测的 Client 类

假设我们正在做一个“上门烹饪预约服务”后端,其中一个功能是调用外部菜单服务获取厨师信息。这个场景取自真实业务,很能体现 RestClient 的使用方式。

package com.example.cooking.client; import com.example.cooking.dto.ChefDto; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; import java.util.List; @Component public class ChefServiceClient { private final RestClient restClient; public ChefServiceClient(@Qualifier("chefServiceRestClient") RestClient restClient) { this.restClient = restClient; } public ChefDto getChefById(Long id) { return restClient.get() .uri("/api/chefs/{id}", id) .accept(MediaType.APPLICATION_JSON) .retrieve() .body(ChefDto.class); } public List<ChefDto> listChefsByCity(String city) { return restClient.get() .uri(uriBuilder -> uriBuilder .path("/api/chefs") .queryParam("city", city) .build()) .accept(MediaType.APPLICATION_JSON) .retrieve() .body(new ParameterizedTypeReference<List<ChefDto>>() {}); } }

这里的@Qualifier("chefServiceRestClient")是为了区分不同的 RestClient Bean,当系统里同时有多个服务需要调用时,不要都在同一个 RestClient 上叠 baseUrl,给每个下游服务一个独立配置的 Client 是更清晰的设计。

3.3 配置 RestClient 的 Bean

@Configuration类中定义 RestClient Bean,并设置连接超时和读取超时:

package com.example.cooking.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.JdkClientHttpRequestFactory; import org.springframework.web.client.RestClient; import java.net.http.HttpClient; import java.time.Duration; @Configuration public class RestClientConfig { @Bean("chefServiceRestClient") public RestClient chefServiceRestClient( @Value("${chef.service.base-url}") String baseUrl) { HttpClient httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) .build(); JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(httpClient); requestFactory.setReadTimeout(Duration.ofSeconds(5)); return RestClient.builder() .baseUrl(baseUrl) .requestFactory(requestFactory) .defaultHeader("X-Request-Source", "cooking-platform") .build(); } }

这里我选了 JDK 自带的 HttpClient 作为底层实现,如果你想用 Apache HttpClient 5.x,需要额外引入依赖并配置连接池,但单测场景没有区别。注意JdkClientHttpRequestFactory在启动时就会创建一个HttpClient,如果项目中还有复杂的 HTTP/2 或连接池配置需求,建议换成HttpComponentsClientHttpRequestFactory

3.4 核心:写 MockRestServiceServer 测试类

终于写到最关键的部分了。由于 RestClient 使用了 builder 模式,MockRestServiceServer 绑定时有特殊要求,我一开始直接套用了 RestTemplate 时代的写法,结果发现 mock 根本拦截不到请求,后来仔细看了官方文档才明白问题出在哪。

正确的完整测试代码如下:

package com.example.cooking.client; import com.example.cooking.dto.ChefDto; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestClient; import java.util.Arrays; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; import static org.springframework.test.web.client.response.MockRestResponseCreators.*; class ChefServiceClientTest { private MockRestServiceServer mockServer; private ChefServiceClient client; @BeforeEach void setUp() { RestClient.Builder builder = RestClient.builder() .baseUrl("http://chef-service.internal"); mockServer = MockRestServiceServer.bindTo(builder).build(); client = new ChefServiceClient(builder.build()); } @Test void testGetChefById_whenThirdPartyReturnsData_shouldDeserializeOk() { // given String json = """ { "id": 1001, "name": "张师傅", "specialty": "川菜", "rating": 4.8 } """; mockServer.expect(requestTo("/api/chefs/1001")) .andExpect(method(HttpMethod.GET)) .andExpect(header("X-Request-Source", "cooking-platform")) .andRespond(withSuccess(json, MediaType.APPLICATION_JSON)); // when ChefDto result = client.getChefById(1001L); // then assertThat(result.getId()).isEqualTo(1001L); assertThat(result.getName()).isEqualTo("张师傅"); assertThat(result.getSpecialty()).isEqualTo("川菜"); mockServer.verify(); } @Test void testListChefsByCity_whenNoData_shouldReturnEmptyList() { // given mockServer.expect(requestToUriStartingWith("/api/chefs?city=上海")) .andExpect(method(HttpMethod.GET)) .andRespond(withSuccess("[]", MediaType.APPLICATION_JSON)); // when List<ChefDto> result = client.listChefsByCity("上海"); // then assertThat(result).isEmpty(); mockServer.verify(); } }

这个测试类有个很重要的细节:MockRestServiceServer.bindTo(builder).build()会在底层包装 builder,之后任何通过这个 builder 创建的 RestClient 请求都会被它拦截。verify()放在最后断言所有声明的期望请求都被匹配到,一个都不能多,一个也不能少。

3.5 异常场景怎么测

RestClient 默认继承了 RestTemplate 的异常处理方式:4xx 抛出HttpClientErrorException,5xx 抛出HttpServerErrorException,连接失败抛出ResourceAccessException。这些异常场景在测试中很容易模拟:

@Test void testGetChefById_whenServerReturns500_shouldThrowHttpServerErrorException() { mockServer.expect(requestTo("/api/chefs/2000")) .andExpect(method(HttpMethod.GET)) .andRespond(withServerError()); assertThatThrownBy(() -> client.getChefById(2000L)) .isInstanceOf(HttpServerErrorException.class) .hasMessageContaining("500"); } @Test void testGetChefById_whenThirdPartyTimeout_shouldThrowResourceAccessException() { // 通过 withException 模拟连接异常 mockServer.expect(requestTo("/api/chefs/3000")) .andExpect(method(HttpMethod.GET)) .andRespond(withException(new ConnectTimeoutException("connect timed out"))); assertThatThrownBy(() -> client.getChefById(3000L)) .isInstanceOf(ResourceAccessException.class) .hasMessageContaining("connect timed out"); }

这里我想强调一个容易混淆的概念:读超时(Read Timeout)和连接超时(Connect Timeout)是两个不同的异常,MockRestServiceServer 单测里能模拟的是“连接层”的异常,因为请求根本没发出去。如果你想验证读超时的处理逻辑,得用 WireMock 或 Testcontainers 起真实服务,然后让服务端故意睡眠超过读超时时间,这个在线上排查特别有用。

3.6 配合 @SpringBootTest 的写法

如果项目里配置比较多,想直接把测试环境拉起来,可以用下面的方式:

@SpringBootTest @AutoConfigureMockRestServiceServer class ChefServiceClientIntegrationTest { @Autowired private ChefServiceClient client; @Autowired private MockRestServiceServer mockServer; @Test void testGetChefById_withSpringContext() { mockServer.expect(requestTo("/api/chefs/1001")) .andRespond(withSuccess("{\"id\":1001,\"name\":\"李师傅\"}", MediaType.APPLICATION_JSON)); ChefDto result = client.getChefById(1001L); assertThat(result.getName()).isEqualTo("李师傅"); mockServer.verify(); } }

@AutoConfigureMockRestServiceServer时,Spring Boot 会自动替换容器里的 RestClient.Builder 为 mock 版,你的测试类不需要有任何绑定逻辑。但这个方案要求被注入的 RestClient 确实是项目中配置的同一个 builder 构建的,如果代码里new RestClient那神仙也拦不住。

4. WireMock 方案实战:连协议栈一起测

4.1 什么时候必须用 WireMock

年初我们团队对接了一个第三方“菜品识别服务”,对方文档没写好,响应里有时候返回 JSON,有时候返回纯文本,还有时候返回的 JSON 字段名大小写不一致。MockRestServiceServer 这种“模拟”方式在这种场景下就使不上劲了,因为不管真实服务返回什么,MockRestServiceServer 给你的 response 都是你定义的字符串,它测不到 RestClient 和消息转换器之间的协商过程。

这时候把请求打到真实服务上、然后让真实服务“听你指挥”才是检验正确性的唯一标准。WireMock 干的就是这个事。

4.2 引入 WireMock 并编写测试

Maven 添加依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.github.tomakehurst</groupId> <artifactId>wiremock-standalone</artifactId> <version>3.3.1</version> <scope>test</scope> </dependency>

测试代码:

package com.example.cooking.client; import com.example.cooking.dto.ChefDto; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; import org.springframework.web.client.RestClient; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static org.assertj.core.api.Assertions.assertThat; class ChefServiceClientWireMockTest { private WireMockServer wireMockServer; private ChefServiceClient client; @BeforeEach void setUp() { wireMockServer = new WireMockServer(8089); wireMockServer.start(); WireMock.configureFor("localhost", 8089); RestClient restClient = RestClient.builder() .baseUrl("http://localhost:8089") .build(); client = new ChefServiceClient(restClient); } @AfterEach void tearDown() { wireMockServer.stop(); } @Test void testGetChefById_withWireMock() { stubFor(get(urlEqualTo("/api/chefs/1001")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("{\"id\":1001,\"name\":\"王师傅\",\"specialty\":\"粤菜\",\"rating\":4.9}"))); ChefDto result = client.getChefById(1001L); assertThat(result.getName()).isEqualTo("王师傅"); verify(getRequestedFor(urlEqualTo("/api/chefs/1001"))); } }

WireMock 跑起来之后,RestClient 发请求就像打真实服务一样,DNS、端口、连接、HTTP 协议全过程都会走一遍,这能暴露一些 MockRestServiceServer 永远发现不了的问题。比如我们团队之前就遇到过:ChefDto里有个LocalDateTime字段,普通 JSON 序列化没问题,但第三方服务返回的是yyyy-MM-dd HH:mm:ss这种非 ISO 格式,在 MockRestServiceServer 里因为 response 直接走消息转换器解析,根本没报警;WireMock 下同样的 JSON 结果解析报错才暴露出问题,最后加了@JsonFormat注解才解决。

4.3 在单元测试里模拟延迟和超时

刚才提到的读超时场景,WireMock 很容易模拟:

@Test void testReadTimeout() { stubFor(get(urlEqualTo("/api/chefs/slow")) .willReturn(aResponse() .withFixedDelay(6000) // 人为延迟 6 秒 .withBody("{}"))); RestClient timeoutClient = RestClient.builder() .baseUrl("http://localhost:8089") .requestFactory(new JdkClientHttpRequestFactory( HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) .build())) .build(); // 需要单独设置读超时 JdkClientHttpRequestFactory factory = (JdkClientHttpRequestFactory) timeoutClient.getRequestFactory(); factory.setReadTimeout(Duration.ofSeconds(2)); assertThatThrownBy(() -> timeoutClient.get() .uri("/api/chefs/slow") .retrieve() .toBodilessEntity()) .isInstanceOf(ResourceAccessException.class) .hasMessageContaining("Read timed out"); }

有一点要提醒:JdkClientHttpRequestFactory的读超时必须在构建RestClient之后单独设置,如果一开始就在HttpClient构建时设置connectTimeout,那只影响连接阶段,不影响读取阶段。这个坑我遇到过不止一次,线上排查超时问题时要分清楚是连接超时还是读超时。

5. 常见问题与排查技巧实录

5.1 报了Unexpected request是什么原因,怎么定位

这是 MockRestServiceServer 最经典的报错,意思是有请求发出来了,但你测试里没有对这次请求做过期望声明,或者期望声明的 URL/method 对不上。最常见的原因是 RestClient 实际请求的 URL 和你预期的不一致。

排查技巧:把异常堆栈完整拉出来,里面会带着实际请求的 method 和 URI,对照你测试里expect的 matcher 逐项核对。另外特别提醒 URL 里的 query 参数,requestTo("/api/chefs?city=上海")这种写法对中文参数和你预期不一致时很容易误判,建议用queryParam逐项匹配而不是拼字符串。

5.2 RestClient 类找不到,或者版本不兼容

RestClient是 Spring 6.1 引入的类,如果你用的 Spring Boot 3.1 或更早,即使引了spring-web,类也是不存在的。这时候 IDE 会报ClassNotFoundException或编译错误,别纠结,直接升 Spring Boot 到 3.2+。还有一个很少人注意的点:如果项目里手动指定了 Spring Framework 版本,开发环境 Spring Boot 用的 6.1.x,但内部某个模块锁定了 6.0.x,那也会出现类找不到,Maven 依赖树里查一下spring-web的版本即可。

5.3 MockRestServiceServer mock 没生效,请求打到了真实服务

发生这种情况,九成是 RestClient 实例不是你绑定后那个 builder 创建的。仔细检查一下:MockRestServiceServer.bindTo(builder).build()之后,你的被测类是否用的是这个 builder.build()出来的 RestClient?如果被测类自己内部RestClient.create()或者 new 了一个,那 mock 自然拦不到。另外检查是否多个RestClientBean 有同名的质量问题,@Qualifier指定错了也会导致注入的不是同一个实例。

5.4 JSON 反序列化报错,但接口文档明明没错

使用 RestClient 时,./body()的解析依赖消息转换器自动协商 content-type。如果第三方返回的Content-Typeapplication/json;charset=UTF-8,没问题;但如果第三方服务不规范,返回了text/plain或者根本没有 Content-Type,RestClient 会认为响应不是 JSON,走默认的消息转换器,结果自然是反序列化失败或者拿到一个 String。

解决办法:在 Client 的请求代码里显式指定.accept(MediaType.APPLICATION_JSON),并且与第三方约定响应头必须带application/json。这个看着是小事,但排查起来最容易让人怀疑人生,我见过有人在这个问题上卡了两天。

5.5 测试用例之间互相影响

如果你在同一个测试类里写了多个测试方法,且都用@BeforeEach绑定 MockRestServiceServer,那么每个测试方法运行前都会新建一个 server 实例,用例之间互不干扰。但如果用了@SpringBootTest且测试类标注@Transactional,事务会贯穿整个测试,此时第三方服务调用不会被事务回滚影响,别指望 mock 会在事务结束后自动恢复,需要在@BeforeEach里手动重置 server。

用 WireMock 时,多个测试类并行跑可能发生端口冲突,建议每个测试类指定一个独立端口,不要全部用8089这种固定值,CI 上爆端口冲突是常事。

5.6 “SQL执行10秒自动关闭”这类配置会不会影响 RestClient 测试

很多人会把 RestClient 和数据库连接池的超时概念搞混,这里顺手捋一下。Spring Boot 中关于“SQL 执行 10 秒自动关闭”这类说法一般是指连接池的连接回收时长时间不归还或事务超时,它影响的范围是数据库连接,和 HTTP 客户端的超时完全是两条线。RestClient 的超时配置在ClientHttpRequestFactory层,与 JDBC 无关。所以你在测试 RestClient 时不需要也没必要依赖 Spring 的spring.datasource.*超时配置,连接超时和读超时都在 RestClient 自己的 RequestFactory 里设置。测试时为了不等待过久,通常把 connectTimeout 设 1~3 秒、readTimeout 设 2~5 秒即可,线上再根据接口实际响应时间放宽。

5.7 测试时遇到JdkClientHttpRequestFactory和 HttpURLConnection 混用

Spring Boot 3.2 默认的ClientHttpRequestFactoryJdkClientHttpRequestFactory,但在某些 Spring Boot 小版本上,如果 classpath 里有 Apache HttpClient 或 Jetty 的客户端,自动配置可能选择其他工厂。这会导致你在本地跑测试时没问题,一到 CI 就发现 RestClient 行为不一致,比如代理设置、连接池参数全变了。

建议在配置类里显式指定requestFactory,不要依赖容器自动选择,这样测试和生产环境行为才一致。如果你像我一样习惯在RestClientConfig里手动构建,就把JdkClientHttpRequestFactory或你选择的工厂实现固定写死。

6. 实操心得:我踩过几次坑之后的几点体会

写这篇文章之前,我把团队里所有用到 RestClient 的模块翻了一遍,发现很多测试代码还在用 RestTemplate 时代的MockRestServiceServer.createServer()老写法,这让我挺感慨的。框架更新换代速度很快,但很多人的测试知识还停留在上一代 API 上。

我觉得 RestClient 的单元测试,核心要把握住三点:方案选型看需求——纯单元测试用 MockRestServiceServer,需要验证 HTTP 细节用 WireMock;响应类型要匹配——单个对象用ChefDto.class,集合用ParameterizedTypeReference,不要混用;异常要全覆盖——正常返回、4xx、5xx、连接异常、读超时这五类场景都该有测试,第三方接口才敢放心调。

最后再分享一个小技巧:MockRestServiceServerexpect支持一次声明多个请求的期望,如果被测代码里循环调用了多次接口,可以用expect(requestTo(...)).andExpect(...).andRespond(...)连续写多个,最后统一verify()。但如果你对一个 URL 期望了两次,且两次返回的内容不一样,建议用andRespond的不同响应或MockRestResponseCreatorswithSuccess参数变体分别处理,不要让测试变成“最后一次赋值生效”,那样排错会非常痛苦。

对于刚接触 RestClient 的读者,我建议先把 3.4 节的基础测试跑通,再把 3.5 的异常场景补上,最后再去研究 WireMock。测试是写给未来的自己看的,一个能快速定位问题的测试类,比什么架构理念都实在。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 23:47:11

鸿蒙ArkWeb实战:请求拦截与前进后退导航全解析

前阵子接了个鸿蒙上的混合应用需求&#xff0c;页面主体是Web组件&#xff0c;要内嵌一个运营H5页面。需求单上写得很简单&#xff1a;顶部放返回和前进按钮&#xff0c;页面加载时拦截一个特定接口&#xff0c;往请求头里塞token&#xff0c;顺便在H5跳转时保持按钮状态正确。…

作者头像 李华
网站建设 2026/9/9 23:44:20

Python魔法方法详解:从对象模型到协议实践

你一定见过 __init__ 和 __str__ &#xff0c;也大概知道它们在类里是干什么用的。但当你看到 __getitem__ 、 __enter__ 、 __radd__ 这种名字时&#xff0c;是不是心里会咯噔一下&#xff1a;这又是哪路神仙&#xff1f;说实话&#xff0c;我在刚开始写Python的几年…

作者头像 李华