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

如何为带有Spring Retry注解的Feign Client编写重试测试?

测试带Spring Retry的Feign Client重试逻辑

你定义了带有@Retryable注解的Feign Client接口,尝试用@SpringBootTest结合Mockito的verify测试重试时,直接Mock MyClient接口的方式无法验证真实的重试逻辑——因为Spring Retry是通过AOP代理实现的,只有容器生成的真实Feign代理实例才会携带重试逻辑,Mock的接口不会触发重试。

以下是两种可行的测试方案:


方案一:用WireMock模拟服务端错误(贴近真实场景)

这种方式通过模拟服务端返回HTTP错误,触发Feign的异常转换,进而触发Spring Retry的重试逻辑,最接近生产环境的真实调用场景。

步骤:

  1. 添加WireMock依赖(以Maven为例):
<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock-jre8-spring-boot-starter</artifactId>
    <version>2.35.0</version>
    <scope>test</scope>
</dependency>
  1. 编写测试类:
import com.github.tomakehurst.wiremock.client.WireMock;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;
import org.springframework.test.context.TestPropertySource;

@SpringBootTest
@AutoConfigureWireMock(port = 0) // 随机端口启动WireMock
@TestPropertySource(properties = {
        "feign.client.config.my-client.url=http://localhost:${wiremock.server.port}",
        "spring.retry.backoff.delay=0", // 关闭退避等待,加快测试速度
        "spring.retry.backoff.multiplier=1"
})
public class MyClientRetryTest {

    @Autowired
    private MyClient myClient;

    @Test
    void testPerformACallRetriesOnFailure() {
        // 模拟服务端返回500错误
        WireMock.stubFor(WireMock.post(WireMock.urlEqualTo(""))
                .willReturn(WireMock.serverError()));

        // 调用方法(预期会抛出FeignException,因为最终重试3次后仍失败)
        try {
            myClient.performACall(new SomeBody());
        } catch (Exception ignored) {
            // 忽略最终的失败异常,只关注重试次数
        }

        // 验证WireMock收到了3次请求(对应maxAttempts=3)
        WireMock.verify(3, WireMock.postRequestedFor(WireMock.urlEqualTo("")));
    }
}

方案二:Mock Feign的ErrorDecoder(轻量单元测试)

如果不想启动完整的HTTP服务,可以通过Mock Feign的ErrorDecoder组件,强制抛出触发重试的异常,验证重试逻辑。

步骤:

编写测试类:

import feign.FeignException;
import feign.codec.ErrorDecoder;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;

@SpringBootTest
@TestPropertySource(properties = {
        "spring.retry.backoff.delay=0",
        "spring.retry.backoff.multiplier=1"
})
public class MyClientRetryMockTest {

    @Autowired
    private MyClient myClient;

    @MockBean
    private ErrorDecoder errorDecoder;

    @Test
    void testPerformACallRetriesOnFailure() {
        // 配置ErrorDecoder每次调用都抛出FeignException
        Mockito.when(errorDecoder.decode(Mockito.anyString(), Mockito.any()))
                .thenThrow(FeignException.internalServerError("Mock error", null));

        // 调用方法
        try {
            myClient.performACall(new SomeBody());
        } catch (Exception ignored) {
        }

        // 验证ErrorDecoder被调用了3次(对应重试3次)
        Mockito.verify(errorDecoder, Mockito.times(3))
                .decode(Mockito.anyString(), Mockito.any());
    }
}

关键注意事项:

  • 重试触发条件:默认@Retryable会对所有RuntimeException重试,Feign的FeignException属于RuntimeException,所以无需额外配置;如果需要指定特定异常,可在@Retryable(include = {FeignException.class})中声明。
  • 关闭退避等待:测试时设置spring.retry.backoff.delay=0可以避免因退避策略导致的测试等待时间过长。
  • 避免Mock接口本身:必须注入Spring容器生成的真实MyClient代理实例,而非直接Mock该接口,否则重试逻辑不会生效。

内容的提问来源于stack exchange,提问作者Chanandler Bong

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 02:42:43