如何编写JUnit测试验证Micronaut API客户端读超时/异常重试
验证Micronaut HttpClient @Retryable重试逻辑的最优测试方案
问题背景
使用Micronaut HttpClient调用外部API,已在ApiClient接口的getItems方法上通过@Retryable注解配置了最多3次重试(含1秒延迟)。通过Wiremock设置响应延迟超过读超时阈值,需要编写JUnit测试验证触发读超时/异常时是否确实重试3次。目前已知一种方案是将ApiClient调用封装到另一个带@Retryable的方法中再验证调用次数,寻求更优测试方法。
相关业务代码
ApiClient接口:
@Validated @Client("${api.url}") public interface ApiClient { @Get("/v1/itemdetails{?ids*}") @Retryable(attempts = "${attempts:3}", delay = "${delay:1s}") HttpResponse<ItemDto> getItems( @NotEmpty List<String> ids, @Header("authorization") @NotBlank String authorization ); }
调用逻辑:
private void saveItems(List<String> ids) { try { String token = getToken(); HttpResponse<ItemDto> itemResponse = ApiClient.getItems(ids, token); handleItemResponse(ids, itemResponse); } catch (Exception e) { log.error("Error saving items", e); } }
已知封装方案示例:
@Retryable(attempts = "${attempts:3}", delay = "${delay:1s}") private HttpResponse<ItemDto> getItemsFromApi(List<String> ids){ logs.debug("calling Api..."); return ApiClient.getItems(ids,token); }
更优测试方案:直接验证Wiremock请求次数 + 利用Micronaut测试特性
无需修改业务代码,直接针对原ApiClient的重试逻辑做测试,核心是通过Wiremock记录请求次数,结合Micronaut测试上下文触发调用:
1. 配置Wiremock模拟超时场景
在测试类中配置Wiremock,让目标接口返回延迟响应,超过HttpClient读超时时间以触发超时异常:
@MicronautTest @AutoConfigureWireMock(port = 0) // 使用随机端口避免冲突 class ApiClientRetryTest { @Inject ApiClient apiClient; @Inject WireMockServer wireMockServer; @BeforeEach void setup() { // 配置Wiremock,响应延迟600ms(假设HttpClient读超时为500ms) wireMockServer.stubFor(get(urlPathEqualTo("/v1/itemdetails")) .withQueryParam("ids", containing("test-id")) .willReturn(aResponse() .withFixedDelay(600) .withStatus(200))); } // 自定义HttpClient配置,设置读超时 @TestConfiguration static class Config { @Bean HttpClientConfiguration httpClientConfiguration() { HttpClientConfiguration config = new HttpClientConfiguration(); config.setReadTimeout(Duration.ofMillis(500)); return config; } } }
2. 验证重试次数
调用ApiClient方法后,通过Wiremock的请求计数验证是否触发3次重试:
@Test void shouldRetry3TimesOnReadTimeout() { try { apiClient.getItems(List.of("test-id"), "dummy-token"); } catch (Exception e) { // 预期会抛出超时异常,捕获不影响测试结果 } // 验证Wiremock收到的请求次数等于配置的重试次数(3次) verify(3, getRequestedFor(urlPathEqualTo("/v1/itemdetails")) .withQueryParam("ids", containing("test-id"))); }
3. 可选:验证重试延迟
若需要验证重试延迟是否符合配置(1秒),可结合CountDownLatch和时间断言:
@Test void shouldRespectRetryDelay() throws InterruptedException { CountDownLatch latch = new CountDownLatch(3); // 修改Wiremock stub,每次请求触发latch计数 wireMockServer.stubFor(get(urlPathEqualTo("/v1/itemdetails")) .withQueryParam("ids", containing("test-id")) .willReturn(aResponse() .withFixedDelay(600) .withStatus(200)) .withPostServeAction("latch", aResponse().withTransformerParameter("latch", latch))); long startTime = System.currentTimeMillis(); try { apiClient.getItems(List.of("test-id"), "dummy-token"); } catch (Exception e) { // 忽略异常 } // 等待所有请求完成 latch.await(10, TimeUnit.SECONDS); long totalTime = System.currentTimeMillis() - startTime; // 3次请求含2次重试延迟(每次1秒),加上每次请求600ms延迟,总耗时至少3800ms assertThat(totalTime).isGreaterThanOrEqualTo(3800); }
方案优势
- 无业务代码侵入:不需要额外封装方法,直接测试原有
ApiClient逻辑,避免测试代码污染业务代码 - 场景真实:通过Wiremock模拟真实外部API超时行为,验证完整调用链路(HttpClient -> Retry -> 外部API)
- 验证维度全面:可同时验证重试次数、延迟时间、异常处理等细节
内容的提问来源于stack exchange,提问作者Manish
相关产品推荐
相关产品推荐

