6

Context


I have a suite of Integration tests in a Spring boot application. The test context uses a MSSQL docker container for it's database using the testcontainers framework.

Some of my tests use Mockito with SpyBean which, apparently by design, will restart the Spring context since the spied beans cannot be shared between tests.

Since I am using a non-embedded database that lives for the duration of all my tests, the database is provisioned by executing my schema.sql and data.sql at the start by using:-

spring.datasource.initialization-mode=always

The problem is that when the Spring context is restarted, my database is re-initialized again which triggers errors such as unique constraint issues, table already exists etc.

My parent test class is as follows if it's of any help:-

@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());
        }
    }
}

Each integration test extends this so that the context (for non-spied tests) is shared and setup occurs just once.

What I want


I would like to be able to execute the startup scripts just one time on startup and never again despite any number of context reloads. If the Spring test framework could remember that I already have a provisioned DB, that would be ideal.

I am wondering if there are any existing configurations or hooks that may help me

If something like the following existed, it'd be perfect.

spring.datasource.initialization-mode=always-once

But, as far as I can tell, it doesn't :(

Possible, but incomplete, solutions


  1. Test container init script
new MSSQLServerContainer<>().withInitScript("setup.sql");

This works and ensures I can run a startup script the first time only since the container is started up just once. However withInitScript only takes a single argument rather than an array. As such, I would need to concatenate all my scripts into one file which means I'd have to maintain two sets of scripts.

If you only had one script, this would work fine.

  1. Continue on error
spring.datasource.continue-on-error=true

This works in the sense that startup errors in the schema are ignored. But.. I want it to fail on startup if someone put some dodgy SQL in the scripts.

  1. Spring event hooks

I couldn't get this to work. My idea was that I could listen for the ContextRefreshedEvent and then inject a new value for spring.datasource.initialization-mode=never.

It's a bit of a hack but I tried something like the following

    @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);
            }
        }
    }

My guess is when the context restarts, it will also reload all my original property sources again which has mode=always. I would need an event right after the properties are loaded and right before the schema creation occurs.

So with that, does anyone have any suggestions?

Matt R
  • 1,276
  • 16
  • 29
  • 1
    you can try to implement a custom `once` functionality for your tests with `ResourceDatabasePopulator` -> `addScripts()` -> `execute()`, if it works for you, also have a look at `DataSourceInitializerPostProcessor` – tsarenkotxt Nov 25 '19 at 02:42
  • @tsarenkotxt Interesting but my understanding is I'd declare this a a bean which means it would be recreated (and thus re-executed) on context reload would it not? I'd effectively need to refer to a state that persists past context reload somehow in order to keep track of a counter or did you have something else in mind? – Matt R Nov 25 '19 at 02:51
  • yes, this bean will be recreated. yes, to store the state for example in a static `AtomicBoolean`, maybe this is not the best approach, but it may work for the tests – tsarenkotxt Nov 25 '19 at 03:24
  • I would move to flyway. Flyway will check a metadata table to see which version scripts will need to be applied. If the DB is persistent flyway will not reapply the existing versions. – Martin Frey Nov 25 '19 at 11:28
  • @MartinFrey great suggestion and it is definitely something in our pipeline to support either Flyway or Liquibase but requires significantly more effort to implement and support. But it doesn't directly solve the problem I'm facing which is preventing a re-provision of a non-embedded DB on Springboottest startup ;) – Matt R Nov 26 '19 at 10:16

1 Answers1

5

So I ended up finding a workaround for this. Feels hacky but unless someone else is able to suggest a more appropriate and less obscure fix, then this is what I'll go with.

The solution uses a combination of @tsarenkotxt suggestion of AtomicBoolean and my #3 partial solution.



    @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);
            }
        }
    }

Basically I set spring.datasource.initialization-mode to be always on the very first startup, since the db hasn't been setup yet, and then reset it to never for every context initialization thereafter. As such, Spring won't attempt to execute the startup scripts after the first run.

Works great but I don't like having to hide this configuration here so still hoping someone else will come up with something better and more "by design"

Matt R
  • 1,276
  • 16
  • 29