Spring Boot集成测试@SpyBean导致上下文重复加载的解决方法
问题背景
在Spring Boot集成测试中使用Testcontainers管理数据库依赖,为复用测试容器,每次测试前会清理整个数据库(包括Liquibase的DBCHANGELOG表)。此前批量运行测试时,Spring仅加载一次上下文,执行一次Liquibase迁移构建库结构,即便清库也能正常复用上下文。但引入@SpyBean后,该注解会触发Spring Boot上下文重新初始化,导致同一数据库再次执行迁移,因表已存在而失败。
解决方案
1. 改用@MockBean(适配业务场景)
如果无需保留原Bean的真实业务逻辑,仅需Mock行为,直接替换为@MockBean。@MockBean默认不会改变上下文的缓存标识,只要Mock的Bean类型与其他测试一致,就不会触发上下文重建。
2. 统一上下文缓存键
通过@TestPropertySource为所有测试类添加相同的缓存标识,强制共享同一上下文:
@TestPropertySource(properties = "test.context.cache.key=shared-integration-cache")
或者自定义ContextCustomizerFactory,忽略@SpyBean对缓存键的影响,确保带Spy的测试与其他测试使用同一缓存。
3. 容器启动时一次性执行Liquibase迁移
将Liquibase迁移逻辑从Spring上下文加载阶段分离,改为在Testcontainers数据库容器启动时仅执行一次:
- 自定义数据库容器的初始化脚本,手动调用Liquibase执行迁移;
- 这样即使上下文多次重建,也不会重复触发迁移,因为迁移已在容器启动时完成。
4. 手动创建Spy对象(不修改上下文)
不在类级别使用@SpyBean,而是在@BeforeEach中手动Spy目标Bean:
@Autowired private YourService yourService; private YourService spiedService; @BeforeEach void setUp() { spiedService = Mockito.spy(yourService); // 若需替换容器内的Bean,可通过BeanPostProcessor或手动替换逻辑实现 }
这种方式不会修改Spring上下文的Bean定义,因此不会触发上下文重建。
5. 临时方案:使用@DirtiesContext(不推荐)
在带@SpyBean的测试类上添加@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS),让测试执行完后销毁上下文。但此方案会导致上下文重新加载,降低测试效率,仅作为临时应急手段。
内容的提问来源于stack exchange,提问作者ishan

