Introduce DynamicPropertyRegistrar to replace DynamicPropertyRegistry bean

Spring Boot's testing support registers a DynamicPropertyRegistry as a
bean in the ApplicationContext, which conflicts with the
DynamicPropertyRegistry registered as a bean by the Spring TestContext
Framework (TCF) since Spring Framework 6.2 M2.

To avoid that conflict and to improve the user experience for Spring's
testing support, this commit introduces a DynamicPropertyRegistrar API
to replace the DynamicPropertyRegistry bean support.

Specifically, the TCF no longer registers a DynamicPropertyRegistry as
a bean in the ApplicationContext.

Instead, users can now register custom implementations of
DynamicPropertyRegistrar as beans in the ApplicationContext, and the
DynamicPropertiesContextCustomizer now registers a
DynamicPropertyRegistrarBeanInitializer which eagerly initializes
DynamicPropertyRegistrar beans and invokes their accept() methods with
an appropriate DynamicPropertyRegistry.

In addition, a singleton DynamicValuesPropertySource is created and
registered with the Environment for use in
DynamicPropertiesContextCustomizer and
DynamicPropertyRegistrarBeanInitializer, which allows
@⁠DynamicPropertySource methods and DynamicPropertyRegistrar beans to
transparently populate the same DynamicValuesPropertySource.

Closes gh-33501
This commit is contained in:
Sam Brannen
2024-09-10 16:34:50 +02:00
parent 78028cde05
commit e7b52cf8b2
15 changed files with 429 additions and 301 deletions

View File

@@ -41,7 +41,7 @@ import org.junit.platform.suite.api.Suite;
@Suite
@SelectClasses(
value = {
DynamicPropertyRegistryIntegrationTests.class,
DynamicPropertyRegistrarIntegrationTests.class,
DynamicPropertySourceIntegrationTests.class
},
names = {

View File

@@ -29,60 +29,77 @@ import org.springframework.core.env.MutablePropertySources;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
/**
* Integration tests for {@link DynamicPropertyRegistry} bean support.
* Integration tests for {@link DynamicPropertyRegistrar} bean support.
*
* @author Sam Brannen
* @since 6.2
* @see DynamicPropertySourceIntegrationTests
*/
@SpringJUnitConfig
@TestPropertySource(properties = "api.url: https://example.com/test")
class DynamicPropertyRegistryIntegrationTests {
@TestPropertySource(properties = "api.url.1: https://example.com/test")
class DynamicPropertyRegistrarIntegrationTests {
private static final String API_URL = "api.url";
private static final String API_URL_1 = "api.url.1";
private static final String API_URL_2 = "api.url.2";
@Test
void customDynamicPropertyRegistryCanExistInApplicationContext(
@Autowired DynamicPropertyRegistry dynamicPropertyRegistry) {
assertThatRuntimeException()
.isThrownBy(() -> dynamicPropertyRegistry.add("test", () -> "test"))
.withMessage("Boom!");
}
@Test
void dynamicPropertySourceOverridesTestPropertySource(@Autowired ConfigurableEnvironment env) {
assertApiUrlIsDynamic(env.getProperty(API_URL));
assertApiUrlIsDynamic1(env.getProperty(API_URL_1));
MutablePropertySources propertySources = env.getPropertySources();
assertThat(propertySources.size()).isGreaterThanOrEqualTo(4);
assertThat(propertySources.contains("Inlined Test Properties")).isTrue();
assertThat(propertySources.contains("Dynamic Test Properties")).isTrue();
assertThat(propertySources.get("Inlined Test Properties").getProperty(API_URL)).isEqualTo("https://example.com/test");
assertThat(propertySources.get("Dynamic Test Properties").getProperty(API_URL)).isEqualTo("https://example.com/dynamic");
assertThat(propertySources.get("Inlined Test Properties").getProperty(API_URL_1)).isEqualTo("https://example.com/test");
assertThat(propertySources.get("Dynamic Test Properties").getProperty(API_URL_1)).isEqualTo("https://example.com/dynamic/1");
assertThat(propertySources.get("Dynamic Test Properties").getProperty(API_URL_2)).isEqualTo("https://example.com/dynamic/2");
}
@Test
void testReceivesDynamicProperty(@Value("${api.url}") String apiUrl) {
assertApiUrlIsDynamic(apiUrl);
void testReceivesDynamicProperties(@Value("${api.url.1}") String apiUrl1, @Value("${api.url.2}") String apiUrl2) {
assertApiUrlIsDynamic1(apiUrl1);
assertApiUrlIsDynamic2(apiUrl2);
}
@Test
void environmentInjectedServiceCanRetrieveDynamicProperty(@Autowired EnvironmentInjectedService service) {
assertApiUrlIsDynamic(service);
assertApiUrlIsDynamic1(service);
}
@Test
void constructorInjectedServiceReceivesDynamicProperty(@Autowired ConstructorInjectedService service) {
assertApiUrlIsDynamic(service);
assertApiUrlIsDynamic1(service);
}
@Test
void setterInjectedServiceReceivesDynamicProperty(@Autowired SetterInjectedService service) {
assertApiUrlIsDynamic(service);
assertApiUrlIsDynamic1(service);
}
private static void assertApiUrlIsDynamic(ApiUrlClient service) {
assertApiUrlIsDynamic(service.getApiUrl());
private static void assertApiUrlIsDynamic1(ApiUrlClient service) {
assertApiUrlIsDynamic1(service.getApiUrl());
}
private static void assertApiUrlIsDynamic(String apiUrl) {
assertThat(apiUrl).isEqualTo("https://example.com/dynamic");
private static void assertApiUrlIsDynamic1(String apiUrl) {
assertThat(apiUrl).isEqualTo("https://example.com/dynamic/1");
}
private static void assertApiUrlIsDynamic2(String apiUrl) {
assertThat(apiUrl).isEqualTo("https://example.com/dynamic/2");
}
@@ -90,16 +107,30 @@ class DynamicPropertyRegistryIntegrationTests {
@Import({ EnvironmentInjectedService.class, ConstructorInjectedService.class, SetterInjectedService.class })
static class Config {
// Annotating this @Bean method with @DynamicPropertySource ensures that
// this bean will be instantiated before any other singleton beans in the
@Bean
ApiServer apiServer() {
return new ApiServer();
}
// Accepting ApiServer as a method argument ensures that the apiServer
// bean will be instantiated before any other singleton beans in the
// context which further ensures that the dynamic "api.url" property is
// available to all standard singleton beans.
@Bean
@DynamicPropertySource
ApiServer apiServer(DynamicPropertyRegistry registry) {
ApiServer apiServer = new ApiServer();
registry.add(API_URL, apiServer::getUrl);
return apiServer;
DynamicPropertyRegistrar apiServerProperties1(ApiServer apiServer) {
return registry -> registry.add(API_URL_1, () -> apiServer.getUrl() + "/1");
}
@Bean
DynamicPropertyRegistrar apiServerProperties2(ApiServer apiServer) {
return registry -> registry.add(API_URL_2, () -> apiServer.getUrl() + "/2");
}
@Bean
DynamicPropertyRegistry dynamicPropertyRegistry() {
return (name, valueSupplier) -> {
throw new RuntimeException("Boom!");
};
}
}
@@ -120,7 +151,7 @@ class DynamicPropertyRegistryIntegrationTests {
@Override
public String getApiUrl() {
return this.env.getProperty(API_URL);
return this.env.getProperty(API_URL_1);
}
}
@@ -129,7 +160,7 @@ class DynamicPropertyRegistryIntegrationTests {
private final String apiUrl;
ConstructorInjectedService(@Value("${api.url}") String apiUrl) {
ConstructorInjectedService(@Value("${api.url.1}") String apiUrl) {
this.apiUrl = apiUrl;
}
@@ -145,7 +176,7 @@ class DynamicPropertyRegistryIntegrationTests {
@Autowired
void setApiUrl(@Value("${api.url}") String apiUrl) {
void setApiUrl(@Value("${api.url.1}") String apiUrl) {
this.apiUrl = apiUrl;
}

View File

@@ -23,6 +23,7 @@ import org.junit.jupiter.api.TestInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.ConfigurableEnvironment;
@@ -30,14 +31,16 @@ import org.springframework.core.env.MutablePropertySources;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
/**
* Integration tests for {@link DynamicPropertySource @DynamicPropertySource}.
* Integration tests for {@link DynamicPropertySource @DynamicPropertySource} and
* {@link DynamicPropertyRegistrar} bean support.
*
* @author Phillip Webb
* @author Sam Brannen
* @see DynamicPropertyRegistryIntegrationTests
* @see DynamicPropertyRegistrarIntegrationTests
*/
@SpringJUnitConfig
@TestPropertySource(properties = "test.container.ip: test")
@@ -46,6 +49,7 @@ import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
class DynamicPropertySourceIntegrationTests {
private static final String TEST_CONTAINER_IP = "test.container.ip";
private static final String MAGIC_WORD = "magic.word";
static {
System.setProperty(TEST_CONTAINER_IP, "system");
@@ -54,8 +58,12 @@ class DynamicPropertySourceIntegrationTests {
static final DemoContainer container = new DemoContainer();
@DynamicPropertySource
static void containerProperties(DynamicPropertyRegistry registry) {
static void containerPropertiesIpAddress(DynamicPropertyRegistry registry) {
registry.add(TEST_CONTAINER_IP, container::getIpAddress);
}
@DynamicPropertySource
static void containerPropertiesPort(DynamicPropertyRegistry registry) {
registry.add("test.container.port", container::getPort);
}
@@ -65,6 +73,16 @@ class DynamicPropertySourceIntegrationTests {
System.clearProperty(TEST_CONTAINER_IP);
}
@Test
@DisplayName("A custom DynamicPropertyRegistry bean can exist in the ApplicationContext")
void customDynamicPropertyRegistryCanExistInApplicationContext(
@Autowired DynamicPropertyRegistry dynamicPropertyRegistry) {
assertThatRuntimeException()
.isThrownBy(() -> dynamicPropertyRegistry.add("test", () -> "test"))
.withMessage("Boom!");
}
@Test
@DisplayName("@DynamicPropertySource overrides @TestPropertySource and JVM system property")
void dynamicPropertySourceOverridesTestPropertySourceAndSystemProperty(@Autowired ConfigurableEnvironment env) {
@@ -74,9 +92,11 @@ class DynamicPropertySourceIntegrationTests {
assertThat(propertySources.contains("Inlined Test Properties")).isTrue();
assertThat(propertySources.contains("systemProperties")).isTrue();
assertThat(propertySources.get("Dynamic Test Properties").getProperty(TEST_CONTAINER_IP)).isEqualTo("127.0.0.1");
assertThat(propertySources.get("Dynamic Test Properties").getProperty(MAGIC_WORD)).isEqualTo("enigma");
assertThat(propertySources.get("Inlined Test Properties").getProperty(TEST_CONTAINER_IP)).isEqualTo("test");
assertThat(propertySources.get("systemProperties").getProperty(TEST_CONTAINER_IP)).isEqualTo("system");
assertThat(env.getProperty(TEST_CONTAINER_IP)).isEqualTo("127.0.0.1");
assertThat(env.getProperty(MAGIC_WORD)).isEqualTo("enigma");
}
@Test
@@ -90,6 +110,19 @@ class DynamicPropertySourceIntegrationTests {
@Configuration
@Import(Service.class)
static class Config {
@Bean
DynamicPropertyRegistrar magicWordProperties() {
return registry -> registry.add(MAGIC_WORD, () -> "enigma");
}
@Bean
DynamicPropertyRegistry dynamicPropertyRegistry() {
return (name, valueSupplier) -> {
throw new RuntimeException("Boom!");
};
}
}
static class Service {

View File

@@ -40,7 +40,7 @@ abstract class AbstractAotTests {
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterImportedConfigTests__TestContext001_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterImportedConfigTests__TestContext001_BeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext001_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext001_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext001_BeanDefinitions.java",
// BasicSpringJupiterSharedConfigTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext002_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext002_BeanDefinitions.java",
@@ -51,7 +51,7 @@ abstract class AbstractAotTests {
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext002_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementConfiguration__TestContext002_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementMessageService__TestContext002_ManagementBeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext002_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext002_BeanDefinitions.java",
// BasicSpringJupiterTests -- not generated b/c already generated for BasicSpringJupiterSharedConfigTests.
// BasicSpringJupiterTests.NestedTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext003_BeanDefinitions.java",
@@ -63,28 +63,28 @@ abstract class AbstractAotTests {
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext003_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementConfiguration__TestContext003_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementMessageService__TestContext003_ManagementBeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext003_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext003_BeanDefinitions.java",
// BasicSpringTestNGTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext004_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext004_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests__TestContext004_ApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests__TestContext004_BeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext004_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext004_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext004_BeanDefinitions.java",
// BasicSpringVintageTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext005_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext005_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext005_ApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext005_BeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext005_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext005_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext005_BeanDefinitions.java",
// DisabledInAotRuntimeMethodLevelTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext006_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext006_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/basic/DisabledInAotRuntimeMethodLevelTests__TestContext006_ApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/basic/DisabledInAotRuntimeMethodLevelTests__TestContext006_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/basic/DisabledInAotRuntimeMethodLevelTests__TestContext006_BeanFactoryRegistrations.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext006_BeanDefinitions.java"
"org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext006_BeanDefinitions.java"
};
Stream<Class<?>> scan() {

View File

@@ -395,7 +395,7 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests {
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext001_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementConfiguration__TestContext001_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementMessageService__TestContext001_ManagementBeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext001_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext001_BeanDefinitions.java",
// BasicSpringJupiterTests -- not generated b/c already generated for BasicSpringJupiterSharedConfigTests.
// BasicSpringJupiterTests.NestedTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext002_BeanDefinitions.java",
@@ -407,28 +407,28 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests {
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext002_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementConfiguration__TestContext002_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementMessageService__TestContext002_ManagementBeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext002_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext002_BeanDefinitions.java",
// BasicSpringTestNGTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext003_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext003_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests__TestContext003_ApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests__TestContext003_BeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext003_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext003_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext003_BeanDefinitions.java",
// BasicSpringVintageTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext004_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext004_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext004_ApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext004_BeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext004_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext004_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext004_BeanDefinitions.java",
// SqlScriptsSpringJupiterTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext005_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext005_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/jdbc/SqlScriptsSpringJupiterTests__TestContext005_ApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/jdbc/SqlScriptsSpringJupiterTests__TestContext005_BeanFactoryRegistrations.java",
"org/springframework/test/context/jdbc/EmptyDatabaseConfig__TestContext005_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext005_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext005_BeanDefinitions.java",
// WebSpringJupiterTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext006_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext006_BeanDefinitions.java",
@@ -437,14 +437,14 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests {
"org/springframework/test/context/aot/samples/web/WebTestConfiguration__TestContext006_BeanDefinitions.java",
"org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration__TestContext006_Autowiring.java",
"org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration__TestContext006_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext006_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext006_BeanDefinitions.java",
// XmlSpringJupiterTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext007_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext007_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/common/DefaultMessageService__TestContext007_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/xml/XmlSpringJupiterTests__TestContext007_ApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/xml/XmlSpringJupiterTests__TestContext007_BeanFactoryRegistrations.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext007_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext007_BeanDefinitions.java",
};
}