Skip to content

Commit cf7daa3

Browse files
sbrannenphilwebb
andcommitted
Add @DynamicPropertySource support in TestContext framework
This commit introduces a @DynamicPropertySource annotation that can be used on methods in test classes that want to add properties to the Environment with a dynamically supplied value. This new feature can be used in conjunction with Testcontainers and other frameworks that manage resources outside the lifecycle of a test's ApplicationContext. Closes gh-24540 Co-authored-by: Phillip Webb <[email protected]>
1 parent 821a8ee commit cf7daa3

11 files changed

+781
-2
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context;
18+
19+
import java.util.function.Supplier;
20+
21+
/**
22+
* Registry used with {@link DynamicPropertySource @DynamicPropertySource}
23+
* methods so that they can add properties to the {@code Environment} that have
24+
* dynamically resolved values.
25+
*
26+
* @author Phillip Webb
27+
* @author Sam Brannen
28+
* @since 5.2.5
29+
* @see DynamicPropertySource
30+
*/
31+
public interface DynamicPropertyRegistry {
32+
33+
/**
34+
* Add a {@link Supplier} for the given property name to this registry.
35+
* @param name the name of the property for which the supplier should be added
36+
* @param valueSupplier a supplier that will provide the property value on demand
37+
*/
38+
void add(String name, Supplier<Object> valueSupplier);
39+
40+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
/**
26+
* Method-level annotation for integration tests that need to add properties with
27+
* dynamic values to the {@code Environment}'s set of {@code PropertySources}.
28+
*
29+
* <p>This annotation and its supporting infrastructure were originally designed
30+
* to allow properties from
31+
* <a href="https://www.testcontainers.org/">Testcontainers</a> based tests to be
32+
* exposed easily to Spring integration tests. However, this feature may also be
33+
* used with any form of external resource whose lifecycle is maintained outside
34+
* the test's {@code ApplicationContext}.
35+
*
36+
* <p>Methods annotated with {@code @DynamicPropertySource} must be {@code static}
37+
* and must have a single {@link DynamicPropertyRegistry} argument which is used
38+
* to add <em>name-value</em> pairs to the {@code Environment}'s set of
39+
* {@code PropertySources}. Values are dynamic and provided via a {@link Supplier}
40+
* which is only invoked when the property is resolved. Typically, method references
41+
* are used to supply values, as in the following example.
42+
*
43+
* <h3>Example</h3>
44+
*
45+
* <pre class="code">
46+
* &#064;SpringJUnitConfig(...)
47+
* &#064;Testcontainers
48+
* class ExampleIntegrationTests {
49+
*
50+
* &#064;Container
51+
* static RedisContainer redis = new RedisContainer();
52+
*
53+
* // ...
54+
*
55+
* &#064;DynamicPropertySource
56+
* static void redisProperties(DynamicPropertyRegistry registry) {
57+
* registry.add("redis.host", redis::getContainerIpAddress);
58+
* registry.add("redis.port", redis::getMappedPort);
59+
* }
60+
*
61+
* }</pre>
62+
*
63+
* @author Phillip Webb
64+
* @author Sam Brannen
65+
* @since 5.2.5
66+
* @see DynamicPropertyRegistry
67+
* @see ContextConfiguration
68+
* @see TestPropertySource
69+
* @see org.springframework.core.env.PropertySource
70+
*/
71+
@Target(ElementType.METHOD)
72+
@Retention(RetentionPolicy.RUNTIME)
73+
@Documented
74+
public @interface DynamicPropertySource {
75+
}

spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -81,6 +81,7 @@
8181
* @author Sam Brannen
8282
* @since 4.1
8383
* @see ContextConfiguration
84+
* @see DynamicPropertySource
8485
* @see org.springframework.core.env.Environment
8586
* @see org.springframework.core.env.PropertySource
8687
* @see org.springframework.context.annotation.PropertySource
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.support;
18+
19+
import java.lang.reflect.Method;
20+
import java.lang.reflect.Modifier;
21+
import java.util.Collections;
22+
import java.util.LinkedHashMap;
23+
import java.util.Map;
24+
import java.util.Set;
25+
import java.util.function.Supplier;
26+
27+
import org.springframework.context.ConfigurableApplicationContext;
28+
import org.springframework.core.env.MutablePropertySources;
29+
import org.springframework.lang.Nullable;
30+
import org.springframework.test.context.ContextCustomizer;
31+
import org.springframework.test.context.DynamicPropertyRegistry;
32+
import org.springframework.test.context.DynamicPropertySource;
33+
import org.springframework.test.context.MergedContextConfiguration;
34+
import org.springframework.util.Assert;
35+
import org.springframework.util.ReflectionUtils;
36+
37+
/**
38+
* {@link ContextCustomizer} to support
39+
* {@link DynamicPropertySource @DynamicPropertySource} methods.
40+
*
41+
* @author Phillip Webb
42+
* @author Sam Brannen
43+
* @since 5.2.5
44+
* @see DynamicPropertiesContextCustomizerFactory
45+
*/
46+
class DynamicPropertiesContextCustomizer implements ContextCustomizer {
47+
48+
private static final String PROPERTY_SOURCE_NAME = "Dynamic Test Properties";
49+
50+
51+
private final Set<Method> methods;
52+
53+
54+
DynamicPropertiesContextCustomizer(Set<Method> methods) {
55+
methods.forEach(this::assertValid);
56+
this.methods = methods;
57+
}
58+
59+
60+
private void assertValid(Method method) {
61+
Assert.state(Modifier.isStatic(method.getModifiers()),
62+
() -> "@DynamicPropertySource method '" + method.getName() + "' must be static");
63+
Class<?>[] types = method.getParameterTypes();
64+
Assert.state(types.length == 1 && types[0] == DynamicPropertyRegistry.class,
65+
() -> "@DynamicPropertySource method '" + method.getName() + "' must accept a single DynamicPropertyRegistry argument");
66+
}
67+
68+
@Override
69+
public void customizeContext(ConfigurableApplicationContext context,
70+
MergedContextConfiguration mergedConfig) {
71+
72+
MutablePropertySources sources = context.getEnvironment().getPropertySources();
73+
sources.addFirst(new DynamicValuesPropertySource(PROPERTY_SOURCE_NAME, buildDynamicPropertiesMap()));
74+
}
75+
76+
@Nullable
77+
private Map<String, Supplier<Object>> buildDynamicPropertiesMap() {
78+
Map<String, Supplier<Object>> map = new LinkedHashMap<>();
79+
DynamicPropertyRegistry dynamicPropertyRegistry = (name, valueSupplier) -> {
80+
Assert.hasText(name, "'name' must not be null or blank");
81+
Assert.notNull(valueSupplier, "'valueSupplier' must not be null");
82+
map.put(name, valueSupplier);
83+
};
84+
this.methods.forEach(method -> {
85+
ReflectionUtils.makeAccessible(method);
86+
ReflectionUtils.invokeMethod(method, null, dynamicPropertyRegistry);
87+
});
88+
return Collections.unmodifiableMap(map);
89+
}
90+
91+
Set<Method> getMethods() {
92+
return this.methods;
93+
}
94+
95+
@Override
96+
public int hashCode() {
97+
return this.methods.hashCode();
98+
}
99+
100+
@Override
101+
public boolean equals(Object obj) {
102+
if (this == obj) {
103+
return true;
104+
}
105+
if (obj == null || getClass() != obj.getClass()) {
106+
return false;
107+
}
108+
return this.methods.equals(((DynamicPropertiesContextCustomizer) obj).methods);
109+
}
110+
111+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.support;
18+
19+
import java.lang.reflect.Method;
20+
import java.util.List;
21+
import java.util.Set;
22+
23+
import org.springframework.core.MethodIntrospector;
24+
import org.springframework.core.annotation.MergedAnnotations;
25+
import org.springframework.lang.Nullable;
26+
import org.springframework.test.context.ContextConfigurationAttributes;
27+
import org.springframework.test.context.ContextCustomizerFactory;
28+
import org.springframework.test.context.DynamicPropertySource;
29+
30+
/**
31+
* {@link ContextCustomizerFactory} to support
32+
* {@link DynamicPropertySource @DynamicPropertySource} methods.
33+
*
34+
* @author Phillip Webb
35+
* @since 5.2.5
36+
* @see DynamicPropertiesContextCustomizer
37+
*/
38+
class DynamicPropertiesContextCustomizerFactory implements ContextCustomizerFactory {
39+
40+
@Override
41+
@Nullable
42+
public DynamicPropertiesContextCustomizer createContextCustomizer(Class<?> testClass,
43+
List<ContextConfigurationAttributes> configAttributes) {
44+
45+
Set<Method> methods = MethodIntrospector.selectMethods(testClass, this::isAnnotated);
46+
if (methods.isEmpty()) {
47+
return null;
48+
}
49+
return new DynamicPropertiesContextCustomizer(methods);
50+
}
51+
52+
private boolean isAnnotated(Method method) {
53+
return MergedAnnotations.from(method).isPresent(DynamicPropertySource.class);
54+
}
55+
56+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.support;
18+
19+
import java.util.Map;
20+
import java.util.function.Supplier;
21+
22+
import org.springframework.core.env.EnumerablePropertySource;
23+
import org.springframework.util.StringUtils;
24+
25+
/**
26+
* {@link EnumerablePropertySource} backed by a map with dynamically supplied
27+
* values.
28+
*
29+
* @author Phillip Webb
30+
* @author Sam Brannen
31+
* @since 5.2.5
32+
*/
33+
class DynamicValuesPropertySource extends EnumerablePropertySource<Map<String, Supplier<Object>>> {
34+
35+
DynamicValuesPropertySource(String name, Map<String, Supplier<Object>> valueSuppliers) {
36+
super(name, valueSuppliers);
37+
}
38+
39+
40+
@Override
41+
public Object getProperty(String name) {
42+
Supplier<Object> valueSupplier = this.source.get(name);
43+
return (valueSupplier != null ? valueSupplier.get() : null);
44+
}
45+
46+
@Override
47+
public boolean containsProperty(String name) {
48+
return this.source.containsKey(name);
49+
}
50+
51+
@Override
52+
public String[] getPropertyNames() {
53+
return StringUtils.toStringArray(this.source.keySet());
54+
}
55+
56+
}

spring-test/src/main/resources/META-INF/spring.factories

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ org.springframework.test.context.TestExecutionListener = \
1212
# Default ContextCustomizerFactory implementations for the Spring TestContext Framework
1313
#
1414
org.springframework.test.context.ContextCustomizerFactory = \
15-
org.springframework.test.context.web.socket.MockServerContainerContextCustomizerFactory
15+
org.springframework.test.context.web.socket.MockServerContainerContextCustomizerFactory,\
16+
org.springframework.test.context.support.DynamicPropertiesContextCustomizerFactory

0 commit comments

Comments
 (0)