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

Spring Boot 3.4中@MockitoSpyBean致@PostConstruct空指针问题求助

问题描述

将Spring Boot应用迁移至3.4版本后,原有的@MockBean和@SpyBean被弃用,替换为@MockitoBean和@MockitoSpyBean。但在标注@TestConfiguration的类中替换注解后,SpringBootTests初始化失败。

相关代码

测试配置类

@TestConfiguration
public class IntegrationTestConfig {

    @MockitoSpyBean
    private LanguageRepository languageRepository;

    @PostConstruct
    void setUp() {
        when(languageRepository.findAllCodes()).thenReturn(List.of("fr", "en"));
    }
}

自定义元注解

@Retention(RetentionPolicy.RUNTIME)
@SpringBootTest(classes = {IntegrationTestConfig.class, SpringSecurityTestConfig.class})
@ActiveProfiles("test")
@Sql(scripts = "classpath:sql/clearTables.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD)
@AutoConfigureMockMvc
public @interface IntegrationTest {

}

错误信息

java.lang.NullPointerException: Cannot invoke "package.whatever.LanguageRepository.findAllCodes()" because "this.languageRepository" is null

补充上下文

该@SpyBean用于模拟应用启动时的外部WS调用,对应启动逻辑:

@Bean
ApplicationRunner fetchTranslations(LocalazyService translationService) {
    log.info("Initializing translations...");
    return ignored -> translationService.updateTranslationsWithCdn();
}

疑问

  1. 我遗漏了什么?
  2. 如何恢复原有行为,最好不依赖基类继承和@BeforeEach初始化?

问题解答

1. 遗漏的关键点

@MockitoSpyBean和原@SpyBean的初始化时机存在差异:

  • 原@SpyBean由Spring Boot测试自动配置直接处理,会在@PostConstruct执行前完成Bean注入与Spy初始化。
  • 新的@MockitoBean/@MockitoSpyBean属于Mockito自身的Spring集成组件,依赖MockitoPostProcessor完成初始化,而该处理器的执行时机晚于@PostConstruct方法。这就导致setUp方法执行时,languageRepository还未被注入和初始化,最终抛出空指针异常。

另外,IntegrationTestConfig作为@TestConfiguration直接纳入测试上下文,@MockitoSpyBean在此类中的处理优先级与原有注解不同,无法像@SpyBean那样在Bean生命周期早期完成注入。

2. 恢复原有行为的方案

方案一:手动创建Spy并注册为Bean

放弃字段上的@MockitoSpyBean,直接通过Mockito手动创建Spy并注册为Bean,确保在@PostConstruct执行前Bean已配置完成:

@TestConfiguration
public class IntegrationTestConfig {

    @Bean
    public LanguageRepository languageRepository() {
        LanguageRepository spy = Mockito.spy(LanguageRepository.class);
        Mockito.when(spy.findAllCodes()).thenReturn(List.of("fr", "en"));
        return spy;
    }
}

这种方式绕过了注解初始化时机问题,在Bean定义阶段就完成Spy配置,能保证应用启动(包括ApplicationRunner执行)时Mock行为已生效。

方案二:使用@EventListener监听上下文初始化事件

将Mock配置逻辑放到ContextRefreshedEvent的监听方法中,该事件会在所有Bean初始化完成后触发:

@TestConfiguration
public class IntegrationTestConfig {

    @MockitoSpyBean
    private LanguageRepository languageRepository;

    @EventListener(ContextRefreshedEvent.class)
    void setUp() {
        when(languageRepository.findAllCodes()).thenReturn(List.of("fr", "en"));
    }
}

此方式能保证languageRepository已初始化,但需注意:若上下文多次刷新(如测试中某些场景),该方法会重复执行,需自行处理幂等性。

方案三:用@DependsOn强制初始化顺序(不推荐,稳定性差)

若坚持使用@MockitoSpyBean,可通过@DependsOn确保IntegrationTestConfig在MockitoPostProcessor处理完成后再执行@PostConstruct:

@TestConfiguration
@DependsOn("mockitoPostProcessor")
public class IntegrationTestConfig {

    @Autowired
    private LanguageRepository languageRepository;

    @PostConstruct
    void setUp() {
        when(languageRepository.findAllCodes()).thenReturn(List.of("fr", "en"));
    }
}

需将@MockitoSpyBean替换为@Autowired,直接注入已初始化的Spy即可。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 00:52:18