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

使用SpringRunner测试Spring-Retry时@Autowired Bean为空求助

问题:SpringRunner测试中@Autowired注入Bean为空且@Retryable重试逻辑验证失败

使用SpringRunner为带有Spring Retry注解的DeltaHelper类编写单元测试时,出现两个问题:

  • @Autowired注入的DeltaHelper实例为空
  • 尝试验证重试逻辑时无法得到预期结果

曾尝试使用MockitoJUnitRunner,但@Retryable注解仅在Spring环境下才能生效,因此必须使用SpringRunner。

测试类代码

@RunWith(SpringRunner.class)
@ContextConfiguration
public class DeltaHelperTest {
    @Autowired
    private DeltaHelper deltaHelper;
    
    @Before
    public void setUp() { System.setProperty("delta.process.retries", "2"); }

    @After
    public void validate() { validateMockitoUsage(); }

    @Test
    public void retriesAfterOneFailAndThenPass() throws Exception {
        when(deltaHelper.restService.call(any(), any())).thenThrow(new HttpException());
        deltaHelper.process(any(),any());
        verify(deltaHelper, times(2)).process(any(), any());
    }

    @Configuration
    @EnableRetry
    @EnableAspectJAutoProxy(proxyTargetClass=true)
    @Import(MockitoSkipAutowireConfiguration.class)

    public static class Application {

        @Bean
        public DeltaHelper deltaHelper() {
            DeltaHelper deltaHelper = new DeltaHelper();
            deltaHelper.myStorageService= myStorageService();
            deltaHelper.restService = restService();
            return deltaHelper;
        }

        @Bean
        public MyStorageService myStorageService() {
            return new MyStorageService();
        }

        @Bean
        public MyRestService restService() {
            return new MyRestService();
        }

        @Bean
        public MyRepo myRepository() {
            return mock(MyRepo.class);
        }
    }


    @Configuration
    public static class MockitoSkipAutowireConfiguration {
        @Bean MockBeanFactory mockBeanFactory() {
            return new MockBeanFactory();
        }
        private static class MockBeanFactory extends InstantiationAwareBeanPostProcessorAdapter {
            @Override
            public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
                return !mockingDetails(bean).isMock();
            }
        }
    }
}

相关业务类代码

@Component
public class DeltaHelper {

    @Autowired
    MyRestService restService;

    @Autowired
    MyStorageService myStorageService;

    @NotNull
    @Retryable(
            value = Exception.class,
            maxAttemptsExpression = "${delta.process.retries}"
    )
    public String process(String api, HttpEntity<?> entity) {
            return restService.call(api, entity);
    }
    @Recover
    public String recover(Exception e, String api, HttpEntity<?> entity) {
            myStorageService.save(api);
            return "recover";
    }
}

@Service
public class MyStorageService {

    @Autowired
    MyRepo myRepo;

    @Async
    public MyEntity save(String api) {
        return myRepo.save(new MyEntity(api, System.currentTimeMillis()));
    }
}

public class MyRestService extends org.springframework.web.client.RestTemplate {
}

问题分析

  1. @Autowired注入为空:

    • 测试类中手动new DeltaHelper()并设置依赖,没有让Spring通过依赖注入机制管理该Bean,导致Spring无法为其创建代理对象,同时可能因为Bean创建方式不符合Spring规范,导致@Autowired注入失败。
    • MockitoSkipAutowireConfiguration中的MockBeanFactory会阻止Mock对象的后续处理,干扰了Spring对Bean的正常初始化流程。
  2. 重试逻辑验证失败:

    • 手动实例化的DeltaHelper没有被Spring的Retry切面代理,@Retryable注解完全不生效,自然无法触发重试。
    • 测试中错误地验证deltaHelper.process()的调用次数,实际上重试是由Spring切面拦截后重复调用方法内部逻辑,而非外部重复调用process方法,应该验证依赖的restService.call()的调用次数。

解决方案

1. 正确配置Spring上下文,使用@MockBean模拟依赖

改用@SpringBootTest(如果是Spring Boot项目)或调整@ContextConfiguration,让Spring管理Bean的创建和代理,同时用@MockBean模拟MyRestService等依赖,避免手动实例化。

2. 调整测试逻辑,验证正确的对象

验证restService.call()的调用次数是否符合重试配置,而不是DeltaHelper的process方法。

修改后的测试类示例

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {DeltaHelper.class, MyStorageService.class})
@EnableRetry
@EnableAsync // 因为MyStorageService有@Async方法,需要开启异步支持
public class DeltaHelperTest {
    @Autowired
    private DeltaHelper deltaHelper;

    @MockBean
    private MyRestService restService;

    @MockBean
    private MyRepo myRepo;

    @Before
    public void setUp() {
        System.setProperty("delta.process.retries", "2");
    }

    @Test
    public void retriesAfterOneFailAndThenPass() throws Exception {
        // 第一次调用抛异常,触发重试
        when(restService.call(anyString(), any(HttpEntity.class)))
                .thenThrow(new HttpException())
                .thenReturn("success");

        String result = deltaHelper.process("test-api", new HttpEntity<>(null));

        // 验证restService被调用2次(1次失败+1次重试成功)
        verify(restService, times(2)).call(anyString(), any(HttpEntity.class));
        assertEquals("success", result);
    }

    @Test
    public void triggersRecoverAfterAllRetriesFail() throws Exception {
        // 所有重试都抛异常,触发@Recover方法
        when(restService.call(anyString(), any(HttpEntity.class)))
                .thenThrow(new HttpException());

        String result = deltaHelper.process("test-api", new HttpEntity<>(null));

        // 验证restService被调用2次(重试次数maxAttempts=2)
        verify(restService, times(2)).call(anyString(), any(HttpEntity.class));
        // 验证存储服务的save方法被调用
        verify(myRepo, times(1)).save(any(MyEntity.class));
        assertEquals("recover", result);
    }
}

额外注意事项

  • MyStorageService中的@Async方法需要在测试类或配置类中添加@EnableAsync才能生效,否则会同步执行。
  • 如果不是Spring Boot项目,可以用@ContextConfiguration指定包含@EnableRetry、@EnableAsync的配置类,替代@SpringBootTest。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 01:05:26