Spring boot with testcontainers - 如何防止上下文重新加载时数据库初始化

Mat*_*t R 7 java spring spring-mvc spring-boot testcontainers

语境


我在 Spring boot 应用程序中有一套集成测试。测试上下文使用 MSSQL docker 容器作为其数据库,并使用testcontainers框架。

我的一些测试使用 Mockito 和 SpyBean,这显然是有意设计的,它将重新启动 Spring 上下文,因为监视的 bean 不能在测试之间共享。

由于我使用的是在所有测试期间都存在的非嵌入式数据库,因此通过在开始时执行我的 schema.sql 和 data.sql 来配置数据库:-

spring.datasource.initialization-mode=always
Run Code Online (Sandbox Code Playgroud)

问题是,当 Spring 上下文重新启动时,我的数据库再次重新初始化,这会触发诸如唯一约束问题、表已存在等错误。

我的父测试类如下(如果有帮助的话):-

@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());
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

每个集成测试都对此进行了扩展,以便共享上下文(对于非间谍测试)并且设置仅发生一次。

我想要的是


我希望能够在启动时仅执行一次启动脚本,并且无论上下文重新加载多少次,都不再执行一次。如果 Spring 测试框架能够记住我已经配置了一个数据库,那就太理想了。

我想知道是否有任何现有的配置或挂钩可以帮助我

如果有像下面这样的东西那就完美了。

spring.datasource.initialization-mode=always-once
Run Code Online (Sandbox Code Playgroud)

但是,据我所知,事实并非如此:(

可能但不完整的解决方案


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

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

如果您只有一个脚本,那么这会很好地工作。

  1. 出错时继续
spring.datasource.continue-on-error=true
Run Code Online (Sandbox Code Playgroud)

从某种意义上说,这是有效的,模式中的启动错误将被忽略。但是..如果有人在脚本中添加了一些不可靠的 SQL,我希望它在启动时失败。

  1. 春季事件挂钩

我无法让它发挥作用。我的想法是我可以监听 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);
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

我的猜测是,当上下文重新启动时,它还会再次重新加载所有具有mode=always的原始属性源。我需要在属性加载之后和模式创建发生之前发生一个事件。

那么,有人有什么建议吗?

Mat*_*t R 5

所以我最终找到了解决这个问题的方法。感觉很老套,但除非其他人能够提出更合适且不那么晦涩的修复方案,否则这就是我会采用的方法。

该解决方案结合使用了 AtomicBoolean 的 @tsarenkotxt 建议和我的 #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
        private static final AtomicBoolean initDB = new AtomicBoolean(true);

        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
                    "spring.datasource.initialization-mode=" + (initDB.get() ? "always" : "never"))
                .applyTo(configurableApplicationContext.getEnvironment());

                //added this
                initDB.set(false);
            }
        }
    }

Run Code Online (Sandbox Code Playgroud)

基本上,我将spring.datasource.initialization-mode设置为始终在第一次启动时设置,因为数据库尚未设置,然后将其重置为never对于此后的每个上下文初始化。因此,Spring 在第一次运行后不会尝试执行启动脚本。

效果很好,但我不喜欢在这里隐藏这个配置,所以仍然希望其他人能想出更好、更“按设计”的东西