'Environment variables set via System#setProperty used in Spring application.yml

I have been reading Baeldung's Testcontainer HowTo where in section 4 it is mentioned:

In the start() method we use System#setProperty to set connection parameters as environment variables. We can now put them in our application.properties file:

spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}

I tried this approach - albeit with using Junit5 and hence no @ClassRule to fire up the container as stated in the tutorial, yet @Container instead.

It turns out: These environment variables are seemingly not used in the Spring context. They seems to be empty, yet e.g. the DB URL apparently defaults to localhost:5432 when using PostgreSQL.

My question: I wonder if and how programatically defined environment variables would work in an application.properties or application.yml.

Would not @SpringBootTest load my context as defined in application.yml (with empty env vars) before the Container is initialized and defines those DB_ variables?

It would be nice if this approach actually worked, because I believe this would help me to trigger Liquibase prior to test execution, although there is certainly a way to do that programmatically as well.

PS: With a custom ApplicationContextInitializer which programatically sets the data source properties, the test does work.

**** EDIT ****

I found out that the feature of setting properties via programmatically defined env vars does work. However, I was misled due to the following setup:

My project base directory has a subdir config with config/application.yml as the externalized config source in it. I thought that is a good idea - better than placing a config in src/main/resources, because - unless one explicitly excludes it - it would land in the jar and I would not necessarily want that.

However, I did believe that a src/test/resources/application.yml (further called: T) would override any settings obtained from config/application.yml (further called: P). Apparently not quite, which I found out after adding Liquibase:

P contains spring.liquibase.enable=false while T contains spring.liquibase.enable=true and spring.liquibase.changeLog=classpath:/db/foo.yaml. Launching the tests, I see no Liquibase updates. Setting spring.liquibase.enable=true in P, however, results in Liquibase updates being applied during integration tests. Also, not setting spring.liquibase.changeLog=classpath:/db/foo.yaml in T makes Liquibase complain about a missing changelog file (it defaults to some file name I do not use). That means, T must be read after all. But apparently, properties set in P are not overridden in T (which may be a feature I was not aware of). Such overriding does work, however, when moving P to src/main/resources/application.yml.

Also removed the Junit4 annotation @RunWith following @M.Deinum's good remark.

**** END OF EDIT ****

Full test class:

// @RunWith(SpringRunner.class)
@ExtendWith(SpringExtension.class)
@SpringBootTest
@Testcontainers
@ActiveProfiles("integration-test-postgres")
class MyRepositoryIntegrationTest {

    @Autowired
    MyRepository repository;

    @Container
    static JdbcDatabaseContainer<?> databaseContainer = IntegrationTestPostgresContainer.getInstance();

    @Test
    void test() {
        assertEquals("postgres", System.getProperty("DB_USERNAME"));
        assertEquals("postgres", System.getProperty("DB_PASSWORD"));
        assertTrue(databaseContainer.isRunning());
    }
}

Container extension:

public class IntegrationTestPostgresContainer extends PostgreSQLContainer<IntegrationTestPostgresContainer> {

    private static final String IMAGE_VERSION = "postgres:12.3";
    private static IntegrationTestPostgresContainer container;

    private IntegrationTestPostgresContainer() {
        super(IMAGE_VERSION);
    }

    public static IntegrationTestPostgresContainer getInstance() {
        if (container == null) {
            container = new IntegrationTestPostgresContainer()
                .withDatabaseName("mddb")
                .withUsername("postgres")
                .withPassword("postgres");
        }
        return container;
    }

    @Override
    public void start() {
        super.start();
        System.setProperty("DB_URL", container.getJdbcUrl());
        System.setProperty("DB_USERNAME", container.getUsername());
        System.setProperty("DB_PASSWORD", container.getPassword());
    }

    @Override
    public void stop() {
        // noop
    }

}

Initializer, to be added to the test context via @ContextConfiguration(initializers = {DataSourceContextInitializer.class}) in order to make the test pass:

public class DataSourceContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    private final static JdbcDatabaseContainer<?> CONTAINER = IntegrationTestPostgresContainer.getInstance();

    public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
            TestPropertyValues.of(
                "spring.datasource.url=" + CONTAINER.getJdbcUrl(),
                "spring.datasource.username=" + CONTAINER.getUsername(),
                "spring.datasource.password=" + CONTAINER.getPassword()
            )
                .applyTo(configurableApplicationContext.getEnvironment());
    }
}

application.yml

---
spring:
  config:
    activate:
      on-profile: integration-test-postgres
  datasource:
    driver-class-name: org.postgresql.Driver


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source