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
相关产品推荐
相关产品推荐

