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(); }
疑问
- 我遗漏了什么?
- 如何恢复原有行为,最好不依赖基类继承和
@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

