diff --git a/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java index 570524371f..0c68666f52 100644 --- a/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java @@ -23,18 +23,29 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; + /** * {@code @NestedTestConfiguration} is a type-level annotation that is used to * configure how Spring test configuration annotations are processed within * enclosing class hierarchies (i.e., for inner test classes). * *

If {@code @NestedTestConfiguration} is not present or + * meta-present on a test class, in its super type hierarchy, or in its + * enclosing class hierarchy, the default enclosing configuration inheritance + * mode will be used. See {@link #ENCLOSING_CONFIGURATION_PROPERTY_NAME} for + * details on how to change the default mode. + * + *

By default, if {@code @NestedTestConfiguration} is not present or * meta-present on a test class, configuration from the test class will * propagate to inner test classes (see {@link EnclosingConfiguration#INHERIT}). * If {@code @NestedTestConfiguration(OVERRIDE)} is used to switch the mode, * inner test classes will have to declare their own Spring test configuration * annotations. If you wish to explicitly configure the mode, annotate either - * the inner test class or the enclosing class with + * the inner test class or an enclosing class with * {@code @NestedTestConfiguration(...}. Note that a * {@code @NestedTestConfiguration(...)} declaration is inherited within the * superclass hierarchy as well as within the enclosing class hierarchy. Thus, @@ -57,14 +68,35 @@ import java.lang.annotation.Target; * @see ActiveProfiles @ActiveProfiles * @see TestPropertySource @TestPropertySource */ -@Target({ElementType.TYPE, ElementType.METHOD}) +@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface NestedTestConfiguration { + /** + * JVM system property used to change the default enclosing configuration + * inheritance mode: {@value #ENCLOSING_CONFIGURATION_PROPERTY_NAME}. + *

Supported values include enum constants defined in + * {@link EnclosingConfiguration}, ignoring case. For example, the default + * may be changed to {@link EnclosingConfiguration#OVERRIDE} by supplying + * the following JVM system property via the command line. + *

-Dspring.test.enclosing.configuration=override
+ *

If the property is not set to {@code OVERRIDE}, test configuration for + * an inner test class will be inherited according to + * {@link EnclosingConfiguration#INHERIT} semantics by default. + *

May alternatively be configured via the + * {@link org.springframework.core.SpringProperties SpringProperties} + * mechanism. + * @see #value + */ + String ENCLOSING_CONFIGURATION_PROPERTY_NAME = "spring.test.enclosing.configuration"; + + /** * Configures the {@link EnclosingConfiguration} mode. + * @see EnclosingConfiguration#INHERIT + * @see EnclosingConfiguration#OVERRIDE */ EnclosingConfiguration value(); @@ -72,6 +104,8 @@ public @interface NestedTestConfiguration { /** * Enumeration of modes that dictate how test configuration from * enclosing classes is processed for inner test classes. + * @see #INHERIT + * @see #OVERRIDE */ enum EnclosingConfiguration { @@ -87,7 +121,34 @@ public @interface NestedTestConfiguration { * override configuration from its * {@linkplain Class#getEnclosingClass() enclosing class}. */ - OVERRIDE + OVERRIDE; + + + /** + * Get the {@code EnclosingConfiguration} enum constant with the supplied + * name, ignoring case. + * @param name the name of the enum constant to retrieve + * @return the corresponding enum constant or {@code null} if not found + * @see EnclosingConfiguration#valueOf(String) + */ + @Nullable + public static EnclosingConfiguration from(@Nullable String name) { + if (name == null) { + return null; + } + try { + return EnclosingConfiguration.valueOf(name.trim().toUpperCase()); + } + catch (IllegalArgumentException ex) { + Log logger = LogFactory.getLog(EnclosingConfiguration.class); + if (logger.isDebugEnabled()) { + logger.debug(String.format( + "Failed to parse enclosing configuration mode from '%s': %s", + name, ex.getMessage())); + } + return null; + } + } } diff --git a/spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java b/spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java index 26aeb565c1..31afd6b2e4 100644 --- a/spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java +++ b/spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java @@ -20,6 +20,7 @@ import java.lang.annotation.Annotation; import java.util.HashSet; import java.util.Set; +import org.springframework.core.SpringProperties; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; @@ -318,12 +319,17 @@ public abstract class MetaAnnotationUtils { } private static EnclosingConfiguration lookUpEnclosingConfiguration(Class clazz) { - // TODO Make the default EnclosingConfiguration mode globally configurable via SpringProperties. return MergedAnnotations.from(clazz, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES) .stream(NestedTestConfiguration.class) .map(mergedAnnotation -> mergedAnnotation.getEnum("value", EnclosingConfiguration.class)) .findFirst() - .orElse(EnclosingConfiguration.INHERIT); + .orElseGet(MetaAnnotationUtils::getDefaultEnclosingConfigurationMode); + } + + private static EnclosingConfiguration getDefaultEnclosingConfigurationMode() { + String value = SpringProperties.getProperty(NestedTestConfiguration.ENCLOSING_CONFIGURATION_PROPERTY_NAME); + EnclosingConfiguration enclosingConfigurationMode = EnclosingConfiguration.from(value); + return (enclosingConfigurationMode != null ? enclosingConfigurationMode : EnclosingConfiguration.INHERIT); } private static void assertNonEmptyAnnotationTypeArray(Class[] annotationTypes, String message) { diff --git a/spring-test/src/test/java/org/springframework/test/util/MetaAnnotationUtilsTests.java b/spring-test/src/test/java/org/springframework/test/util/MetaAnnotationUtilsTests.java index 2deea9a7b6..ea1f512442 100644 --- a/spring-test/src/test/java/org/springframework/test/util/MetaAnnotationUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/util/MetaAnnotationUtilsTests.java @@ -22,22 +22,27 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.springframework.core.SpringProperties; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.NestedTestConfiguration; import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor; import org.springframework.test.util.MetaAnnotationUtils.UntypedAnnotationDescriptor; import org.springframework.transaction.annotation.Transactional; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.OVERRIDE; import static org.springframework.test.util.MetaAnnotationUtils.findAnnotationDescriptor; import static org.springframework.test.util.MetaAnnotationUtils.findAnnotationDescriptorForTypes; +import static org.springframework.test.util.MetaAnnotationUtils.searchEnclosingClass; /** * Unit tests for {@link MetaAnnotationUtils}. @@ -48,6 +53,35 @@ import static org.springframework.test.util.MetaAnnotationUtils.findAnnotationDe */ class MetaAnnotationUtilsTests { + @Nested + @DisplayName("searchEnclosingClass() tests") + class SearchEnclosingClassTests { + + @AfterEach + void clearGlobalFlag() { + setGlobalFlag(null); + } + + @Test + void standardDefaultMode() { + assertThat(searchEnclosingClass(OuterTestCase1.class)).isFalse(); + assertThat(searchEnclosingClass(OuterTestCase1.NestedTestCase.class)).isTrue(); + assertThat(searchEnclosingClass(OuterTestCase1.NestedTestCase.DoubleNestedTestCase.class)).isTrue(); + } + + @Test + void overriddenDefaultMode() { + setGlobalFlag("\t" + OVERRIDE.name().toLowerCase() + " "); + assertThat(searchEnclosingClass(OuterTestCase2.class)).isFalse(); + assertThat(searchEnclosingClass(OuterTestCase2.NestedTestCase.class)).isFalse(); + assertThat(searchEnclosingClass(OuterTestCase2.NestedTestCase.DoubleNestedTestCase.class)).isFalse(); + } + + private void setGlobalFlag(String flag) { + SpringProperties.setProperty(NestedTestConfiguration.ENCLOSING_CONFIGURATION_PROPERTY_NAME, flag); + } + } + @Nested @DisplayName("findAnnotationDescriptor() tests") class FindAnnotationDescriptorTests { @@ -636,4 +670,20 @@ class MetaAnnotationUtilsTests { static class MetaAnnotatedAndSuperAnnotatedContextConfigClass extends AnnotatedContextConfigClass { } + // We need two variants of "OuterTestCase", since the results for searchEnclosingClass() get cached. + static class OuterTestCase1 { + class NestedTestCase { + class DoubleNestedTestCase { + } + } + } + + // We need two variants of "OuterTestCase", since the results for searchEnclosingClass() get cached. + static class OuterTestCase2 { + class NestedTestCase { + class DoubleNestedTestCase { + } + } + } + }