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

Spring Boot Kotlin中@Retryable下无法Mock第三方Final类的测试问题

解决Spring Retry与Mockito Inline Mock的冲突问题

这问题我之前在项目里也踩过坑,本质是Spring的CGLIB重试代理和Mockito的inline Mock模式之间的兼容性冲突,我来给你一步步拆解原因和解决方案:

问题根源

当你同时启用@EnableRetry和引入mockito-inline时,会出现两个关键冲突点:

  1. @EnableRetry会让Spring通过CGLIB为带有@Retryable的MyService生成代理类,用来实现重试逻辑;
  2. mockito-inline替换了Mockito默认的CGLIB MockMaker为inline实现,导致@SpyBean无法正确把Mockito的Spy包装到Spring的CGLIB代理实例上——这就是为什么你调用verify(myService)时会报错“不是mock对象”。

而当你移除其中一个依赖/注解时,冲突消失,测试自然就能通过,但这显然无法满足同时测试重试和mock final类的需求。

解决方案

我们可以调整测试方式,既保留Spring的重试代理,又能正常mock final类和验证方法调用:

方案1:调整测试类配置,兼容Spring代理与Mockito Spy

直接注入Spring代理后的MyService,在需要验证方法调用时手动用Mockito Spy包装它,这样既能保留重试逻辑,又能让Mockito识别对象:

@RunWith(SpringRunner::class)
@EnableRetry
class TTest {
    @MockBean
    lateinit var restClient: RestClient

    // 注入Spring生成的带重试代理的MyService实例
    @Autowired
    lateinit var myService: MyService

    @Test
    fun `should throw exception and trigger retry`() {
        // 借助mockito-inline mock final异常类
        val notFoundException = mock<HttpClientErrorException.NotFound>()
        whenever(restClient.getAllData()).thenThrow(notFoundException)

        // 验证调用时抛出目标异常
        assertThrows<HttpClientErrorException.NotFound> {
            myService.makeRestCall()
        }

        // 核心验证:重试逻辑生效,总共调用3次RestClient方法
        verify(restClient, times(3)).getAllData()
    }

    @Test
    fun `makeRestCall should be invoked`() {
        // 手动将Spring代理实例包装为Mockito Spy
        val spiedService = Mockito.spy(myService)
        
        spiedService.makeRestCall()
        
        // 现在可以正常验证方法调用了
        verify(spiedService).makeRestCall()
    }
}

方案2:优化依赖版本(可选)

建议把mockito-inline的版本升级到更稳定的版本(比如3.12.4及以上),避免旧版本和Spring的CGLIB代理之间的潜在兼容性问题:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <version>3.12.4</version>
    <scope>test</scope>
</dependency>

方案3:更合理的测试策略(推荐)

其实验证myService.makeRestCall()是否被调用的测试意义不大,重试功能的核心测试目标应该是验证重试逻辑是否生效——比如验证restClient.getAllData()被调用了3次(对应你设置的maxAttempts = 3),这也是方案1中第一个测试的核心逻辑。

为什么这个方案能解决问题?

  • 直接注入Spring代理的MyService保证了@Retryable的重试逻辑正常生效;
  • 手动用Mockito.spy()包装Spring代理实例,让Mockito能够识别并验证方法调用;
  • mockito-inline依然可以正常处理HttpClientErrorException.NotFound这类final类的mock需求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.07 07:24:11