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

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -20,6 +20,7 @@ import java.lang.reflect.Method;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.Set; import java.util.Set;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.testcontainers.properties.TestcontainersPropertySource; import org.springframework.boot.testcontainers.properties.TestcontainersPropertySource;
import org.springframework.core.MethodIntrospector; import org.springframework.core.MethodIntrospector;
import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations;
@ -43,16 +44,17 @@ class DynamicPropertySourceMethodsImporter {
this.environment = environment; this.environment = environment;
} }
void registerDynamicPropertySources(Class<?> definitionClass) { void registerDynamicPropertySources(BeanDefinitionRegistry beanDefinitionRegistry, Class<?> definitionClass) {
Set<Method> methods = MethodIntrospector.selectMethods(definitionClass, this::isAnnotated); Set<Method> methods = MethodIntrospector.selectMethods(definitionClass, this::isAnnotated);
if (methods.isEmpty()) { if (methods.isEmpty()) {
return; return;
} }
DynamicPropertyRegistry registry = TestcontainersPropertySource.attach(this.environment); DynamicPropertyRegistry dynamicPropertyRegistry = TestcontainersPropertySource.attach(this.environment,
beanDefinitionRegistry);
methods.forEach((method) -> { methods.forEach((method) -> {
assertValid(method); assertValid(method);
ReflectionUtils.makeAccessible(method); ReflectionUtils.makeAccessible(method);
ReflectionUtils.invokeMethod(method, null, registry); ReflectionUtils.invokeMethod(method, null, dynamicPropertyRegistry);
}); });
} }

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -62,7 +62,7 @@ class ImportTestcontainersRegistrar implements ImportBeanDefinitionRegistrar {
for (Class<?> definitionClass : definitionClasses) { for (Class<?> definitionClass : definitionClasses) {
this.containerFieldsImporter.registerBeanDefinitions(registry, definitionClass); this.containerFieldsImporter.registerBeanDefinitions(registry, definitionClass);
if (this.dynamicPropertySourceMethodsImporter != null) { if (this.dynamicPropertySourceMethodsImporter != null) {
this.dynamicPropertySourceMethodsImporter.registerDynamicPropertySources(definitionClass); this.dynamicPropertySourceMethodsImporter.registerDynamicPropertySources(registry, definitionClass);
} }
} }
} }

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -48,7 +48,10 @@ public class TestcontainersLifecycleApplicationContextInitializer
ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory(); ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
applicationContext.addBeanFactoryPostProcessor(new TestcontainersLifecycleBeanFactoryPostProcessor()); applicationContext.addBeanFactoryPostProcessor(new TestcontainersLifecycleBeanFactoryPostProcessor());
TestcontainersStartup startup = TestcontainersStartup.get(applicationContext.getEnvironment()); TestcontainersStartup startup = TestcontainersStartup.get(applicationContext.getEnvironment());
beanFactory.addBeanPostProcessor(new TestcontainersLifecycleBeanPostProcessor(beanFactory, startup)); TestcontainersLifecycleBeanPostProcessor beanPostProcessor = new TestcontainersLifecycleBeanPostProcessor(
beanFactory, startup);
beanFactory.addBeanPostProcessor(beanPostProcessor);
applicationContext.addApplicationListener(beanPostProcessor);
} }
} }

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -38,6 +38,8 @@ import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor;
import org.springframework.boot.testcontainers.properties.BeforeTestcontainersPropertySuppliedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.core.log.LogMessage; import org.springframework.core.log.LogMessage;
@ -56,7 +58,8 @@ import org.springframework.core.log.LogMessage;
* @see TestcontainersLifecycleApplicationContextInitializer * @see TestcontainersLifecycleApplicationContextInitializer
*/ */
@Order(Ordered.LOWEST_PRECEDENCE) @Order(Ordered.LOWEST_PRECEDENCE)
class TestcontainersLifecycleBeanPostProcessor implements DestructionAwareBeanPostProcessor { class TestcontainersLifecycleBeanPostProcessor
implements DestructionAwareBeanPostProcessor, ApplicationListener<BeforeTestcontainersPropertySuppliedEvent> {
private static final Log logger = LogFactory.getLog(TestcontainersLifecycleBeanPostProcessor.class); private static final Log logger = LogFactory.getLog(TestcontainersLifecycleBeanPostProcessor.class);
@ -74,9 +77,14 @@ class TestcontainersLifecycleBeanPostProcessor implements DestructionAwareBeanPo
this.startup = startup; this.startup = startup;
} }
@Override
public void onApplicationEvent(BeforeTestcontainersPropertySuppliedEvent event) {
initializeContainers();
}
@Override @Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (this.beanFactory.isConfigurationFrozen() && this.containersInitialized.compareAndSet(false, true)) { if (this.beanFactory.isConfigurationFrozen()) {
initializeContainers(); initializeContainers();
} }
if (bean instanceof Startable startableBean) { if (bean instanceof Startable startableBean) {
@ -121,6 +129,7 @@ class TestcontainersLifecycleBeanPostProcessor implements DestructionAwareBeanPo
} }
private void initializeContainers() { private void initializeContainers() {
if (this.containersInitialized.compareAndSet(false, true)) {
logger.trace("Initializing containers"); logger.trace("Initializing containers");
List<String> beanNames = List.of(this.beanFactory.getBeanNamesForType(ContainerState.class, false, false)); List<String> beanNames = List.of(this.beanFactory.getBeanNamesForType(ContainerState.class, false, false));
List<Object> beans = getBeans(beanNames); List<Object> beans = getBeans(beanNames);
@ -132,6 +141,7 @@ class TestcontainersLifecycleBeanPostProcessor implements DestructionAwareBeanPo
this.containersInitialized.set(false); this.containersInitialized.set(false);
} }
} }
}
private List<Object> getBeans(List<String> beanNames) { private List<Object> getBeans(List<String> beanNames) {
List<Object> beans = new ArrayList<>(beanNames.size()); List<Object> beans = new ArrayList<>(beanNames.size());

View File

@ -0,0 +1,47 @@
/*
* 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.properties;
import java.util.function.Supplier;
import org.springframework.context.ApplicationEvent;
/**
* Event published just before the {@link Supplier value supplier} of a
* {@link TestcontainersPropertySource} property is called.
*
* @author Phillip Webb
* @since 3.2.2
*/
public class BeforeTestcontainersPropertySuppliedEvent extends ApplicationEvent {
private final String propertyName;
BeforeTestcontainersPropertySuppliedEvent(TestcontainersPropertySource source, String propertyName) {
super(source);
this.propertyName = propertyName;
}
/**
* Return the name of the property about to be supplied.
* @return the propertyName the property name
*/
public String getPropertyName() {
return this.propertyName;
}
}

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,10 +19,20 @@ package org.springframework.boot.testcontainers.properties;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.testcontainers.containers.Container; import org.testcontainers.containers.Container;
import org.springframework.beans.BeansException;
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.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
@ -44,6 +54,8 @@ public class TestcontainersPropertySource extends EnumerablePropertySource<Map<S
private final DynamicPropertyRegistry registry; private final DynamicPropertyRegistry registry;
private final Set<ApplicationEventPublisher> eventPublishers = new CopyOnWriteArraySet<>();
TestcontainersPropertySource() { TestcontainersPropertySource() {
this(Collections.synchronizedMap(new LinkedHashMap<>())); this(Collections.synchronizedMap(new LinkedHashMap<>()));
} }
@ -57,10 +69,20 @@ public class TestcontainersPropertySource extends EnumerablePropertySource<Map<S
}; };
} }
private void addEventPublisher(ApplicationEventPublisher eventPublisher) {
this.eventPublishers.add(eventPublisher);
}
@Override @Override
public Object getProperty(String name) { public Object getProperty(String name) {
Supplier<Object> valueSupplier = this.source.get(name); Supplier<Object> valueSupplier = this.source.get(name);
return (valueSupplier != null) ? valueSupplier.get() : null; return (valueSupplier != null) ? getProperty(name, valueSupplier) : null;
}
private Object getProperty(String name, Supplier<Object> valueSupplier) {
BeforeTestcontainersPropertySuppliedEvent event = new BeforeTestcontainersPropertySuppliedEvent(this, name);
this.eventPublishers.forEach((eventPublisher) -> eventPublisher.publishEvent(event));
return valueSupplier.get();
} }
@Override @Override
@ -74,20 +96,73 @@ public class TestcontainersPropertySource extends EnumerablePropertySource<Map<S
} }
public static DynamicPropertyRegistry attach(Environment environment) { public static DynamicPropertyRegistry attach(Environment environment) {
Assert.state(environment instanceof ConfigurableEnvironment, return attach(environment, null);
"TestcontainersPropertySource can only be attached to a ConfigurableEnvironment");
return attach((ConfigurableEnvironment) environment);
} }
private static DynamicPropertyRegistry attach(ConfigurableEnvironment environment) { static DynamicPropertyRegistry attach(ConfigurableApplicationContext applicationContext) {
return attach(applicationContext.getEnvironment(), applicationContext, null);
}
public static DynamicPropertyRegistry attach(Environment environment, BeanDefinitionRegistry registry) {
return attach(environment, null, registry);
}
private static DynamicPropertyRegistry attach(Environment environment, ApplicationEventPublisher eventPublisher,
BeanDefinitionRegistry registry) {
Assert.state(environment instanceof ConfigurableEnvironment,
"TestcontainersPropertySource can only be attached to a ConfigurableEnvironment");
TestcontainersPropertySource propertySource = getOrAdd((ConfigurableEnvironment) environment);
if (eventPublisher != null) {
propertySource.addEventPublisher(eventPublisher);
}
else if (registry != null) {
registry.registerBeanDefinition(EventPublisherRegistrar.NAME, new RootBeanDefinition(
EventPublisherRegistrar.class, () -> new EventPublisherRegistrar(environment)));
}
return propertySource.registry;
}
static TestcontainersPropertySource getOrAdd(ConfigurableEnvironment environment) {
PropertySource<?> propertySource = environment.getPropertySources().get(NAME); PropertySource<?> propertySource = environment.getPropertySources().get(NAME);
if (propertySource == null) { if (propertySource == null) {
environment.getPropertySources().addFirst(new TestcontainersPropertySource()); environment.getPropertySources().addFirst(new TestcontainersPropertySource());
return attach(environment); return getOrAdd(environment);
} }
Assert.state(propertySource instanceof TestcontainersPropertySource, Assert.state(propertySource instanceof TestcontainersPropertySource,
"Incorrect DynamicValuesPropertySource type registered"); "Incorrect DynamicValuesPropertySource type registered");
return ((TestcontainersPropertySource) propertySource).registry; return ((TestcontainersPropertySource) propertySource);
}
/**
* {@link BeanFactoryPostProcessor} to register the {@link ApplicationEventPublisher}
* to the {@link TestcontainersPropertySource}. This class is a
* {@link BeanFactoryPostProcessor} so that it is initialized as early as possible.
*/
private static class EventPublisherRegistrar implements BeanFactoryPostProcessor, ApplicationEventPublisherAware {
static final String NAME = EventPublisherRegistrar.class.getName();
private final Environment environment;
private ApplicationEventPublisher eventPublisher;
EventPublisherRegistrar(Environment environment) {
this.environment = environment;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
if (this.eventPublisher != null) {
TestcontainersPropertySource.getOrAdd((ConfigurableEnvironment) this.environment)
.addEventPublisher(this.eventPublisher);
}
}
} }
} }

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,9 +16,12 @@
package org.springframework.boot.testcontainers.properties; package org.springframework.boot.testcontainers.properties;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertyRegistry;
/** /**
@ -28,6 +31,8 @@ import org.springframework.test.context.DynamicPropertyRegistry;
* @author Phillip Webb * @author Phillip Webb
* @since 3.1.0 * @since 3.1.0
*/ */
@AutoConfiguration
@Order(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(DynamicPropertyRegistry.class) @ConditionalOnClass(DynamicPropertyRegistry.class)
public class TestcontainersPropertySourceAutoConfiguration { public class TestcontainersPropertySourceAutoConfiguration {
@ -35,8 +40,8 @@ public class TestcontainersPropertySourceAutoConfiguration {
} }
@Bean @Bean
DynamicPropertyRegistry dynamicPropertyRegistry(ConfigurableEnvironment environment) { static DynamicPropertyRegistry dynamicPropertyRegistry(ConfigurableApplicationContext applicationContext) {
return TestcontainersPropertySource.attach(environment); return TestcontainersPropertySource.attach(applicationContext);
} }
} }

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,9 @@
package org.springframework.boot.testcontainers.properties; package org.springframework.boot.testcontainers.properties;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations; 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.testcontainers.lifecycle.TestcontainersLifecycleApplicationContextInitializer;
import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable; import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable;
import org.springframework.boot.testsupport.testcontainers.RedisContainer; import org.springframework.boot.testsupport.testcontainers.RedisContainer;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
@ -46,10 +50,15 @@ class TestcontainersPropertySourceAutoConfigurationTests {
@Test @Test
void containerBeanMethodContributesProperties() { void containerBeanMethodContributesProperties() {
this.contextRunner.withUserConfiguration(ContainerAndPropertiesConfiguration.class).run((context) -> { List<ApplicationEvent> events = new ArrayList<>();
this.contextRunner.withUserConfiguration(ContainerAndPropertiesConfiguration.class)
.withInitializer((context) -> context.addApplicationListener(events::add))
.run((context) -> {
TestBean testBean = context.getBean(TestBean.class); TestBean testBean = context.getBean(TestBean.class);
RedisContainer redisContainer = context.getBean(RedisContainer.class); RedisContainer redisContainer = context.getBean(RedisContainer.class);
assertThat(testBean.getUsingPort()).isEqualTo(redisContainer.getFirstMappedPort()); assertThat(testBean.getUsingPort()).isEqualTo(redisContainer.getFirstMappedPort());
assertThat(events.stream().filter(BeforeTestcontainersPropertySuppliedEvent.class::isInstance))
.hasSize(1);
}); });
} }

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,10 +16,15 @@
package org.springframework.boot.testcontainers.properties; package org.springframework.boot.testcontainers.properties;
import java.util.ArrayList;
import java.util.List;
import java.util.Map; import java.util.Map;
import org.junit.jupiter.api.Test; 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.EnumerablePropertySource;
import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySource;
import org.springframework.mock.env.MockEnvironment; import org.springframework.mock.env.MockEnvironment;
@ -101,4 +106,20 @@ class TestcontainersPropertySourceTests {
assertThat(p1).isSameAs(p2); 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);
}
}
} }