如何使用不同配置执行同一@SpringBootTest集成测试?
这个问题我太熟了!核心原因是Spring测试上下文的缓存机制在搞鬼——当你用同配置的@SpringBootTest时,Spring会复用已经创建好的上下文,而你的单例Bean在上下文启动时就完成初始化并读取了@Value的属性,后续@TestPropertySource的新属性根本没机会覆盖它。下面给你几个实用的解决办法,按推荐程度排序:
@DynamicPropertySource动态注入属性(推荐) 这是Spring Boot 2.2+提供的新特性,能在不重启上下文的情况下动态替换属性,既高效又灵活。比如:
@SpringBootTest public class FirstIntegrationTest { @DynamicPropertySource static void setProperties(DynamicPropertyRegistry registry) { registry.add("your.config.property", () -> "test-value-1"); } @Autowired private YourSingletonBean yourBean; @Test void testWithFirstProperty() { assertEquals("test-value-1", yourBean.getConfigProperty()); } } @SpringBootTest public class SecondIntegrationTest { @DynamicPropertySource static void setProperties(DynamicPropertyRegistry registry) { registry.add("your.config.property", () -> "test-value-2"); } @Autowired private YourSingletonBean yourBean; @Test void testWithSecondProperty() { assertEquals("test-value-2", yourBean.getConfigProperty()); } }
这种方式下,Spring会在每个测试类运行前动态替换属性,但需要调整你的单例Bean实现,改成从Environment里实时获取属性(而不是直接用@Value注入后就缓存):
@Component public class YourSingletonBean { private final Environment env; public YourSingletonBean(Environment env) { this.env = env; } public String getConfigProperty() { return env.getProperty("your.config.property"); } }
@TestPropertySource + @DirtiesContext(简单但低效) 如果不想修改Bean的代码,可以在每个测试类上加上@DirtiesContext,这样测试结束后Spring会销毁当前上下文,下一个测试类会重新创建上下文并加载新的@TestPropertySource属性:
@SpringBootTest @TestPropertySource(properties = "your.config.property=test-value-1") @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) public class FirstIntegrationTest { // 测试代码 } @SpringBootTest @TestPropertySource(properties = "your.config.property=test-value-2") @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) public class SecondIntegrationTest { // 测试代码 }
缺点是每次测试都要重启Spring上下文,测试速度会变慢,尤其是测试类多的时候,不太推荐大型项目用。
如果你的Bean已经用了@Value注入字段,还可以在测试中手动修改Spring的PropertySources,强制刷新属性值:
@SpringBootTest @TestPropertySource(properties = "your.config.property=default-value") public class IntegrationTest { @Autowired private YourSingletonBean yourBean; @Autowired private ConfigurableApplicationContext context; @Test void testFirstScenario() { // 修改属性 TestPropertyValues.of("your.config.property=test-value-1").applyTo(context.getEnvironment()); // 刷新Bean的属性 context.getAutowireCapableBeanFactory().autowireBean(yourBean); assertEquals("test-value-1", yourBean.getConfigProperty()); } @Test void testSecondScenario() { TestPropertyValues.of("your.config.property=test-value-2").applyTo(context.getEnvironment()); context.getAutowireCapableBeanFactory().autowireBean(yourBean); assertEquals("test-value-2", yourBean.getConfigProperty()); } }
如果Bean的属性是初始化后就固定的,还可以给Bean加上@RefreshScope注解,让它支持动态刷新:
@Component @RefreshScope public class YourSingletonBean { @Value("${your.config.property}") private String configProperty; // getter方法 }
如果业务逻辑允许,可以把单例Bean改成prototype scope,但这会改变Bean的生命周期,可能影响应用的正常运行,除非你的Bean本身就适合多实例,否则不建议这么做。
内容的提问来源于stack exchange,提问作者smigfu

