From 0af09e076b61d7f63dde9df8a033599648d22248 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 13 Oct 2020 00:09:49 +0200 Subject: [PATCH] Make default @NestedTestConfiguration mode configurable Prior to this commit, the EnclosingConfiguration mode used in conjunction with @NestedTestConfiguration defaulted to INHERIT. In other to allow development teams to change the default to OVERRIDE (e.g., for compatibility with Spring Framework 5.0 through 5.2), this commit introduces support for changing the default EnclosingConfiguration mode globally via a JVM system property or via the SpringProperties mechanism. For example, the default may be changed to EnclosingConfiguration.OVERRIDE by supplying the following JVM system property via the command line. -Dspring.test.enclosing.configuration=override Closes gh-19930 --- .../test/context/NestedTestConfiguration.java | 67 ++++++++++++++++++- .../test/util/MetaAnnotationUtils.java | 10 ++- .../test/util/MetaAnnotationUtilsTests.java | 50 ++++++++++++++ 3 files changed, 122 insertions(+), 5 deletions(-) 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 { + } + } + } + }