Spring 使用测试容器启动 - 如何防止在上下文重新加载时进行数据库初始化
Spring boot with testcontainers - how to prevent DB initialization on context reload
上下文
我在 Spring 启动应用程序中有一套集成测试。测试上下文使用 MSSQL docker 容器作为其使用 testcontainers 框架的数据库。
我的一些测试将 Mockito 与 SpyBean 结合使用,apparently by design 将重新启动 Spring 上下文,因为侦测 bean 无法在测试之间共享。
因为我使用的是在我所有测试期间都存在的非嵌入式数据库,所以数据库是通过在开始时执行我的 schema.sql 和 data.sql 来配置的:-
spring.datasource.initialization-mode=always
问题是 当 Spring 上下文重新启动时,我的数据库再次重新初始化,这会触发诸如唯一约束问题、table 已经存在等错误。
我的亲测class如下如果对你有帮助的话:-
@ActiveProfiles(Profiles.PROFILE_TEST)
@Testcontainers
@SpringJUnitWebConfig
@AutoConfigureMockMvc
@SpringBootTest(classes = Application.class)
@ContextConfiguration(initializers = {IntegrationTest.Initializer.class})
public abstract class IntegrationTest {
private static final MSSQLServerContainer<?> mssqlContainer;
static {
mssqlContainer = new MSSQLServerContainer<>()
.withInitScript("setup.sql"); //Creates users/permissions etc
mssqlContainer.start();
}
static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(final ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues.of("spring.datasource.url=" + mssqlContainer.getJdbcUrl())
.applyTo(configurableApplicationContext.getEnvironment());
}
}
}
每个集成测试都会对此进行扩展,以便共享上下文(对于非间谍测试)并且设置仅发生一次。
我想要的
我希望能够在启动时只执行一次启动脚本,而不管上下文重新加载多少次都不会再执行一次。如果 Spring 测试框架可以记住我已经有一个已配置的数据库,那就太理想了。
我想知道是否有任何现有的配置或挂钩可以帮助我
如果有下面这样的东西就完美了。
spring.datasource.initialization-mode=always-once
但是,据我所知,它并没有:(
可能但不完整的解决方案
- 测试容器初始化脚本
new MSSQLServerContainer<>().withInitScript("setup.sql");
这有效并确保我可以 运行 第一次启动脚本,因为容器只启动了一次。然而 withInitScript 只接受一个参数而不是一个数组。因此,我需要将所有脚本连接到一个文件中,这意味着我必须维护两组脚本。
如果您只有一个脚本,这会很好。
- 出现错误继续
spring.datasource.continue-on-error=true
从架构中的启动错误被忽略的意义上来说,这是可行的。但是.. 如果有人在脚本中添加了一些狡猾的 SQL,我希望它在启动时失败。
- Spring 事件挂钩
我无法让它工作。我的想法是我可以监听 ContextRefreshedEvent,然后为 spring.datasource.initialization-mode=never.
注入一个新值
有点乱,但我尝试了类似下面的方法
@Component
public static class EventListener implements ApplicationListener<ApplicationEvent> {
@Autowired
private ConfigurableEnvironment environment;
@Override
public void onApplicationEvent(final ApplicationEvent event) {
log.info(event.getClass().getSimpleName());
if (event instanceof ContextRefreshedEvent) {
TestPropertyValues.of("spring.datasource.initialization-mode=never")
.applyTo(this.environment);
}
}
}
我的猜测是当上下文重新启动时,它还会重新加载我所有的原始 属性 来源,其中 mode=always。我需要在加载属性之后和模式创建之前立即发生事件。
那么,大家有什么建议吗?
所以我最终找到了解决方法。感觉很老套,但除非其他人能够提出更合适且不那么晦涩的修复方法,否则这就是我要解决的问题。
该解决方案结合了@tsarenkotx 建议的 AtomicBoolean 和我的 #3 部分解决方案。
@ActiveProfiles(Profiles.PROFILE_TEST)
@Testcontainers
@SpringJUnitWebConfig
@AutoConfigureMockMvc
@SpringBootTest(classes = Application.class)
@ContextConfiguration(initializers = {IntegrationTest.Initializer.class})
public abstract class IntegrationTest {
private static final MSSQLServerContainer mssqlContainer;
//added this
<b>private static final AtomicBoolean initDB = new AtomicBoolean(true);</b>
static {
mssqlContainer = new MSSQLServerContainer()
.withInitScript("setup.sql"); //Creates users/permissions etc
mssqlContainer.start();
}
static class Initializer implements ApplicationContextInitializer {
@Override
public void initialize(final ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues.of(
"spring.datasource.url=" + mssqlContainer.getJdbcUrl(),
//added this
<b>"spring.datasource.initialization-mode=" + (initDB.get() ? "always" : "never"))</b>
.applyTo(configurableApplicationContext.getEnvironment());
//added this
<b>initDB.set(false);</b>
}
}
}
基本上,我在第一次启动时将 spring.datasource.initialization-mode 设置为 always,因为数据库还没有尚未设置,然后在之后的每个上下文初始化中将其重置为 never。因此,Spring 不会在第一个 运行.
之后尝试执行启动脚本
效果很好,但我不喜欢在这里隐藏这个配置,所以仍然希望其他人能想出更好的东西 "by design"
上下文
我在 Spring 启动应用程序中有一套集成测试。测试上下文使用 MSSQL docker 容器作为其使用 testcontainers 框架的数据库。
我的一些测试将 Mockito 与 SpyBean 结合使用,apparently by design 将重新启动 Spring 上下文,因为侦测 bean 无法在测试之间共享。
因为我使用的是在我所有测试期间都存在的非嵌入式数据库,所以数据库是通过在开始时执行我的 schema.sql 和 data.sql 来配置的:-
spring.datasource.initialization-mode=always
问题是 当 Spring 上下文重新启动时,我的数据库再次重新初始化,这会触发诸如唯一约束问题、table 已经存在等错误。
我的亲测class如下如果对你有帮助的话:-
@ActiveProfiles(Profiles.PROFILE_TEST)
@Testcontainers
@SpringJUnitWebConfig
@AutoConfigureMockMvc
@SpringBootTest(classes = Application.class)
@ContextConfiguration(initializers = {IntegrationTest.Initializer.class})
public abstract class IntegrationTest {
private static final MSSQLServerContainer<?> mssqlContainer;
static {
mssqlContainer = new MSSQLServerContainer<>()
.withInitScript("setup.sql"); //Creates users/permissions etc
mssqlContainer.start();
}
static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(final ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues.of("spring.datasource.url=" + mssqlContainer.getJdbcUrl())
.applyTo(configurableApplicationContext.getEnvironment());
}
}
}
每个集成测试都会对此进行扩展,以便共享上下文(对于非间谍测试)并且设置仅发生一次。
我想要的
我希望能够在启动时只执行一次启动脚本,而不管上下文重新加载多少次都不会再执行一次。如果 Spring 测试框架可以记住我已经有一个已配置的数据库,那就太理想了。
我想知道是否有任何现有的配置或挂钩可以帮助我
如果有下面这样的东西就完美了。
spring.datasource.initialization-mode=always-once
但是,据我所知,它并没有:(
可能但不完整的解决方案
- 测试容器初始化脚本
new MSSQLServerContainer<>().withInitScript("setup.sql");
这有效并确保我可以 运行 第一次启动脚本,因为容器只启动了一次。然而 withInitScript 只接受一个参数而不是一个数组。因此,我需要将所有脚本连接到一个文件中,这意味着我必须维护两组脚本。
如果您只有一个脚本,这会很好。
- 出现错误继续
spring.datasource.continue-on-error=true
从架构中的启动错误被忽略的意义上来说,这是可行的。但是.. 如果有人在脚本中添加了一些狡猾的 SQL,我希望它在启动时失败。
- Spring 事件挂钩
我无法让它工作。我的想法是我可以监听 ContextRefreshedEvent,然后为 spring.datasource.initialization-mode=never.
注入一个新值有点乱,但我尝试了类似下面的方法
@Component
public static class EventListener implements ApplicationListener<ApplicationEvent> {
@Autowired
private ConfigurableEnvironment environment;
@Override
public void onApplicationEvent(final ApplicationEvent event) {
log.info(event.getClass().getSimpleName());
if (event instanceof ContextRefreshedEvent) {
TestPropertyValues.of("spring.datasource.initialization-mode=never")
.applyTo(this.environment);
}
}
}
我的猜测是当上下文重新启动时,它还会重新加载我所有的原始 属性 来源,其中 mode=always。我需要在加载属性之后和模式创建之前立即发生事件。
那么,大家有什么建议吗?
所以我最终找到了解决方法。感觉很老套,但除非其他人能够提出更合适且不那么晦涩的修复方法,否则这就是我要解决的问题。
该解决方案结合了@tsarenkotx 建议的 AtomicBoolean 和我的 #3 部分解决方案。
@ActiveProfiles(Profiles.PROFILE_TEST)
@Testcontainers
@SpringJUnitWebConfig
@AutoConfigureMockMvc
@SpringBootTest(classes = Application.class)
@ContextConfiguration(initializers = {IntegrationTest.Initializer.class})
public abstract class IntegrationTest {
private static final MSSQLServerContainer mssqlContainer;
//added this
<b>private static final AtomicBoolean initDB = new AtomicBoolean(true);</b>
static {
mssqlContainer = new MSSQLServerContainer()
.withInitScript("setup.sql"); //Creates users/permissions etc
mssqlContainer.start();
}
static class Initializer implements ApplicationContextInitializer {
@Override
public void initialize(final ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues.of(
"spring.datasource.url=" + mssqlContainer.getJdbcUrl(),
//added this
<b>"spring.datasource.initialization-mode=" + (initDB.get() ? "always" : "never"))</b>
.applyTo(configurableApplicationContext.getEnvironment());
//added this
<b>initDB.set(false);</b>
}
}
}
基本上,我在第一次启动时将 spring.datasource.initialization-mode 设置为 always,因为数据库还没有尚未设置,然后在之后的每个上下文初始化中将其重置为 never。因此,Spring 不会在第一个 运行.
之后尝试执行启动脚本效果很好,但我不喜欢在这里隐藏这个配置,所以仍然希望其他人能想出更好的东西 "by design"