Ensure containers are started before binding datasource properties

Update `TestcontainersLifecycleBeanPostProcessor` so that containers
are now initialized either on the first `postProcessAfterInitialization`
call with a frozen configuration or just before a test container
property is supplied.

Prior to this commit, it was assumed that the first post-process call
after the configuration was frozen was suitably early to initialize
the containers. This turns out to not be no always the case.

Specifically, in the `finishBeanFactoryInitialization` method of
`AbstractApplicationContext` we see that `LoadTimeWeaverAware` beans
are obtained before the configuration is frozen. One such bean is
`DefaultPersistenceUnitManager` which is likely to need datasource
properties that will require a started container.

To fix the problem, the `TestcontainersPropertySource` now publishes
a `BeforeTestcontainersPropertySuppliedEvent` to the ApplicationContext
just before any value is supplied. By listening for this event, we can
ensure that containers are initialized and started before any dynamic
property is read.

Fixes gh-38913
This commit is contained in:
Phillip Webb
2024-01-16 10:26:34 -08:00
parent f59fa2e3f7
commit 89874d351a
10 changed files with 313 additions and 39 deletions

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testcontainers.lifecycle;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.context.ImportTestcontainers;
import org.springframework.boot.testcontainers.lifecycle.TestcontainersImportWithPropertiesInjectedIntoLoadTimeWeaverAwareBeanIntegrationTests.Containers;
import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import org.springframework.context.annotation.Bean;
import org.springframework.context.weaving.LoadTimeWeaverAware;
import org.springframework.instrument.classloading.LoadTimeWeaver;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* @author Phillip Webb
*/
@ExtendWith(SpringExtension.class)
@DirtiesContext
@DisabledIfDockerUnavailable
@ImportTestcontainers(Containers.class)
class TestcontainersImportWithPropertiesInjectedIntoLoadTimeWeaverAwareBeanIntegrationTests {
// gh-38913
@Test
void starts() {
}
@TestConfiguration
@EnableConfigurationProperties(MockDataSourceProperties.class)
static class Config {
@Bean
MockEntityManager mockEntityManager(MockDataSourceProperties properties) {
return new MockEntityManager();
}
}
static class MockEntityManager implements LoadTimeWeaverAware {
@Override
public void setLoadTimeWeaver(LoadTimeWeaver loadTimeWeaver) {
}
}
@ConfigurationProperties("spring.datasource")
public static class MockDataSourceProperties {
private String url;
public String getUrl() {
return this.url;
}
public void setUrl(String url) {
this.url = url;
}
}
static class Containers {
@Container
static PostgreSQLContainer<?> container = new PostgreSQLContainer<>(DockerImageNames.postgresql());
@DynamicPropertySource
static void setConnectionProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", container::getJdbcUrl);
registry.add("spring.datasource.password", container::getPassword);
registry.add("spring.datasource.username", container::getUsername);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,9 @@
package org.springframework.boot.testcontainers.properties;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -25,6 +28,7 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleApplicationContextInitializer;
import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable;
import org.springframework.boot.testsupport.testcontainers.RedisContainer;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@@ -46,11 +50,16 @@ class TestcontainersPropertySourceAutoConfigurationTests {
@Test
void containerBeanMethodContributesProperties() {
this.contextRunner.withUserConfiguration(ContainerAndPropertiesConfiguration.class).run((context) -> {
TestBean testBean = context.getBean(TestBean.class);
RedisContainer redisContainer = context.getBean(RedisContainer.class);
assertThat(testBean.getUsingPort()).isEqualTo(redisContainer.getFirstMappedPort());
});
List<ApplicationEvent> events = new ArrayList<>();
this.contextRunner.withUserConfiguration(ContainerAndPropertiesConfiguration.class)
.withInitializer((context) -> context.addApplicationListener(events::add))
.run((context) -> {
TestBean testBean = context.getBean(TestBean.class);
RedisContainer redisContainer = context.getBean(RedisContainer.class);
assertThat(testBean.getUsingPort()).isEqualTo(redisContainer.getFirstMappedPort());
assertThat(events.stream().filter(BeforeTestcontainersPropertySuppliedEvent.class::isInstance))
.hasSize(1);
});
}
@Configuration(proxyBeanMethods = false)

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,10 +16,15 @@
package org.springframework.boot.testcontainers.properties;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.mock.env.MockEnvironment;
@@ -101,4 +106,20 @@ class TestcontainersPropertySourceTests {
assertThat(p1).isSameAs(p2);
}
@Test
void getPropertyPublishesEvent() {
try (GenericApplicationContext applicationContext = new GenericApplicationContext()) {
List<ApplicationEvent> events = new ArrayList<>();
applicationContext.addApplicationListener(events::add);
DynamicPropertyRegistry registry = TestcontainersPropertySource.attach(applicationContext.getEnvironment(),
(BeanDefinitionRegistry) applicationContext.getBeanFactory());
applicationContext.refresh();
registry.add("test", () -> "spring");
assertThat(applicationContext.getEnvironment().containsProperty("test")).isTrue();
assertThat(events.isEmpty());
assertThat(applicationContext.getEnvironment().getProperty("test")).isEqualTo("spring");
assertThat(events.stream().filter(BeforeTestcontainersPropertySuppliedEvent.class::isInstance)).hasSize(1);
}
}
}