Skip to content

Commit

Permalink
Register DynamicPropertyRegistry as singleton bean in test Applicatio…
Browse files Browse the repository at this point in the history
…nContext

Prior to this commit, DynamicPropertyRegistry could only be used with a
static @⁠DynamicPropertySource method in an integration test class;
however, it can also be useful to be able to register a "dynamic
property" from within a test's ApplicationContext -- for example, in a
@⁠Bean method in a @⁠Configuration class that is specific to testing
scenarios.

To support such use cases, this commit updates the dynamic property
source infrastructure so that a DynamicPropertyRegistry is always
registered as a singleton bean in a test's ApplicationContext. This
allows DynamicPropertyRegistry to be autowired into a @⁠Configuration
class or supplied to a @⁠Bean method as an argument as shown in the
following example.

@⁠Bean
@⁠DynamicPropertySource
ApiServer apiServer(DynamicPropertyRegistry registry) {
	ApiServer apiServer = new ApiServer();
	registry.add("api.url", apiServer::getUrl);
	return apiServer;
}

Note that the use of @⁠DynamicPropertySource on the @⁠Bean method is
optional and results in the corresponding bean being eagerly
initialized so that other singleton beans in the context can be given
access to the dynamic properties sourced from that bean when those
other beans are initialized.

Side note: DynamicPropertySourceBeanInitializer temporarily implements
LoadTimeWeaverAware since doing so is currently the only way to have a
component eagerly initialized before the
ConfigurableListableBeanFactory.preInstantiateSingletons() phase.
However, we plan to introduce a first-class callback to support such
use cases in the future.

Closes gh-32271
  • Loading branch information
sbrannen committed May 15, 2024
1 parent 7d8279a commit 6cdb344
Show file tree
Hide file tree
Showing 15 changed files with 507 additions and 89 deletions.
Original file line number Diff line number Diff line change
@@ -1,43 +1,58 @@
[[testcontext-ctx-management-dynamic-property-sources]]
= Context Configuration with Dynamic Property Sources

As of Spring Framework 5.2.5, the TestContext framework provides support for _dynamic_
properties via the `@DynamicPropertySource` annotation. This annotation can be used in
integration tests that need to add properties with dynamic values to the set of
`PropertySources` in the `Environment` for the `ApplicationContext` loaded for the
integration test.
The Spring TestContext Framework provides support for _dynamic_ properties via the
`@DynamicPropertySource` annotation and the `DynamicPropertyRegistry`.

[NOTE]
====
The `@DynamicPropertySource` annotation and its supporting infrastructure were
originally designed to allow properties from
{testcontainers-site}[Testcontainers] based tests to be exposed easily to
Spring integration tests. However, this feature may also be used with any form of
external resource whose lifecycle is maintained outside the test's `ApplicationContext`.
The `@DynamicPropertySource` annotation and its supporting infrastructure were originally
designed to allow properties from {testcontainers-site}[Testcontainers] based tests to be
exposed easily to Spring integration tests. However, this feature may be used with any
form of external resource whose lifecycle is managed outside the test's
`ApplicationContext` or with beans whose lifecycle is managed by the test's
`ApplicationContext`.
====

In contrast to the xref:testing/testcontext-framework/ctx-management/property-sources.adoc[`@TestPropertySource`]
annotation that is applied at the class level, `@DynamicPropertySource` must be applied
to a `static` method that accepts a single `DynamicPropertyRegistry` argument which is
used to add _name-value_ pairs to the `Environment`. Values are dynamic and provided via
a `Supplier` which is only invoked when the property is resolved. Typically, method
references are used to supply values, as can be seen in the following example which uses
the Testcontainers project to manage a Redis container outside of the Spring
`ApplicationContext`. The IP address and port of the managed Redis container are made
available to components within the test's `ApplicationContext` via the `redis.host` and
`redis.port` properties. These properties can be accessed via Spring's `Environment`
abstraction or injected directly into Spring-managed components – for example, via
`@Value("${redis.host}")` and `@Value("${redis.port}")`, respectively.
In contrast to the
xref:testing/testcontext-framework/ctx-management/property-sources.adoc[`@TestPropertySource`]
annotation that is applied at the class level, `@DynamicPropertySource` can be applied to
`static` methods in integration test classes or to `@Bean` methods in test
`@Configuration` classes in order to add properties with dynamic values to the set of
`PropertySources` in the `Environment` for the `ApplicationContext` loaded for the
integration test.

A `DynamicPropertyRegistry` is used to add _name-value_ pairs to the `Environment`.
Values are dynamic and provided via a `Supplier` which is only invoked when the property
is resolved. Typically, method references are used to supply values.

Methods in integration test classes that are annotated with `@DynamicPropertySource` must
be `static` and must accept a single `DynamicPropertyRegistry` argument.

`@Bean` methods annotated with `@DynamicPropertySource` may either accept an argument of
type `DynamicPropertyRegistry` or access a `DynamicPropertyRegistry` instance autowired
into their enclosing `@Configuration` class. Note, however, that `@Bean` methods which
interact with a `DynamicPropertyRegistry` are not required to be annotated with
`@DynamicPropertySource` unless they need to enforce eager initialization of the bean
within the context. See the class-level javadoc for `DynamicPropertyRegistry` for details.

[TIP]
====
If you use `@DynamicPropertySource` in a base class and discover that tests in subclasses
fail because the dynamic properties change between subclasses, you may need to annotate
your base class with xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[`@DirtiesContext`] to
ensure that each subclass gets its own `ApplicationContext` with the correct dynamic
your base class with
xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[`@DirtiesContext`]
to ensure that each subclass gets its own `ApplicationContext` with the correct dynamic
properties.
====

The following example uses the Testcontainers project to manage a Redis container outside
of the Spring `ApplicationContext`. The IP address and port of the managed Redis
container are made available to components within the test's `ApplicationContext` via the
`redis.host` and `redis.port` properties. These properties can be accessed via Spring's
`Environment` abstraction or injected directly into Spring-managed components – for
example, via `@Value("${redis.host}")` and `@Value("${redis.port}")`, respectively.

[tabs]
======
Java::
Expand Down Expand Up @@ -92,7 +107,55 @@ Kotlin::
----
======

[[precedence]]
The following example demonstrates how to use `DynamicPropertyRegistry` and
`@DynamicPropertySource` with a `@Bean` method. The `api.url` property can be accessed
via Spring's `Environment` abstraction or injected directly into other Spring-managed
components – for example, via `@Value("${api.url}")`. The value of the `api.url` property
will be dynamically retrieved from the `ApiServer` bean.

[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
@Configuration
class TestConfig {
@Bean
@DynamicPropertySource
ApiServer apiServer(DynamicPropertyRegistry registry) {
ApiServer apiServer = new ApiServer();
registry.add("api.url", apiServer::getUrl);
return apiServer;
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
@Configuration
class TestConfig {
@Bean
@DynamicPropertySource
fun apiServer(registry: DynamicPropertyRegistry): ApiServer {
val apiServer = ApiServer()
registry.add("api.url", apiServer::getUrl)
return apiServer
}
}
----
======

NOTE: The use of `@DynamicPropertySource` on the `@Bean` method is optional and results
in the `ApiServer` bean being eagerly initialized so that other beans in the context can
be given access to the dynamic properties sourced from the `ApiServer` bean when those
other beans are initialized.

[[testcontext-ctx-management-dynamic-property-sources-precedence]]
== Precedence

Dynamic properties have higher precedence than those loaded from `@TestPropertySource`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,6 @@ meta-present `@TestPropertySource` annotations. In other words, `locations` and
meta-annotation.
====


[[default-properties-file-detection]]
== Default Properties File Detection

Expand All @@ -195,7 +194,7 @@ if the annotated test class is `com.example.MyTest`, the corresponding default p
file is `classpath:com/example/MyTest.properties`. If the default cannot be detected, an
`IllegalStateException` is thrown.

[[precedence]]
[[testcontext-ctx-management-property-sources-precedence]]
== Precedence

Test properties have higher precedence than those defined in the operating system's
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-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.
Expand All @@ -19,9 +19,25 @@
import java.util.function.Supplier;

/**
* Registry used with {@link DynamicPropertySource @DynamicPropertySource}
* methods so that they can add properties to the {@code Environment} that have
* dynamically resolved values.
* Registry that is used to add properties with dynamically resolved values to
* the {@code Environment}.
*
* <p>A {@code DynamicPropertyRegistry} is supplied as an argument to static
* {@link DynamicPropertySource @DynamicPropertySource} methods in integration
* test classes.
*
* <p>As of Spring Framework 6.2, a {@code DynamicPropertyRegistry} is also
* registered as a singleton bean in the test's {@code ApplicationContext}. This
* allows a {@code DynamicPropertyRegistry} to be autowired into a
* {@code @Configuration} class or supplied to a {@code @Bean} method as an
* argument, making it possible to register a dynamic property from within a test's
* {@code ApplicationContext}. For example, a {@code @Bean} method can register
* a property whose value is dynamically sourced from the bean that the method
* returns. Note that such a {@code @Bean} method can optionally be annotated
* with {@code @DynamicPropertySource} to enforce eager initialization of the
* bean within the context, thereby ensuring that any dynamic properties sourced
* from that bean are available to other singleton beans within the context.
* See {@link DynamicPropertySource @DynamicPropertySource} for an example.
*
* @author Phillip Webb
* @author Sam Brannen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,43 @@
import java.lang.annotation.Target;

/**
* {@code @DynamicPropertySource} is an annotation that can be applied to methods
* in integration test classes that need to add properties with dynamic values to
* the {@code Environment}'s set of {@code PropertySources}.
* {@code @DynamicPropertySource} is an annotation that can be applied to static
* methods in integration test classes or to {@code @Bean} methods in test
* {@code @Configuration} classes in order to add properties with dynamic values
* to the {@code Environment}'s set of {@code PropertySources}.
*
* <p>This annotation and its supporting infrastructure were originally designed
* to allow properties from
* <a href="https://www.testcontainers.org/">Testcontainers</a> based tests to be
* exposed easily to Spring integration tests. However, this feature may also be
* used with any form of external resource whose lifecycle is maintained outside
* exposed easily to Spring integration tests. However, this feature may be used
* with any form of external resource whose lifecycle is managed outside the
* test's {@code ApplicationContext} or with beans whose lifecycle is managed by
* the test's {@code ApplicationContext}.
*
* <p>Methods annotated with {@code @DynamicPropertySource} must be {@code static}
* and must have a single {@link DynamicPropertyRegistry} argument which is used
* to add <em>name-value</em> pairs to the {@code Environment}'s set of
* {@code PropertySources}. Values are dynamic and provided via a
* {@link java.util.function.Supplier} which is only invoked when the property
* is resolved. Typically, method references are used to supply values, as in the
* example below.
* <p>{@code @DynamicPropertySource}-annotated methods use a
* {@code DynamicPropertyRegistry} to add <em>name-value</em> pairs to the
* {@code Environment}'s set of {@code PropertySources}. Values are dynamic and
* provided via a {@link java.util.function.Supplier} which is only invoked when
* the property is resolved. Typically, method references are used to supply values,
* as in the example below.
*
* <p>As of Spring Framework 5.3.2, dynamic properties from methods annotated with
* {@code @DynamicPropertySource} will be <em>inherited</em> from enclosing test
* classes, analogous to inheritance from superclasses and interfaces. See
* {@link NestedTestConfiguration @NestedTestConfiguration} for details.
* <p>Methods in integration test classes that are annotated with
* {@code @DynamicPropertySource} must be {@code static} and must accept a single
* {@link DynamicPropertyRegistry} argument.
*
* <p>{@code @Bean} methods annotated with {@code @DynamicPropertySource} may
* either accept an argument of type {@code DynamicPropertyRegistry} or access a
* {@code DynamicPropertyRegistry} instance autowired into their enclosing
* {@code @Configuration} class. Note, however, that {@code @Bean} methods which
* interact with a {@code DynamicPropertyRegistry} are not required to be annotated
* with {@code @DynamicPropertySource} unless they need to enforce eager
* initialization of the bean within the context.
* See {@link DynamicPropertyRegistry} for details.
*
* <p>Dynamic properties from methods annotated with {@code @DynamicPropertySource}
* will be <em>inherited</em> from enclosing test classes, analogous to inheritance
* from superclasses and interfaces.
* See {@link NestedTestConfiguration @NestedTestConfiguration} for details.
*
* <p><strong>NOTE</strong>: if you use {@code @DynamicPropertySource} in a base
* class and discover that tests in subclasses fail because the dynamic properties
Expand All @@ -64,7 +78,13 @@
* override properties loaded via {@code @TestPropertySource}, system property
* sources, and application property sources.
*
* <h3>Example</h3>
* <h3>Examples</h3>
*
* <p>The following example demonstrates how to use {@code @DynamicPropertySource}
* in an integration test class. Beans in the {@code ApplicationContext} can
* access the {@code redis.host} and {@code redis.port} properties which are
* dynamically retrieved from the Redis container.
*
* <pre class="code">
* &#064;SpringJUnitConfig(...)
* &#064;Testcontainers
Expand All @@ -81,7 +101,24 @@
* registry.add("redis.host", redis::getHost);
* registry.add("redis.port", redis::getFirstMappedPort);
* }
* }</pre>
*
* <p>The following example demonstrates how to use {@code @DynamicPropertySource}
* with a {@code @Bean} method. Beans in the {@code ApplicationContext} can
* access the {@code api.url} property which is dynamically retrieved from the
* {@code ApiServer} bean.
*
* <pre class="code">
* &#064;Configuration
* class TestConfig {
*
* &#064;Bean
* &#064;DynamicPropertySource
* ApiServer apiServer(DynamicPropertyRegistry registry) {
* ApiServer apiServer = new ApiServer();
* registry.add("api.url", apiServer::getUrl);
* return apiServer;
* }
* }</pre>
*
* @author Phillip Webb
Expand Down

0 comments on commit 6cdb344

Please sign in to comment.