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

但是,据我所知,它并没有:(

可能但不完整的解决方案


  1. 测试容器初始化脚本
new MSSQLServerContainer<>().withInitScript("setup.sql");

这有效并确保我可以 运行 第一次启动脚本,因为容器只启动了一次。然而 withInitScript 只接受一个参数而不是一个数组。因此,我需要将所有脚本连接到一个文件中,这意味着我必须维护两组脚本。

如果您只有一个脚本,这会很好。

  1. 出现错误继续
spring.datasource.continue-on-error=true

从架构中的启动错误被忽略的意义上来说,这是可行的。但是.. 如果有人在脚本中添加了一些狡猾的 SQL,我希望它在启动时失败。

  1. Spring 事件挂钩

我无法让它工作。我的想法是我可以监听 Co​​ntextRefreshedEvent,然后为 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"