Prefer DynamicPropertyRegistar to DynamicPropertyRegistry
Closes gh-41996
This commit is contained in:
@@ -364,9 +364,9 @@ TIP: You can use the Maven goal `spring-boot:test-run` or the Gradle task `bootT
|
||||
[[features.dev-services.testcontainers.at-development-time.dynamic-properties]]
|
||||
==== Contributing Dynamic Properties at Development Time
|
||||
|
||||
If you want to contribute dynamic properties at development time from your `Container` `@Bean` methods, you can do so by injecting a `DynamicPropertyRegistry`.
|
||||
This works in a similar way to the xref:testing/testcontainers.adoc#testing.testcontainers.dynamic-properties[`@DynamicPropertySource` annotation] that you can use in your tests.
|
||||
It allows you to add properties that will become available once your container has started.
|
||||
If you want to contribute dynamic properties at development time from your `Container` `@Bean` methods, define an additional `DynamicPropertyRegistrar` bean.
|
||||
The registrar should be defined using a `@Bean` method that injects the container from which the properties will be sourced as a parameter.
|
||||
This arrangement ensures that container has been started before the properties are used.
|
||||
|
||||
A typical configuration would look like this:
|
||||
|
||||
|
||||
@@ -20,17 +20,22 @@ import org.testcontainers.containers.MongoDBContainer;
|
||||
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertyRegistrar;
|
||||
|
||||
@TestConfiguration(proxyBeanMethods = false)
|
||||
public class MyContainersConfiguration {
|
||||
|
||||
@Bean
|
||||
public MongoDBContainer mongoDbContainer(DynamicPropertyRegistry properties) {
|
||||
MongoDBContainer container = new MongoDBContainer("mongo:5.0");
|
||||
properties.add("spring.data.mongodb.host", container::getHost);
|
||||
properties.add("spring.data.mongodb.port", container::getFirstMappedPort);
|
||||
return container;
|
||||
public MongoDBContainer mongoDbContainer() {
|
||||
return new MongoDBContainer("mongo:5.0");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DynamicPropertyRegistrar mongoDbProperties(MongoDBContainer container) {
|
||||
return (properties) -> {
|
||||
properties.add("spring.data.mongodb.host", container::getHost);
|
||||
properties.add("spring.data.mongodb.port", container::getFirstMappedPort);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -18,18 +18,23 @@ package org.springframework.boot.docs.features.testcontainers.atdevelopmenttime.
|
||||
|
||||
import org.springframework.boot.test.context.TestConfiguration
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.test.context.DynamicPropertyRegistry
|
||||
import org.springframework.test.context.DynamicPropertyRegistrar;
|
||||
import org.testcontainers.containers.MongoDBContainer
|
||||
|
||||
@TestConfiguration(proxyBeanMethods = false)
|
||||
class MyContainersConfiguration {
|
||||
|
||||
@Bean
|
||||
fun mongoDbContainer(properties: DynamicPropertyRegistry): MongoDBContainer {
|
||||
var container = MongoDBContainer("mongo:5.0")
|
||||
properties.add("spring.data.mongodb.host", container::getHost)
|
||||
properties.add("spring.data.mongodb.port", container::getFirstMappedPort)
|
||||
return container
|
||||
fun mongoDbContainer(): MongoDBContainer {
|
||||
return MongoDBContainer("mongo:5.0")
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun mongoDbProperties(container: MongoDBContainer): DynamicPropertyRegistrar {
|
||||
return DynamicPropertyRegistrar { properties ->
|
||||
properties.add("spring.data.mongodb.host") { container.host }
|
||||
properties.add("spring.data.mongodb.port") { container.firstMappedPort }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ class ImportTestcontainersTests {
|
||||
void importWhenHasContainerDefinitionsWithDynamicPropertySource() {
|
||||
this.applicationContext = new AnnotationConfigApplicationContext(
|
||||
ContainerDefinitionsWithDynamicPropertySource.class);
|
||||
assertThat(this.applicationContext.getEnvironment().containsProperty("container.port")).isTrue();
|
||||
assertThat(this.applicationContext.getEnvironment().getProperty("container.port")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -21,19 +21,22 @@ import java.util.List;
|
||||
|
||||
import com.redis.testcontainers.RedisContainer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.boot.testcontainers.lifecycle.BeforeTestcontainerUsedEvent;
|
||||
import org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleApplicationContextInitializer;
|
||||
import org.springframework.boot.testsupport.container.DisabledIfDockerUnavailable;
|
||||
import org.springframework.boot.testsupport.container.TestImage;
|
||||
import org.springframework.boot.testsupport.system.CapturedOutput;
|
||||
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.DynamicPropertyRegistrar;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -42,8 +45,10 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* Tests for {@link TestcontainersPropertySourceAutoConfiguration}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@DisabledIfDockerUnavailable
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
class TestcontainersPropertySourceAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
@@ -51,18 +56,67 @@ class TestcontainersPropertySourceAutoConfigurationTests {
|
||||
.withConfiguration(AutoConfigurations.of(TestcontainersPropertySourceAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void containerBeanMethodContributesProperties() {
|
||||
List<ApplicationEvent> events = new ArrayList<>();
|
||||
@SuppressWarnings("removal")
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
void registeringADynamicPropertyFailsByDefault() {
|
||||
this.contextRunner.withUserConfiguration(ContainerAndPropertiesConfiguration.class)
|
||||
.run((context) -> assertThat(context).getFailure()
|
||||
.rootCause()
|
||||
.isInstanceOf(
|
||||
org.springframework.boot.testcontainers.properties.TestcontainersPropertySource.DynamicPropertyRegistryInjectionException.class)
|
||||
.hasMessageStartingWith(
|
||||
"Support for injecting a DynamicPropertyRegistry into @Bean methods is deprecated"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("removal")
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
void registeringADynamicPropertyCanLogAWarningAndContributeProperty(CapturedOutput output) {
|
||||
List<ApplicationEvent> events = new ArrayList<>();
|
||||
this.contextRunner.withPropertyValues("spring.testcontainers.dynamic-property-registry-injection=warn")
|
||||
.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(BeforeTestcontainerUsedEvent.class::isInstance)).hasSize(1);
|
||||
assertThat(events.stream()
|
||||
.filter(org.springframework.boot.testcontainers.lifecycle.BeforeTestcontainerUsedEvent.class::isInstance))
|
||||
.hasSize(1);
|
||||
assertThat(output)
|
||||
.contains("Support for injecting a DynamicPropertyRegistry into @Bean methods is deprecated");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("removal")
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
void registeringADynamicPropertyCanBePermittedAndContributeProperty(CapturedOutput output) {
|
||||
List<ApplicationEvent> events = new ArrayList<>();
|
||||
this.contextRunner.withPropertyValues("spring.testcontainers.dynamic-property-registry-injection=allow")
|
||||
.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(org.springframework.boot.testcontainers.lifecycle.BeforeTestcontainerUsedEvent.class::isInstance))
|
||||
.hasSize(1);
|
||||
assertThat(output)
|
||||
.doesNotContain("Support for injecting a DynamicPropertyRegistry into @Bean methods is deprecated");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void dynamicPropertyRegistrarBeanContributesProperties(CapturedOutput output) {
|
||||
this.contextRunner.withUserConfiguration(ContainerAndPropertyRegistrarConfiguration.class).run((context) -> {
|
||||
TestBean testBean = context.getBean(TestBean.class);
|
||||
RedisContainer redisContainer = context.getBean(RedisContainer.class);
|
||||
assertThat(testBean.getUsingPort()).isEqualTo(redisContainer.getFirstMappedPort());
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableConfigurationProperties(ContainerProperties.class)
|
||||
@Import(TestBean.class)
|
||||
@@ -77,6 +131,23 @@ class TestcontainersPropertySourceAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableConfigurationProperties(ContainerProperties.class)
|
||||
@Import(TestBean.class)
|
||||
static class ContainerAndPropertyRegistrarConfiguration {
|
||||
|
||||
@Bean
|
||||
RedisContainer redisContainer() {
|
||||
return TestImage.container(RedisContainer.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
DynamicPropertyRegistrar redisProperties(RedisContainer container) {
|
||||
return (registry) -> registry.add("container.port", container::getFirstMappedPort);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ConfigurationProperties("container")
|
||||
record ContainerProperties(int port) {
|
||||
}
|
||||
|
||||
@@ -18,26 +18,41 @@ package org.springframework.boot.testcontainers.properties;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.testcontainers.properties.TestcontainersPropertySourceAutoConfigurationWithSpringBootTestIntegrationTest.TestConfig;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.test.context.DynamicPropertyRegistrar;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link TestcontainersPropertySourceAutoConfiguration} when combined with
|
||||
* {@link SpringBootTest @SpringBootTest}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@SpringBootTest(classes = TestConfig.class)
|
||||
@SpringBootTest(classes = TestConfig.class,
|
||||
properties = "spring.testcontainers.dynamic-property-registry-injection=allow")
|
||||
class TestcontainersPropertySourceAutoConfigurationWithSpringBootTestIntegrationTest {
|
||||
|
||||
@Test
|
||||
void injectsRegistry() {
|
||||
@Autowired
|
||||
private Environment environment;
|
||||
|
||||
@Test
|
||||
void injectsRegistryIntoBeanMethod() {
|
||||
assertThat(this.environment.getProperty("from.bean.method")).isEqualTo("one");
|
||||
}
|
||||
|
||||
@Test
|
||||
void callsRegistrars() {
|
||||
assertThat(this.environment.getProperty("from.registrar")).isEqualTo("two");
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
@@ -47,10 +62,15 @@ class TestcontainersPropertySourceAutoConfigurationWithSpringBootTestIntegration
|
||||
|
||||
@Bean
|
||||
String example(DynamicPropertyRegistry registry) {
|
||||
registry.add("test", () -> "test");
|
||||
registry.add("from.bean.method", () -> "one");
|
||||
return "Hello";
|
||||
}
|
||||
|
||||
@Bean
|
||||
DynamicPropertyRegistrar propertyRegistrar() {
|
||||
return (registry) -> registry.add("from.registrar", () -> "two");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,9 +19,12 @@ package org.springframework.boot.testcontainers.context;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.testcontainers.containers.Container;
|
||||
import org.testcontainers.lifecycle.Startable;
|
||||
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.boot.autoconfigure.container.ContainerImageMetadata;
|
||||
@@ -35,12 +38,17 @@ import org.springframework.util.ReflectionUtils;
|
||||
*/
|
||||
class ContainerFieldsImporter {
|
||||
|
||||
void registerBeanDefinitions(BeanDefinitionRegistry registry, Class<?> definitionClass) {
|
||||
Set<Startable> registerBeanDefinitions(BeanDefinitionRegistry registry, Class<?> definitionClass) {
|
||||
Set<Startable> importedContainers = new HashSet<>();
|
||||
for (Field field : getContainerFields(definitionClass)) {
|
||||
assertValid(field);
|
||||
Container<?> container = getContainer(field);
|
||||
if (container instanceof Startable startable) {
|
||||
importedContainers.add(startable);
|
||||
}
|
||||
registerBeanDefinition(registry, field, container);
|
||||
}
|
||||
return importedContainers;
|
||||
}
|
||||
|
||||
private List<Field> getContainerFields(Class<?> containersClass) {
|
||||
|
||||
@@ -19,12 +19,16 @@ package org.springframework.boot.testcontainers.context;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.testcontainers.lifecycle.Startable;
|
||||
|
||||
import org.springframework.beans.factory.config.ConstructorArgumentValues;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.boot.testcontainers.properties.TestcontainersPropertySource;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.core.MethodIntrospector;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.test.context.DynamicPropertyRegistrar;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -32,30 +36,29 @@ import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Used by {@link ImportTestcontainersRegistrar} to import
|
||||
* {@link DynamicPropertySource @DynamicPropertySource} methods.
|
||||
* {@link DynamicPropertySource @DynamicPropertySource} through a
|
||||
* {@link DynamicPropertyRegistrar}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class DynamicPropertySourceMethodsImporter {
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
DynamicPropertySourceMethodsImporter(Environment environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
void registerDynamicPropertySources(BeanDefinitionRegistry beanDefinitionRegistry, Class<?> definitionClass) {
|
||||
void registerDynamicPropertySources(BeanDefinitionRegistry beanDefinitionRegistry, Class<?> definitionClass,
|
||||
Set<Startable> importedContainers) {
|
||||
Set<Method> methods = MethodIntrospector.selectMethods(definitionClass, this::isAnnotated);
|
||||
if (methods.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
DynamicPropertyRegistry dynamicPropertyRegistry = TestcontainersPropertySource.attach(this.environment,
|
||||
beanDefinitionRegistry);
|
||||
methods.forEach((method) -> {
|
||||
assertValid(method);
|
||||
ReflectionUtils.makeAccessible(method);
|
||||
ReflectionUtils.invokeMethod(method, null, dynamicPropertyRegistry);
|
||||
});
|
||||
methods.forEach((method) -> assertValid(method));
|
||||
RootBeanDefinition registrarDefinition = new RootBeanDefinition();
|
||||
registrarDefinition.setBeanClass(DynamicPropertySourcePropertyRegistrar.class);
|
||||
ConstructorArgumentValues arguments = new ConstructorArgumentValues();
|
||||
arguments.addGenericArgumentValue(methods);
|
||||
arguments.addGenericArgumentValue(importedContainers);
|
||||
registrarDefinition.setConstructorArgumentValues(arguments);
|
||||
beanDefinitionRegistry.registerBeanDefinition(definitionClass.getName() + ".dynamicPropertyRegistrar",
|
||||
registrarDefinition);
|
||||
}
|
||||
|
||||
private boolean isAnnotated(Method method) {
|
||||
@@ -71,4 +74,52 @@ class DynamicPropertySourceMethodsImporter {
|
||||
+ "' must accept a single DynamicPropertyRegistry argument");
|
||||
}
|
||||
|
||||
static class DynamicPropertySourcePropertyRegistrar implements DynamicPropertyRegistrar {
|
||||
|
||||
private final Set<Method> methods;
|
||||
|
||||
private final Set<Startable> containers;
|
||||
|
||||
DynamicPropertySourcePropertyRegistrar(Set<Method> methods, Set<Startable> containers) {
|
||||
this.methods = methods;
|
||||
this.containers = containers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(DynamicPropertyRegistry registry) {
|
||||
DynamicPropertyRegistry containersBackedRegistry = new ContainersBackedDynamicPropertyRegistry(registry,
|
||||
this.containers);
|
||||
this.methods.forEach((method) -> {
|
||||
ReflectionUtils.makeAccessible(method);
|
||||
ReflectionUtils.invokeMethod(method, null, containersBackedRegistry);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class ContainersBackedDynamicPropertyRegistry implements DynamicPropertyRegistry {
|
||||
|
||||
private final DynamicPropertyRegistry delegate;
|
||||
|
||||
private final Set<Startable> containers;
|
||||
|
||||
ContainersBackedDynamicPropertyRegistry(DynamicPropertyRegistry delegate, Set<Startable> containers) {
|
||||
this.delegate = delegate;
|
||||
this.containers = containers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(String name, Supplier<Object> valueSupplier) {
|
||||
this.delegate.add(name, () -> {
|
||||
startContainers();
|
||||
return valueSupplier.get();
|
||||
});
|
||||
}
|
||||
|
||||
private void startContainers() {
|
||||
this.containers.forEach(Startable::start);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -24,6 +24,8 @@ import java.lang.annotation.Target;
|
||||
|
||||
import org.testcontainers.containers.Container;
|
||||
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.testcontainers.properties.TestcontainersPropertySourceAutoConfiguration;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
@@ -43,6 +45,7 @@ import org.springframework.context.annotation.Import;
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Import(ImportTestcontainersRegistrar.class)
|
||||
@ImportAutoConfiguration(TestcontainersPropertySourceAutoConfiguration.class)
|
||||
public @interface ImportTestcontainers {
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
|
||||
package org.springframework.boot.testcontainers.context;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.testcontainers.lifecycle.Startable;
|
||||
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
@@ -43,7 +47,7 @@ class ImportTestcontainersRegistrar implements ImportBeanDefinitionRegistrar {
|
||||
ImportTestcontainersRegistrar(Environment environment) {
|
||||
this.containerFieldsImporter = new ContainerFieldsImporter();
|
||||
this.dynamicPropertySourceMethodsImporter = (!ClassUtils.isPresent(DYNAMIC_PROPERTY_SOURCE_CLASS, null)) ? null
|
||||
: new DynamicPropertySourceMethodsImporter(environment);
|
||||
: new DynamicPropertySourceMethodsImporter();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -60,9 +64,11 @@ class ImportTestcontainersRegistrar implements ImportBeanDefinitionRegistrar {
|
||||
|
||||
private void registerBeanDefinitions(BeanDefinitionRegistry registry, Class<?>[] definitionClasses) {
|
||||
for (Class<?> definitionClass : definitionClasses) {
|
||||
this.containerFieldsImporter.registerBeanDefinitions(registry, definitionClass);
|
||||
Set<Startable> importedContainers = this.containerFieldsImporter.registerBeanDefinitions(registry,
|
||||
definitionClass);
|
||||
if (this.dynamicPropertySourceMethodsImporter != null) {
|
||||
this.dynamicPropertySourceMethodsImporter.registerDynamicPropertySources(registry, definitionClass);
|
||||
this.dynamicPropertySourceMethodsImporter.registerDynamicPropertySources(registry, definitionClass,
|
||||
importedContainers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,13 +19,18 @@ package org.springframework.boot.testcontainers.lifecycle;
|
||||
import org.testcontainers.containers.Container;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.test.context.DynamicPropertyRegistrar;
|
||||
|
||||
/**
|
||||
* Event published just before a Testcontainers {@link Container} is used.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 3.2.6
|
||||
* @deprecated since 3.4.0 for removal in 3.6.0 in favor of property registration using a
|
||||
* {@link DynamicPropertyRegistrar} bean that injects the {@link Container} from which the
|
||||
* properties will be sourced.
|
||||
*/
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
public class BeforeTestcontainerUsedEvent extends ApplicationEvent {
|
||||
|
||||
public BeforeTestcontainerUsedEvent(Object source) {
|
||||
|
||||
@@ -58,6 +58,7 @@ import org.springframework.core.log.LogMessage;
|
||||
* @author Scott Frederick
|
||||
* @see TestcontainersLifecycleApplicationContextInitializer
|
||||
*/
|
||||
@SuppressWarnings({ "removal", "deprecation" })
|
||||
@Order(Ordered.LOWEST_PRECEDENCE)
|
||||
class TestcontainersLifecycleBeanPostProcessor
|
||||
implements DestructionAwareBeanPostProcessor, ApplicationListener<BeforeTestcontainerUsedEvent> {
|
||||
@@ -79,6 +80,7 @@ class TestcontainersLifecycleBeanPostProcessor
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
public void onApplicationEvent(BeforeTestcontainerUsedEvent event) {
|
||||
initializeContainers();
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.testcontainers.containers.Container;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
@@ -30,6 +32,8 @@ import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.boot.context.properties.bind.BindResult;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
import org.springframework.boot.testcontainers.lifecycle.BeforeTestcontainerUsedEvent;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.ApplicationEventPublisherAware;
|
||||
@@ -39,6 +43,7 @@ import org.springframework.core.env.EnumerablePropertySource;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.test.context.DynamicPropertyRegistrar;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.function.SupplierUtils;
|
||||
@@ -49,23 +54,31 @@ import org.springframework.util.function.SupplierUtils;
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.1.0
|
||||
* @deprecated since 3.4.0 for removal in 3.6.0 in favor of declaring one or more
|
||||
* {@link DynamicPropertyRegistrar} beans.
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
public class TestcontainersPropertySource extends MapPropertySource {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(TestcontainersPropertySource.class);
|
||||
|
||||
static final String NAME = "testcontainersPropertySource";
|
||||
|
||||
private final DynamicPropertyRegistry registry;
|
||||
|
||||
private final Set<ApplicationEventPublisher> eventPublishers = new CopyOnWriteArraySet<>();
|
||||
|
||||
TestcontainersPropertySource() {
|
||||
this(Collections.synchronizedMap(new LinkedHashMap<>()));
|
||||
TestcontainersPropertySource(DynamicPropertyRegistryInjection registryInjection) {
|
||||
this(Collections.synchronizedMap(new LinkedHashMap<>()), registryInjection);
|
||||
}
|
||||
|
||||
private TestcontainersPropertySource(Map<String, Supplier<Object>> valueSuppliers) {
|
||||
private TestcontainersPropertySource(Map<String, Supplier<Object>> valueSuppliers,
|
||||
DynamicPropertyRegistryInjection registryInjection) {
|
||||
super(NAME, Collections.unmodifiableMap(valueSuppliers));
|
||||
this.registry = (name, valueSupplier) -> {
|
||||
Assert.hasText(name, "'name' must not be null or blank");
|
||||
DynamicPropertyRegistryInjectionException.throwIfNecessary(name, registryInjection);
|
||||
Assert.notNull(valueSupplier, "'valueSupplier' must not be null");
|
||||
valueSuppliers.put(name, valueSupplier);
|
||||
};
|
||||
@@ -117,7 +130,12 @@ public class TestcontainersPropertySource extends MapPropertySource {
|
||||
static TestcontainersPropertySource getOrAdd(ConfigurableEnvironment environment) {
|
||||
PropertySource<?> propertySource = environment.getPropertySources().get(NAME);
|
||||
if (propertySource == null) {
|
||||
environment.getPropertySources().addFirst(new TestcontainersPropertySource());
|
||||
BindResult<DynamicPropertyRegistryInjection> bindingResult = Binder.get(environment)
|
||||
.bind("spring.testcontainers.dynamic-property-registry-injection",
|
||||
DynamicPropertyRegistryInjection.class);
|
||||
environment.getPropertySources()
|
||||
.addFirst(
|
||||
new TestcontainersPropertySource(bindingResult.orElse(DynamicPropertyRegistryInjection.FAIL)));
|
||||
return getOrAdd(environment);
|
||||
}
|
||||
Assert.state(propertySource instanceof TestcontainersPropertySource,
|
||||
@@ -157,4 +175,34 @@ public class TestcontainersPropertySource extends MapPropertySource {
|
||||
|
||||
}
|
||||
|
||||
private enum DynamicPropertyRegistryInjection {
|
||||
|
||||
ALLOW,
|
||||
|
||||
FAIL,
|
||||
|
||||
WARN
|
||||
|
||||
}
|
||||
|
||||
static final class DynamicPropertyRegistryInjectionException extends RuntimeException {
|
||||
|
||||
private DynamicPropertyRegistryInjectionException(String propertyName) {
|
||||
super("Support for injecting a DynamicPropertyRegistry into @Bean methods is deprecated. Register '"
|
||||
+ propertyName + "' using a DynamicPropertyRegistrar bean instead. Alternatively, set "
|
||||
+ "spring.testcontainers.dynamic-property-registry-injection to 'warn' to replace this "
|
||||
+ "failure with a warning or to 'allow' to permit injection of the registry.");
|
||||
}
|
||||
|
||||
private static void throwIfNecessary(String propertyName, DynamicPropertyRegistryInjection registryInjection) {
|
||||
switch (registryInjection) {
|
||||
case FAIL -> throw new DynamicPropertyRegistryInjectionException(propertyName);
|
||||
case WARN -> logger
|
||||
.warn("Support for injecting a DynamicPropertyRegistry into @Bean methods is deprecated. Register '"
|
||||
+ propertyName + "' using a DynamicPropertyRegistrar bean instead.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,19 +16,27 @@
|
||||
|
||||
package org.springframework.boot.testcontainers.properties;
|
||||
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.support.DynamicPropertyRegistrarBeanInitializer;
|
||||
|
||||
/**
|
||||
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||
* Auto-configuration} to add {@link TestcontainersPropertySource} support.
|
||||
* Auto-configuration} to add support for properties sourced from a Testcontainers
|
||||
* {@link GenericContainer container}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @since 3.1.0
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@@ -36,12 +44,18 @@ import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
@ConditionalOnClass(DynamicPropertyRegistry.class)
|
||||
public class TestcontainersPropertySourceAutoConfiguration {
|
||||
|
||||
TestcontainersPropertySourceAutoConfiguration() {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("removal")
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
static DynamicPropertyRegistry dynamicPropertyRegistry(ConfigurableApplicationContext applicationContext) {
|
||||
return TestcontainersPropertySource.attach(applicationContext);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static DynamicPropertyRegistrarBeanInitializer dynamicPropertyRegistrarBeanInitializer() {
|
||||
return new DynamicPropertyRegistrarBeanInitializer();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.stream.Stream;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.testcontainers.containers.Container;
|
||||
import org.testcontainers.lifecycle.Startable;
|
||||
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
@@ -32,10 +33,8 @@ import org.springframework.boot.autoconfigure.service.connection.ConnectionDetai
|
||||
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory;
|
||||
import org.springframework.boot.origin.Origin;
|
||||
import org.springframework.boot.origin.OriginProvider;
|
||||
import org.springframework.boot.testcontainers.lifecycle.BeforeTestcontainerUsedEvent;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler;
|
||||
@@ -164,8 +163,6 @@ public abstract class ContainerConnectionDetailsFactory<C extends Container<?>,
|
||||
|
||||
private final ContainerConnectionSource<C> source;
|
||||
|
||||
private volatile ApplicationEventPublisher eventPublisher;
|
||||
|
||||
private volatile C container;
|
||||
|
||||
/**
|
||||
@@ -190,7 +187,9 @@ public abstract class ContainerConnectionDetailsFactory<C extends Container<?>,
|
||||
protected final C getContainer() {
|
||||
Assert.state(this.container != null,
|
||||
"Container cannot be obtained before the connection details bean has been initialized");
|
||||
this.eventPublisher.publishEvent(new BeforeTestcontainerUsedEvent(this));
|
||||
if (this.container instanceof Startable startable) {
|
||||
startable.start();
|
||||
}
|
||||
return this.container;
|
||||
}
|
||||
|
||||
@@ -200,8 +199,8 @@ public abstract class ContainerConnectionDetailsFactory<C extends Container<?>,
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
this.eventPublisher = applicationContext;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,8 +3,32 @@
|
||||
{
|
||||
"name": "spring.testcontainers.beans.startup",
|
||||
"type": "org.springframework.boot.testcontainers.lifecycle.TestcontainersStartup",
|
||||
"description": "Testcontainers startup modes.",
|
||||
"defaultValue": "sequential"
|
||||
"description": "Testcontainers startup modes.",
|
||||
"defaultValue": "sequential"
|
||||
},
|
||||
{
|
||||
"name": "spring.testcontainers.dynamic-property-registry-injection",
|
||||
"description": "How to treat injection of DynamicPropertyRegistry into a @Bean method.",
|
||||
"defaultValue": "fail"
|
||||
}
|
||||
],
|
||||
"hints": [
|
||||
{
|
||||
"name": "spring.testcontainers.dynamic-property-registry-injection",
|
||||
"values": [
|
||||
{
|
||||
"value": "fail",
|
||||
"description": "Fail with an exception."
|
||||
},
|
||||
{
|
||||
"value": "warn",
|
||||
"description": "Log a warning."
|
||||
},
|
||||
{
|
||||
"value": "allow",
|
||||
"description": "Allow the use despite its deprecation."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -25,10 +25,11 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.boot.testcontainers.lifecycle.BeforeTestcontainerUsedEvent;
|
||||
import org.springframework.boot.testcontainers.properties.TestcontainersPropertySource.EventPublisherRegistrar;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.EnumerablePropertySource;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
@@ -40,10 +41,14 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
* Tests for {@link TestcontainersPropertySource}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @deprecated since 3.4.0 for removal in 3.6.0
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
class TestcontainersPropertySourceTests {
|
||||
|
||||
private MockEnvironment environment = new MockEnvironment();
|
||||
private MockEnvironment environment = new MockEnvironment()
|
||||
.withProperty("spring.testcontainers.dynamic-property-registry-injection", "allow");
|
||||
|
||||
private GenericApplicationContext context = new GenericApplicationContext();
|
||||
|
||||
@@ -121,7 +126,8 @@ class TestcontainersPropertySourceTests {
|
||||
TestcontainersPropertySource.attach(this.environment, this.context);
|
||||
PropertySource<?> propertySource = this.environment.getPropertySources().get(TestcontainersPropertySource.NAME);
|
||||
assertThat(propertySource).isNotNull();
|
||||
assertThat(this.context.containsBean(EventPublisherRegistrar.NAME));
|
||||
assertThat(this.context.containsBean(
|
||||
org.springframework.boot.testcontainers.properties.TestcontainersPropertySource.EventPublisherRegistrar.NAME));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -137,15 +143,19 @@ class TestcontainersPropertySourceTests {
|
||||
@Test
|
||||
void getPropertyPublishesEvent() {
|
||||
try (GenericApplicationContext applicationContext = new GenericApplicationContext()) {
|
||||
ConfigurableEnvironment environment = applicationContext.getEnvironment();
|
||||
environment.getPropertySources()
|
||||
.addLast(new MapPropertySource("test",
|
||||
Map.of("spring.testcontainers.dynamic-property-registry-injection", "allow")));
|
||||
List<ApplicationEvent> events = new ArrayList<>();
|
||||
applicationContext.addApplicationListener(events::add);
|
||||
DynamicPropertyRegistry registry = TestcontainersPropertySource.attach(applicationContext.getEnvironment(),
|
||||
DynamicPropertyRegistry registry = TestcontainersPropertySource.attach(environment,
|
||||
(BeanDefinitionRegistry) applicationContext.getBeanFactory());
|
||||
applicationContext.refresh();
|
||||
registry.add("test", () -> "spring");
|
||||
assertThat(applicationContext.getEnvironment().containsProperty("test")).isTrue();
|
||||
assertThat(environment.containsProperty("test")).isTrue();
|
||||
assertThat(events.isEmpty());
|
||||
assertThat(applicationContext.getEnvironment().getProperty("test")).isEqualTo("spring");
|
||||
assertThat(environment.getProperty("test")).isEqualTo("spring");
|
||||
assertThat(events.stream().filter(BeforeTestcontainerUsedEvent.class::isInstance)).hasSize(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,16 +30,12 @@ import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
|
||||
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
|
||||
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory;
|
||||
import org.springframework.boot.origin.Origin;
|
||||
import org.springframework.boot.testcontainers.lifecycle.BeforeTestcontainerUsedEvent;
|
||||
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactoryTests.TestContainerConnectionDetailsFactory.TestContainerConnectionDetails;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
@@ -135,14 +131,11 @@ class ContainerConnectionDetailsFactoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void getContainerWhenInitializedPublishesEventAndReturnsSuppliedContainer() throws Exception {
|
||||
void getContainerWhenInitializedReturnsSuppliedContainer() throws Exception {
|
||||
TestContainerConnectionDetailsFactory factory = new TestContainerConnectionDetailsFactory();
|
||||
TestContainerConnectionDetails connectionDetails = getConnectionDetails(factory, this.source);
|
||||
ApplicationContext context = mock(ApplicationContext.class);
|
||||
connectionDetails.setApplicationContext(context);
|
||||
connectionDetails.afterPropertiesSet();
|
||||
assertThat(connectionDetails.callGetContainer()).isSameAs(this.container);
|
||||
then(context).should().publishEvent(any(BeforeTestcontainerUsedEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user