Skip to content

Commit

Permalink
Make default @NestedTestConfiguration mode configurable
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sbrannen committed Oct 12, 2020
1 parent 7f36594 commit 0af09e0
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,29 @@
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 <em>inner</em> test classes).
*
* <p>If {@code @NestedTestConfiguration} is not <em>present</em> or
* <em>meta-present</em> on a test class, in its super type hierarchy, or in its
* enclosing class hierarchy, the default <em>enclosing configuration inheritance
* mode</em> will be used. See {@link #ENCLOSING_CONFIGURATION_PROPERTY_NAME} for
* details on how to change the default mode.
*
* <p>By default, if {@code @NestedTestConfiguration} is not <em>present</em> or
* <em>meta-present</em> 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,
Expand All @@ -57,21 +68,44 @@
* @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 <em>enclosing configuration
* inheritance mode</em>: {@value #ENCLOSING_CONFIGURATION_PROPERTY_NAME}.
* <p>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.
* <pre style="code">-Dspring.test.enclosing.configuration=override</pre>
* <p>If the property is not set to {@code OVERRIDE}, test configuration for
* an inner test class will be <em>inherited</em> according to
* {@link EnclosingConfiguration#INHERIT} semantics by default.
* <p>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();


/**
* Enumeration of <em>modes</em> that dictate how test configuration from
* enclosing classes is processed for inner test classes.
* @see #INHERIT
* @see #OVERRIDE
*/
enum EnclosingConfiguration {

Expand All @@ -87,7 +121,34 @@ enum EnclosingConfiguration {
* <em>override</em> 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;
}
}

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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;
Expand Down Expand Up @@ -318,12 +319,17 @@ private static EnclosingConfiguration getEnclosingConfiguration(Class<?> clazz)
}

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,27 @@
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}.
Expand All @@ -48,6 +53,35 @@
*/
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 {
Expand Down Expand Up @@ -636,4 +670,20 @@ static class AnnotatedContextConfigClass {
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 {
}
}
}

}

0 comments on commit 0af09e0

Please sign in to comment.