Skip to content

Commit 0819a9f

Browse files
committed
Discover @DynamicPropertySource methods on enclosing test classes
spring-projectsgh-19930 introduced support for finding class-level test configuration annotations on enclosing classes when using JUnit Jupiter @nested test classes, but support for @DynamicPropertySource methods got overlooked since they are method-level annotations. This commit addresses this shortcoming by introducing full support for @NestedTestConfiguration semantics for @DynamicPropertySource methods on enclosing classes. Closes spring-projectsgh-26091
1 parent 0b580d1 commit 0819a9f

File tree

5 files changed

+204
-1
lines changed

5 files changed

+204
-1
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@
4141
* is resolved. Typically, method references are used to supply values, as in the
4242
* example below.
4343
*
44+
* <p>As of Spring Framework 5.3.2, dynamic properties from methods annotated with
45+
* {@code @DynamicPropertySource} will be <em>inherited</em> from enclosing test
46+
* classes, analogous to inheritance from superclasses and interfaces. See
47+
* {@link NestedTestConfiguration @NestedTestConfiguration} for details.
48+
*
4449
* <p><strong>NOTE</strong>: if you use {@code @DynamicPropertySource} in a base
4550
* class and discover that tests in subclasses fail because the dynamic properties
4651
* change between subclasses, you may need to annotate your base class with

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
* <li>{@link org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration}</li>
7575
* <li>{@link ActiveProfiles @ActiveProfiles}</li>
7676
* <li>{@link TestPropertySource @TestPropertySource}</li>
77+
* <li>{@link DynamicPropertySource @DynamicPropertySource}</li>
7778
* <li>{@link org.springframework.test.annotation.DirtiesContext @DirtiesContext}</li>
7879
* <li>{@link org.springframework.transaction.annotation.Transactional @Transactional}</li>
7980
* <li>{@link org.springframework.test.annotation.Rollback @Rollback}</li>

spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.test.context.support;
1818

1919
import java.lang.reflect.Method;
20+
import java.util.LinkedHashSet;
2021
import java.util.List;
2122
import java.util.Set;
2223

@@ -26,12 +27,14 @@
2627
import org.springframework.test.context.ContextConfigurationAttributes;
2728
import org.springframework.test.context.ContextCustomizerFactory;
2829
import org.springframework.test.context.DynamicPropertySource;
30+
import org.springframework.test.context.TestContextAnnotationUtils;
2931

3032
/**
3133
* {@link ContextCustomizerFactory} to support
3234
* {@link DynamicPropertySource @DynamicPropertySource} methods.
3335
*
3436
* @author Phillip Webb
37+
* @author Sam Brannen
3538
* @since 5.2.5
3639
* @see DynamicPropertiesContextCustomizer
3740
*/
@@ -42,13 +45,21 @@ class DynamicPropertiesContextCustomizerFactory implements ContextCustomizerFact
4245
public DynamicPropertiesContextCustomizer createContextCustomizer(Class<?> testClass,
4346
List<ContextConfigurationAttributes> configAttributes) {
4447

45-
Set<Method> methods = MethodIntrospector.selectMethods(testClass, this::isAnnotated);
48+
Set<Method> methods = new LinkedHashSet<>();
49+
findMethods(testClass, methods);
4650
if (methods.isEmpty()) {
4751
return null;
4852
}
4953
return new DynamicPropertiesContextCustomizer(methods);
5054
}
5155

56+
private void findMethods(Class<?> testClass, Set<Method> methods) {
57+
methods.addAll(MethodIntrospector.selectMethods(testClass, this::isAnnotated));
58+
if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) {
59+
findMethods(testClass.getEnclosingClass(), methods);
60+
}
61+
}
62+
5263
private boolean isAnnotated(Method method) {
5364
return MergedAnnotations.from(method).isPresent(DynamicPropertySource.class);
5465
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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.junit.jupiter.nested;
18+
19+
import org.junit.jupiter.api.DisplayName;
20+
import org.junit.jupiter.api.Nested;
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.springframework.beans.factory.annotation.Autowired;
24+
import org.springframework.beans.factory.annotation.Value;
25+
import org.springframework.context.annotation.Configuration;
26+
import org.springframework.context.annotation.Import;
27+
import org.springframework.test.context.DynamicPropertyRegistry;
28+
import org.springframework.test.context.DynamicPropertySource;
29+
import org.springframework.test.context.NestedTestConfiguration;
30+
import org.springframework.test.context.junit.jupiter.SpringExtension;
31+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
32+
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.OVERRIDE;
35+
36+
/**
37+
* Integration tests that verify support for {@code @Nested} test classes using
38+
* {@link DynamicPropertySource @DynamicPropertySource} in conjunction with the
39+
* {@link SpringExtension} in a JUnit Jupiter environment.
40+
*
41+
* @author Sam Brannen
42+
* @since 5.3.2
43+
*/
44+
@SpringJUnitConfig
45+
class DynamicPropertySourceNestedTests {
46+
47+
private static final String TEST_CONTAINER_IP = "DynamicPropertySourceNestedTests.test.container.ip";
48+
49+
private static final String TEST_CONTAINER_PORT = "DynamicPropertySourceNestedTests.test.container.port";
50+
51+
static DemoContainer container = new DemoContainer();
52+
53+
@DynamicPropertySource
54+
static void containerProperties(DynamicPropertyRegistry registry) {
55+
registry.add(TEST_CONTAINER_IP, container::getIpAddress);
56+
registry.add(TEST_CONTAINER_PORT, container::getPort);
57+
}
58+
59+
60+
@Test
61+
@DisplayName("@Service has values injected from @DynamicPropertySource")
62+
void serviceHasInjectedValues(@Autowired Service service) {
63+
assertServiceHasInjectedValues(service);
64+
}
65+
66+
private static void assertServiceHasInjectedValues(Service service) {
67+
assertThat(service.getIp()).isEqualTo("127.0.0.1");
68+
assertThat(service.getPort()).isEqualTo(4242);
69+
}
70+
71+
@Nested
72+
@NestedTestConfiguration(OVERRIDE)
73+
@SpringJUnitConfig(Config.class)
74+
class DynamicPropertySourceFromSuperclassTests extends DynamicPropertySourceSuperclass {
75+
76+
@Test
77+
@DisplayName("@Service has values injected from @DynamicPropertySource in superclass")
78+
void serviceHasInjectedValues(@Autowired Service service) {
79+
assertServiceHasInjectedValues(service);
80+
}
81+
}
82+
83+
@Nested
84+
@NestedTestConfiguration(OVERRIDE)
85+
@SpringJUnitConfig(Config.class)
86+
class DynamicPropertySourceFromInterfaceTests implements DynamicPropertySourceInterface {
87+
88+
@Test
89+
@DisplayName("@Service has values injected from @DynamicPropertySource in interface")
90+
void serviceHasInjectedValues(@Autowired Service service) {
91+
assertServiceHasInjectedValues(service);
92+
}
93+
}
94+
95+
@Nested
96+
@NestedTestConfiguration(OVERRIDE)
97+
@SpringJUnitConfig(Config.class)
98+
class OverriddenConfigTests {
99+
100+
@Test
101+
@DisplayName("@Service does not have values injected from @DynamicPropertySource in enclosing class")
102+
void serviceHasDefaultInjectedValues(@Autowired Service service) {
103+
assertThat(service.getIp()).isEqualTo("10.0.0.1");
104+
assertThat(service.getPort()).isEqualTo(-999);
105+
}
106+
}
107+
108+
@Nested
109+
class DynamicPropertySourceFromEnclosingClassTests {
110+
111+
@Test
112+
@DisplayName("@Service has values injected from @DynamicPropertySource in enclosing class")
113+
void serviceHasInjectedValues(@Autowired Service service) {
114+
assertServiceHasInjectedValues(service);
115+
}
116+
117+
@Nested
118+
class DoubleNestedDynamicPropertySourceFromEnclosingClassTests {
119+
120+
@Test
121+
@DisplayName("@Service has values injected from @DynamicPropertySource in enclosing class")
122+
void serviceHasInjectedValues(@Autowired Service service) {
123+
assertServiceHasInjectedValues(service);
124+
}
125+
}
126+
}
127+
128+
129+
static abstract class DynamicPropertySourceSuperclass {
130+
131+
@DynamicPropertySource
132+
static void containerProperties(DynamicPropertyRegistry registry) {
133+
registry.add(TEST_CONTAINER_IP, container::getIpAddress);
134+
registry.add(TEST_CONTAINER_PORT, container::getPort);
135+
}
136+
}
137+
138+
interface DynamicPropertySourceInterface {
139+
140+
@DynamicPropertySource
141+
static void containerProperties(DynamicPropertyRegistry registry) {
142+
registry.add(TEST_CONTAINER_IP, container::getIpAddress);
143+
registry.add(TEST_CONTAINER_PORT, container::getPort);
144+
}
145+
}
146+
147+
148+
@Configuration
149+
@Import(Service.class)
150+
static class Config {
151+
}
152+
153+
static class Service {
154+
155+
private final String ip;
156+
157+
private final int port;
158+
159+
160+
Service(@Value("${" + TEST_CONTAINER_IP + ":10.0.0.1}") String ip, @Value("${" + TEST_CONTAINER_PORT + ":-999}") int port) {
161+
this.ip = ip;
162+
this.port = port;
163+
}
164+
165+
String getIp() {
166+
return this.ip;
167+
}
168+
169+
int getPort() {
170+
return this.port;
171+
}
172+
}
173+
174+
static class DemoContainer {
175+
176+
String getIpAddress() {
177+
return "127.0.0.1";
178+
}
179+
180+
int getPort() {
181+
return 4242;
182+
}
183+
}
184+
185+
}

src/docs/asciidoc/testing.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1877,6 +1877,7 @@ following annotations.
18771877
* <<spring-testing-annotation-contexthierarchy>>
18781878
* <<spring-testing-annotation-activeprofiles>>
18791879
* <<spring-testing-annotation-testpropertysource>>
1880+
* <<spring-testing-annotation-dynamicpropertysource>>
18801881
* <<spring-testing-annotation-dirtiescontext>>
18811882
* <<spring-testing-annotation-testexecutionlisteners>>
18821883
* <<testcontext-tx,`@Transactional`>>

0 commit comments

Comments
 (0)