You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何编写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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 06:15:14