Prefer DynamicPropertyRegistar to DynamicPropertyRegistry

Closes gh-41996
This commit is contained in:
Andy Wilkinson
2024-09-25 13:54:43 +01:00
parent fdc4a51e24
commit 3c095b4ec2
18 changed files with 341 additions and 77 deletions

View File

@@ -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

View File

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

View File

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

View File

@@ -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) {

View File

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

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.
@@ -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 {
/**

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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."
}
]
}
]
}

View File

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

View File

@@ -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