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

Spring Boot单元测试中Retryable/Recover功能无法正常生效

Spring Retry单元测试不触发重试的问题

我搭建了一个调用外部服务并配置3次重试的Spring应用,用Postman测试时重试功能正常,但单元测试里目标方法只被调用1次就终止,完全不触发重试。我试过在测试类上加@EnableRetry,问题还是没解决,验证方法调用次数始终不符合预期。

相关代码如下:

主应用类

@SpringBootApplication
@EnableAutoConfiguration
@EnableRetry
public class ChallengeApplication {

    public static void main(String[] args) {
        SpringApplication.run(ChallengeApplication.class, args);
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

服务类

@Service
@Slf4j
public class RetryableInterestService {

    final RestTemplate restTemplate;
    protected Double lastResult = null;
    
    RetryableInterestService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }
    
    String uri = "http://localhost:8082/api/interest";
    
    @Retryable(value = {RestClientException.class}, maxAttempts = 3, backoff = @Backoff(200))
    public Double callInterestService() {
        log.info("Calling Interest service using uri: " + uri);
        lastResult = restTemplate.getForObject(uri, Double.class);
        return lastResult;
    }

    @Recover
    public Double returnLastValue() throws ExternalApiCallException {
        log.info("Returning last stored value: " + lastResult);
        if(lastResult != null) {
            return lastResult;
        } else {
            throw new ExternalApiCallException("There wasn't a backup Interest value to return");
        }
    }
}

原测试类

@SpringBootTest(classes = {ChallengeApplicationTests.class})
public class RetryableInterestServiceTest {
    @Mock
    RestTemplate restTemplate;
    @InjectMocks
    RetryableInterestService retryableInterestService;

    @Test
    void whenCallInterestService_shouldReturnDouble() {
        when(restTemplate.getForObject(anyString(), any())).thenReturn(10.0);
        Double result = retryableInterestService.callInterestService();
        assertEquals(result, 10.0);
    }

    @Test
    void givenException_whenCallInterestService_shouldRetry() {
        when(restTemplate.getForObject(anyString(), any()))
                .thenThrow(new RestClientException(""))
                .thenThrow(new RestClientException(""))
                .thenReturn(10.0);
        Double result = retryableInterestService.callInterestService();
        verify(restTemplate, times(3)).getForObject(any(), any());
    }
}

另外,当我用如下代码捕获异常时,验证显示方法仅被调用1次,导致测试失败:

Exception exception = assertThrows(RestClientException.class, () -> {
    retryableInterestService.callInterestService();
});

问题原因及解决方案

问题核心:Spring Retry依靠动态代理实现重试逻辑,只有Spring容器管理的代理对象才会触发重试。你用@InjectMocks直接实例化的RetryableInterestService是普通对象,没有被Spring代理,所以重试注解完全不起作用。

方案一:使用Spring容器管理Bean(推荐)

修改测试类,加载真实Spring上下文,用@MockBean替换容器中的RestTemplate,确保调用的是Spring管理的代理对象:

@SpringBootTest
@EnableRetry
public class RetryableInterestServiceTest {
    @MockBean
    RestTemplate restTemplate;

    @Autowired
    RetryableInterestService retryableInterestService;

    @Test
    void whenCallInterestService_shouldReturnDouble() {
        when(restTemplate.getForObject(anyString(), eq(Double.class))).thenReturn(10.0);
        Double result = retryableInterestService.callInterestService();
        assertEquals(10.0, result);
    }

    @Test
    void givenException_whenCallInterestService_shouldRetry() {
        when(restTemplate.getForObject(anyString(), eq(Double.class)))
                .thenThrow(new RestClientException(""))
                .thenThrow(new RestClientException(""))
                .thenReturn(10.0);
        
        Double result = retryableInterestService.callInterestService();
        verify(restTemplate, times(3)).getForObject(anyString(), eq(Double.class));
        assertEquals(10.0, result);
    }

    @Test
    void givenNoBackup_whenAllRetriesFail_shouldThrowException() {
        when(restTemplate.getForObject(anyString(), eq(Double.class)))
                .thenThrow(new RestClientException(""))
                .thenThrow(new RestClientException(""))
                .thenThrow(new RestClientException(""));
        
        ExternalApiCallException exception = assertThrows(ExternalApiCallException.class, () -> {
            retryableInterestService.callInterestService();
        });
        
        assertEquals("There wasn't a backup Interest value to return", exception.getMessage());
        verify(restTemplate, times(3)).getForObject(anyString(), eq(Double.class));
    }
}

方案二:手动创建重试代理(轻量版)

如果不想加载完整Spring上下文,可通过RetryTemplate手动为目标方法创建代理:

public class RetryableInterestServiceTest {
    @Mock
    RestTemplate restTemplate;
    private RetryableInterestService retryableInterestService;

    @BeforeEach
    void setUp() {
        retryableInterestService = new RetryableInterestService(restTemplate);
        // 构建重试模板,匹配注解配置
        RetryTemplate retryTemplate = RetryTemplate.builder()
                .maxAttempts(3)
                .retryOn(RestClientException.class)
                .fixedBackoff(200)
                .build();
        
        // 生成代理对象
        retryableInterestService = RetryProxyFactory.createProxy(
                retryableInterestService,
                RetryableInterestService.class,
                retryTemplate
        );
    }

    // 测试方法保持不变
}

额外注意事项

  • 原测试类中@SpringBootTest(classes = {ChallengeApplicationTests.class})是错误配置,应指定主应用类ChallengeApplication.class或留空(默认扫描主类)
  • 验证方法调用时,尽量用eq()明确参数类型,避免any()太宽泛导致匹配失效
  • 若使用@Recover方法,测试时要注意重试耗尽后会触发该方法,而非抛出原异常

内容的提问来源于stack exchange,提问作者alejo.montania

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 13:55:58