Skip to content

Commit 6641dbc

Browse files
committed
Discover test config on enclosing classes for nested test classes
Prior to this commit (and since Spring Framework 5.0), Spring's integration with JUnit Jupiter supported detection of test configuration (e.g., @ContextConfiguration, etc.) on @nested classes. However, if a @nested class did not declare its own test configuration, Spring would not find the configuration from the enclosing class. This is in contrast to Spring's support for automatic inheritance of test configuration from superclasses. The only workaround was to copy-n-paste the entire annotation configuration from enclosing classes to nested tests classes, which is cumbersome and error prone. This commit introduces a new @NestedTestConfiguration annotation that allows one to choose the EnclosingConfiguration mode that Spring should use when searching for test configuration on a @nested test class. Currently, the options are INHERIT or OVERRIDE, where the current default is OVERRIDE. Note, however, that the default mode will be changed to INHERIT in a subsequent commit. In addition, support will be added to configure the global default mode via the SpringProperties mechanism in order to allow development teams to revert to the behavior prior to Spring Framework 5.3. As of this commit, inheritance of the following annotations is honored when the EnclosingConfiguration mode is INHERIT. - @ContextConfiguration / @ContextHierarchy - @activeprofiles - @TestPropertySource / @TestPropertySources - @WebAppConfiguration - @Testconstructor - @BootstrapWith - @TestExecutionListeners - @DirtiesContext - @transactional - @Rollback / @Commit This commit does NOT include support for inheriting the following annotations on enclosing classes. - @SQL / @SqlConfig / @SqlGroup In order to implement this feature, the search algorithms in MetaAnnotationUtils (and various other spring-test internals) have been enhanced to detect when annotations should be looked up on enclosing classes. Other parts of the ecosystem may find the new searchEnclosingClass() method in MetaAnnotationUtils useful to provide similar support. As a side effect of the changes in this commit, validation of user configuration in repeated @TestPropertySource declarations has been removed, but this may be reintroduced at a later date. Closes gh-19930
1 parent b9f7b0d commit 6641dbc

30 files changed

+3162
-926
lines changed

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

+30-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 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.
@@ -16,16 +16,18 @@
1616

1717
package org.springframework.test.context;
1818

19+
import java.lang.annotation.Annotation;
1920
import java.lang.reflect.Constructor;
21+
import java.util.LinkedHashSet;
2022
import java.util.Set;
2123

2224
import org.apache.commons.logging.Log;
2325
import org.apache.commons.logging.LogFactory;
2426

2527
import org.springframework.beans.BeanUtils;
26-
import org.springframework.core.annotation.AnnotatedElementUtils;
27-
import org.springframework.core.annotation.AnnotationAttributes;
2828
import org.springframework.lang.Nullable;
29+
import org.springframework.test.util.MetaAnnotationUtils;
30+
import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor;
2931
import org.springframework.util.ClassUtils;
3032

3133
/**
@@ -56,6 +58,8 @@ abstract class BootstrapUtils {
5658
private static final String WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME =
5759
"org.springframework.test.context.web.WebAppConfiguration";
5860

61+
private static final Class<? extends Annotation> webAppConfigurationClass = loadWebAppConfigurationClass();
62+
5963
private static final Log logger = LogFactory.getLog(BootstrapUtils.class);
6064

6165

@@ -149,7 +153,14 @@ static TestContextBootstrapper resolveTestContextBootstrapper(BootstrapContext b
149153

150154
@Nullable
151155
private static Class<?> resolveExplicitTestContextBootstrapper(Class<?> testClass) {
152-
Set<BootstrapWith> annotations = AnnotatedElementUtils.findAllMergedAnnotations(testClass, BootstrapWith.class);
156+
Set<BootstrapWith> annotations = new LinkedHashSet<>();
157+
AnnotationDescriptor<BootstrapWith> descriptor =
158+
MetaAnnotationUtils.findAnnotationDescriptor(testClass, BootstrapWith.class);
159+
while (descriptor != null) {
160+
annotations.addAll(descriptor.findAllLocalMergedAnnotations());
161+
descriptor = descriptor.next();
162+
}
163+
153164
if (annotations.isEmpty()) {
154165
return null;
155166
}
@@ -169,13 +180,22 @@ private static Class<?> resolveExplicitTestContextBootstrapper(Class<?> testClas
169180
}
170181

171182
private static Class<?> resolveDefaultTestContextBootstrapper(Class<?> testClass) throws Exception {
172-
ClassLoader classLoader = BootstrapUtils.class.getClassLoader();
173-
AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(testClass,
174-
WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME, false, false);
175-
if (attributes != null) {
176-
return ClassUtils.forName(DEFAULT_WEB_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME, classLoader);
183+
boolean webApp = (MetaAnnotationUtils.findMergedAnnotation(testClass, webAppConfigurationClass) != null);
184+
String bootstrapperClassName = (webApp ? DEFAULT_WEB_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME :
185+
DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME);
186+
return ClassUtils.forName(bootstrapperClassName, BootstrapUtils.class.getClassLoader());
187+
}
188+
189+
@SuppressWarnings("unchecked")
190+
private static Class<? extends Annotation> loadWebAppConfigurationClass() {
191+
try {
192+
return (Class<? extends Annotation>) ClassUtils.forName(WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME,
193+
BootstrapUtils.class.getClassLoader());
194+
}
195+
catch (ClassNotFoundException | LinkageError ex) {
196+
throw new IllegalStateException(
197+
"Failed to load class for @" + WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME, ex);
177198
}
178-
return ClassUtils.forName(DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME, classLoader);
179199
}
180200

181201
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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.Inherited;
22+
import java.lang.annotation.Retention;
23+
import java.lang.annotation.RetentionPolicy;
24+
import java.lang.annotation.Target;
25+
26+
/**
27+
* {@code @NestedTestConfiguration} is a type-level annotation that is used to
28+
* configure how Spring test configuration annotations are processed within
29+
* enclosing class hierarchies (i.e., for <em>inner</em> test classes).
30+
*
31+
* <p>If {@code @NestedTestConfiguration} is not <em>present</em> or
32+
* <em>meta-present</em> on a test class, configuration from the test class will
33+
* not propagate to inner test classes (see {@link EnclosingConfiguration#OVERRIDE}).
34+
* Consequently, inner test classes will have to declare their own Spring test
35+
* configuration annotations. If you wish for an inner test class to inherit
36+
* configuration from its enclosing class, annotate either the inner test class
37+
* or the enclosing class with
38+
* {@code @NestedTestConfiguration(EnclosingConfiguration.INHERIT)}. Note that
39+
* a {@code @NestedTestConfiguration(...)} declaration is inherited within the
40+
* superclass hierarchy as well as within the enclosing class hierarchy. Thus,
41+
* there is no need to redeclare the annotation unless you wish to switch the
42+
* mode.
43+
*
44+
* <p>This annotation may be used as a <em>meta-annotation</em> to create custom
45+
* <em>composed annotations</em>.
46+
*
47+
* <p>As of Spring Framework 5.3, the use of this annotation typically only makes
48+
* sense in conjunction with {@link org.junit.jupiter.api.Nested @Nested} test
49+
* classes in JUnit Jupiter.
50+
*
51+
* @author Sam Brannen
52+
* @since 5.3
53+
* @see EnclosingConfiguration#INHERIT
54+
* @see EnclosingConfiguration#OVERRIDE
55+
* @see ContextConfiguration @ContextConfiguration
56+
* @see ContextHierarchy @ContextHierarchy
57+
* @see ActiveProfiles @ActiveProfiles
58+
* @see TestPropertySource @TestPropertySource
59+
*/
60+
@Target({ElementType.TYPE, ElementType.METHOD})
61+
@Retention(RetentionPolicy.RUNTIME)
62+
@Documented
63+
@Inherited
64+
public @interface NestedTestConfiguration {
65+
66+
/**
67+
* Configures the {@link EnclosingConfiguration} mode.
68+
*/
69+
EnclosingConfiguration value();
70+
71+
72+
/**
73+
* Enumeration of <em>modes</em> that dictate how test configuration from
74+
* enclosing classes is processed for inner test classes.
75+
*/
76+
enum EnclosingConfiguration {
77+
78+
/**
79+
* Indicates that test configuration for an inner test class should be
80+
* <em>inherited</em> from its {@linkplain Class#getEnclosingClass()
81+
* enclosing class}, as if the enclosing class were a superclass.
82+
*/
83+
INHERIT,
84+
85+
/**
86+
* Indicates that test configuration for an inner test class should
87+
* <em>override</em> configuration from its
88+
* {@linkplain Class#getEnclosingClass() enclosing class}.
89+
*/
90+
OVERRIDE
91+
92+
}
93+
94+
}

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 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.
@@ -29,6 +29,7 @@
2929
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
3030
import org.springframework.test.annotation.DirtiesContext.MethodMode;
3131
import org.springframework.test.context.TestContext;
32+
import org.springframework.test.util.MetaAnnotationUtils;
3233
import org.springframework.util.Assert;
3334

3435
/**
@@ -96,7 +97,7 @@ protected void beforeOrAfterTestMethod(TestContext testContext, MethodMode requi
9697
Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null");
9798

9899
DirtiesContext methodAnn = AnnotatedElementUtils.findMergedAnnotation(testMethod, DirtiesContext.class);
99-
DirtiesContext classAnn = AnnotatedElementUtils.findMergedAnnotation(testClass, DirtiesContext.class);
100+
DirtiesContext classAnn = MetaAnnotationUtils.findMergedAnnotation(testClass, DirtiesContext.class);
100101
boolean methodAnnotated = (methodAnn != null);
101102
boolean classAnnotated = (classAnn != null);
102103
MethodMode methodMode = (methodAnnotated ? methodAnn.methodMode() : null);
@@ -133,7 +134,7 @@ protected void beforeOrAfterTestClass(TestContext testContext, ClassMode require
133134
Class<?> testClass = testContext.getTestClass();
134135
Assert.notNull(testClass, "The test class of the supplied TestContext must not be null");
135136

136-
DirtiesContext dirtiesContext = AnnotatedElementUtils.findMergedAnnotation(testClass, DirtiesContext.class);
137+
DirtiesContext dirtiesContext = MetaAnnotationUtils.findMergedAnnotation(testClass, DirtiesContext.class);
137138
boolean classAnnotated = (dirtiesContext != null);
138139
ClassMode classMode = (classAnnotated ? dirtiesContext.classMode() : null);
139140

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

+5-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 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.
@@ -31,7 +31,6 @@
3131
import org.springframework.beans.BeanInstantiationException;
3232
import org.springframework.beans.BeanUtils;
3333
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
34-
import org.springframework.core.annotation.AnnotationUtils;
3534
import org.springframework.core.io.support.SpringFactoriesLoader;
3635
import org.springframework.lang.Nullable;
3736
import org.springframework.test.context.BootstrapContext;
@@ -139,13 +138,11 @@ public final List<TestExecutionListener> getTestExecutionListeners() {
139138
}
140139

141140
boolean inheritListeners = testExecutionListeners.inheritListeners();
142-
AnnotationDescriptor<TestExecutionListeners> superDescriptor =
143-
MetaAnnotationUtils.findAnnotationDescriptor(
144-
descriptor.getRootDeclaringClass().getSuperclass(), annotationType);
141+
AnnotationDescriptor<TestExecutionListeners> parentDescriptor = descriptor.next();
145142

146143
// If there are no listeners to inherit, we might need to merge the
147144
// locally declared listeners with the defaults.
148-
if ((!inheritListeners || superDescriptor == null) &&
145+
if ((!inheritListeners || parentDescriptor == null) &&
149146
testExecutionListeners.mergeMode() == MergeMode.MERGE_WITH_DEFAULTS) {
150147
if (logger.isDebugEnabled()) {
151148
logger.debug(String.format("Merging default listeners with listeners configured via " +
@@ -157,7 +154,7 @@ public final List<TestExecutionListener> getTestExecutionListeners() {
157154

158155
classesList.addAll(0, Arrays.asList(testExecutionListeners.listeners()));
159156

160-
descriptor = (inheritListeners ? superDescriptor : null);
157+
descriptor = (inheritListeners ? parentDescriptor : null);
161158
}
162159
}
163160

@@ -265,7 +262,7 @@ public final MergedContextConfiguration buildMergedContextConfiguration() {
265262
return buildDefaultMergedContextConfiguration(testClass, cacheAwareContextLoaderDelegate);
266263
}
267264

268-
if (AnnotationUtils.findAnnotation(testClass, ContextHierarchy.class) != null) {
265+
if (MetaAnnotationUtils.findAnnotationDescriptor(testClass, ContextHierarchy.class) != null) {
269266
Map<String, List<ContextConfigurationAttributes>> hierarchyMap =
270267
ContextLoaderUtils.buildContextHierarchyMap(testClass);
271268
MergedContextConfiguration parentConfig = null;

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

+9-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 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.
@@ -28,12 +28,13 @@
2828
import org.springframework.beans.BeanUtils;
2929
import org.springframework.test.context.ActiveProfiles;
3030
import org.springframework.test.context.ActiveProfilesResolver;
31-
import org.springframework.test.util.MetaAnnotationUtils;
3231
import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor;
3332
import org.springframework.util.Assert;
3433
import org.springframework.util.ObjectUtils;
3534
import org.springframework.util.StringUtils;
3635

36+
import static org.springframework.test.util.MetaAnnotationUtils.findAnnotationDescriptor;
37+
3738
/**
3839
* Utility methods for working with {@link ActiveProfiles @ActiveProfiles} and
3940
* {@link ActiveProfilesResolver ActiveProfilesResolvers}.
@@ -70,25 +71,21 @@ abstract class ActiveProfilesUtils {
7071
static String[] resolveActiveProfiles(Class<?> testClass) {
7172
Assert.notNull(testClass, "Class must not be null");
7273

73-
final List<String[]> profileArrays = new ArrayList<>();
74-
75-
Class<ActiveProfiles> annotationType = ActiveProfiles.class;
76-
AnnotationDescriptor<ActiveProfiles> descriptor =
77-
MetaAnnotationUtils.findAnnotationDescriptor(testClass, annotationType);
74+
List<String[]> profileArrays = new ArrayList<>();
75+
AnnotationDescriptor<ActiveProfiles> descriptor = findAnnotationDescriptor(testClass, ActiveProfiles.class);
7876
if (descriptor == null && logger.isDebugEnabled()) {
7977
logger.debug(String.format(
8078
"Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]",
81-
annotationType.getName(), testClass.getName()));
79+
ActiveProfiles.class.getName(), testClass.getName()));
8280
}
8381

8482
while (descriptor != null) {
8583
Class<?> rootDeclaringClass = descriptor.getRootDeclaringClass();
86-
Class<?> declaringClass = descriptor.getDeclaringClass();
8784
ActiveProfiles annotation = descriptor.synthesizeAnnotation();
8885

8986
if (logger.isTraceEnabled()) {
9087
logger.trace(String.format("Retrieved @ActiveProfiles [%s] for declaring class [%s]",
91-
annotation, declaringClass.getName()));
88+
annotation, descriptor.getDeclaringClass().getName()));
9289
}
9390

9491
Class<? extends ActiveProfilesResolver> resolverClass = annotation.resolver();
@@ -112,14 +109,13 @@ static String[] resolveActiveProfiles(Class<?> testClass) {
112109
profileArrays.add(profiles);
113110
}
114111

115-
descriptor = (annotation.inheritProfiles() ? MetaAnnotationUtils.findAnnotationDescriptor(
116-
rootDeclaringClass.getSuperclass(), annotationType) : null);
112+
descriptor = (annotation.inheritProfiles() ? descriptor.next() : null);
117113
}
118114

119115
// Reverse the list so that we can traverse "down" the hierarchy.
120116
Collections.reverse(profileArrays);
121117

122-
final Set<String> activeProfiles = new LinkedHashSet<>();
118+
Set<String> activeProfiles = new LinkedHashSet<>();
123119
for (String[] profiles : profileArrays) {
124120
for (String profile : profiles) {
125121
if (StringUtils.hasText(profile)) {

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

+16-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 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.
@@ -150,8 +150,8 @@ else if (contextHierarchyDeclaredLocally) {
150150
}
151151

152152
hierarchyAttributes.add(0, configAttributesList);
153-
desc = findAnnotationDescriptorForTypes(
154-
rootDeclaringClass.getSuperclass(), contextConfigType, contextHierarchyType);
153+
154+
desc = desc.next();
155155
}
156156

157157
return hierarchyAttributes;
@@ -182,7 +182,7 @@ else if (contextHierarchyDeclaredLocally) {
182182
* @see #resolveContextHierarchyAttributes(Class)
183183
*/
184184
static Map<String, List<ContextConfigurationAttributes>> buildContextHierarchyMap(Class<?> testClass) {
185-
final Map<String, List<ContextConfigurationAttributes>> map = new LinkedHashMap<>();
185+
Map<String, List<ContextConfigurationAttributes>> map = new LinkedHashMap<>();
186186
int hierarchyLevel = 1;
187187

188188
for (List<ContextConfigurationAttributes> configAttributesList : resolveContextHierarchyAttributes(testClass)) {
@@ -237,21 +237,25 @@ static Map<String, List<ContextConfigurationAttributes>> buildContextHierarchyMa
237237
static List<ContextConfigurationAttributes> resolveContextConfigurationAttributes(Class<?> testClass) {
238238
Assert.notNull(testClass, "Class must not be null");
239239

240-
List<ContextConfigurationAttributes> attributesList = new ArrayList<>();
241240
Class<ContextConfiguration> annotationType = ContextConfiguration.class;
242-
243241
AnnotationDescriptor<ContextConfiguration> descriptor = findAnnotationDescriptor(testClass, annotationType);
244242
Assert.notNull(descriptor, () -> String.format(
245243
"Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]",
246244
annotationType.getName(), testClass.getName()));
247245

248-
while (descriptor != null) {
246+
List<ContextConfigurationAttributes> attributesList = new ArrayList<>();
247+
resolveContextConfigurationAttributes(attributesList, descriptor);
248+
return attributesList;
249+
}
250+
251+
private static void resolveContextConfigurationAttributes(List<ContextConfigurationAttributes> attributesList,
252+
AnnotationDescriptor<ContextConfiguration> descriptor) {
253+
254+
if (descriptor != null) {
249255
convertContextConfigToConfigAttributesAndAddToList(descriptor.synthesizeAnnotation(),
250-
descriptor.getRootDeclaringClass(), attributesList);
251-
descriptor = findAnnotationDescriptor(descriptor.getRootDeclaringClass().getSuperclass(), annotationType);
256+
descriptor.getRootDeclaringClass(), attributesList);
257+
resolveContextConfigurationAttributes(attributesList, descriptor.next());
252258
}
253-
254-
return attributesList;
255259
}
256260

257261
/**
@@ -260,7 +264,7 @@ static List<ContextConfigurationAttributes> resolveContextConfigurationAttribute
260264
* declaring class and then adding the attributes to the supplied list.
261265
*/
262266
private static void convertContextConfigToConfigAttributesAndAddToList(ContextConfiguration contextConfiguration,
263-
Class<?> declaringClass, final List<ContextConfigurationAttributes> attributesList) {
267+
Class<?> declaringClass, List<ContextConfigurationAttributes> attributesList) {
264268

265269
if (logger.isTraceEnabled()) {
266270
logger.trace(String.format("Retrieved @ContextConfiguration [%s] for declaring class [%s].",

0 commit comments

Comments
 (0)