From 6641dbc852dddd46276820501cda0cac5e519eaa Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 20 May 2019 18:59:07 +0200 Subject: [PATCH] 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 --- .../test/context/BootstrapUtils.java | 40 +- .../test/context/NestedTestConfiguration.java | 94 ++ ...ctDirtiesContextTestExecutionListener.java | 7 +- .../AbstractTestContextBootstrapper.java | 13 +- .../context/support/ActiveProfilesUtils.java | 22 +- .../context/support/ContextLoaderUtils.java | 30 +- .../support/MergedTestPropertySources.java | 57 +- .../context/support/TestConstructorUtils.java | 3 +- .../support/TestPropertySourceAttributes.java | 87 +- .../support/TestPropertySourceUtils.java | 97 ++- .../TransactionalTestExecutionListener.java | 27 +- .../web/WebTestContextBootstrapper.java | 13 +- .../test/util/MetaAnnotationUtils.java | 249 +++++- .../test/context/BootstrapUtilsTests.java | 115 ++- .../nested/ActiveProfilesNestedTests.java | 211 +++++ ...a => ConstructorInjectionNestedTests.java} | 8 +- .../ContextConfigurationNestedTests.java | 212 +++++ .../nested/ContextHierarchyNestedTests.java | 235 +++++ ...edTestsWithSpringAndJUnitJupiterTests.java | 95 -- ...erTests.java => SqlScriptNestedTests.java} | 8 +- .../nested/TestConstructorNestedTests.java | 169 ++++ .../TestExecutionListenersNestedTests.java | 192 ++++ .../nested/TestPropertySourceNestedTests.java | 195 +++++ .../nested/TransactionalNestedTests.java | 185 ++++ .../WebAppConfigurationNestedTests.java | 137 +++ ...bstractContextConfigurationUtilsTests.java | 99 ++- .../BootstrapTestUtilsMergedConfigTests.java | 132 +++ ...tiesContextTestExecutionListenerTests.java | 576 +++++++----- .../support/TestPropertySourceUtilsTests.java | 3 + .../test/util/MetaAnnotationUtilsTests.java | 823 +++++++++--------- 30 files changed, 3185 insertions(+), 949 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/ActiveProfilesNestedTests.java rename spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/{NestedTestsWithConstructorInjectionWithSpringAndJUnitJupiterTests.java => ConstructorInjectionNestedTests.java} (90%) create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/ContextConfigurationNestedTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/ContextHierarchyNestedTests.java delete mode 100644 spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/NestedTestsWithSpringAndJUnitJupiterTests.java rename spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/{NestedTestsWithSqlScriptsAndJUnitJupiterTests.java => SqlScriptNestedTests.java} (89%) create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/TestConstructorNestedTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/TestExecutionListenersNestedTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/TestPropertySourceNestedTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/TransactionalNestedTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/WebAppConfigurationNestedTests.java diff --git a/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java b/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java index 8292a3cd99..807c41acbe 100644 --- a/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,18 @@ package org.springframework.test.context; +import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; +import java.util.LinkedHashSet; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeanUtils; -import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.lang.Nullable; +import org.springframework.test.util.MetaAnnotationUtils; +import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor; import org.springframework.util.ClassUtils; /** @@ -56,6 +58,8 @@ abstract class BootstrapUtils { private static final String WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME = "org.springframework.test.context.web.WebAppConfiguration"; + private static final Class webAppConfigurationClass = loadWebAppConfigurationClass(); + private static final Log logger = LogFactory.getLog(BootstrapUtils.class); @@ -149,7 +153,14 @@ abstract class BootstrapUtils { @Nullable private static Class resolveExplicitTestContextBootstrapper(Class testClass) { - Set annotations = AnnotatedElementUtils.findAllMergedAnnotations(testClass, BootstrapWith.class); + Set annotations = new LinkedHashSet<>(); + AnnotationDescriptor descriptor = + MetaAnnotationUtils.findAnnotationDescriptor(testClass, BootstrapWith.class); + while (descriptor != null) { + annotations.addAll(descriptor.findAllLocalMergedAnnotations()); + descriptor = descriptor.next(); + } + if (annotations.isEmpty()) { return null; } @@ -169,13 +180,22 @@ abstract class BootstrapUtils { } private static Class resolveDefaultTestContextBootstrapper(Class testClass) throws Exception { - ClassLoader classLoader = BootstrapUtils.class.getClassLoader(); - AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(testClass, - WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME, false, false); - if (attributes != null) { - return ClassUtils.forName(DEFAULT_WEB_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME, classLoader); + boolean webApp = (MetaAnnotationUtils.findMergedAnnotation(testClass, webAppConfigurationClass) != null); + String bootstrapperClassName = (webApp ? DEFAULT_WEB_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME : + DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME); + return ClassUtils.forName(bootstrapperClassName, BootstrapUtils.class.getClassLoader()); + } + + @SuppressWarnings("unchecked") + private static Class loadWebAppConfigurationClass() { + try { + return (Class) ClassUtils.forName(WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME, + BootstrapUtils.class.getClassLoader()); + } + catch (ClassNotFoundException | LinkageError ex) { + throw new IllegalStateException( + "Failed to load class for @" + WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME, ex); } - return ClassUtils.forName(DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME, classLoader); } } 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 new file mode 100644 index 0000000000..4c7d42006e --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * {@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, configuration from the test class will + * not propagate to inner test classes (see {@link EnclosingConfiguration#OVERRIDE}). + * Consequently, inner test classes will have to declare their own Spring test + * configuration annotations. If you wish for an inner test class to inherit + * configuration from its enclosing class, annotate either the inner test class + * or the enclosing class with + * {@code @NestedTestConfiguration(EnclosingConfiguration.INHERIT)}. Note that + * a {@code @NestedTestConfiguration(...)} declaration is inherited within the + * superclass hierarchy as well as within the enclosing class hierarchy. Thus, + * there is no need to redeclare the annotation unless you wish to switch the + * mode. + * + *

This annotation may be used as a meta-annotation to create custom + * composed annotations. + * + *

As of Spring Framework 5.3, the use of this annotation typically only makes + * sense in conjunction with {@link org.junit.jupiter.api.Nested @Nested} test + * classes in JUnit Jupiter. + * + * @author Sam Brannen + * @since 5.3 + * @see EnclosingConfiguration#INHERIT + * @see EnclosingConfiguration#OVERRIDE + * @see ContextConfiguration @ContextConfiguration + * @see ContextHierarchy @ContextHierarchy + * @see ActiveProfiles @ActiveProfiles + * @see TestPropertySource @TestPropertySource + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface NestedTestConfiguration { + + /** + * Configures the {@link EnclosingConfiguration} mode. + */ + EnclosingConfiguration value(); + + + /** + * Enumeration of modes that dictate how test configuration from + * enclosing classes is processed for inner test classes. + */ + enum EnclosingConfiguration { + + /** + * Indicates that test configuration for an inner test class should be + * inherited from its {@linkplain Class#getEnclosingClass() + * enclosing class}, as if the enclosing class were a superclass. + */ + INHERIT, + + /** + * Indicates that test configuration for an inner test class should + * override configuration from its + * {@linkplain Class#getEnclosingClass() enclosing class}. + */ + OVERRIDE + + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractDirtiesContextTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractDirtiesContextTestExecutionListener.java index 84a4f0cf65..f274fc30c9 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractDirtiesContextTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractDirtiesContextTestExecutionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; import org.springframework.test.annotation.DirtiesContext.MethodMode; import org.springframework.test.context.TestContext; +import org.springframework.test.util.MetaAnnotationUtils; import org.springframework.util.Assert; /** @@ -96,7 +97,7 @@ public abstract class AbstractDirtiesContextTestExecutionListener extends Abstra Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null"); DirtiesContext methodAnn = AnnotatedElementUtils.findMergedAnnotation(testMethod, DirtiesContext.class); - DirtiesContext classAnn = AnnotatedElementUtils.findMergedAnnotation(testClass, DirtiesContext.class); + DirtiesContext classAnn = MetaAnnotationUtils.findMergedAnnotation(testClass, DirtiesContext.class); boolean methodAnnotated = (methodAnn != null); boolean classAnnotated = (classAnn != null); MethodMode methodMode = (methodAnnotated ? methodAnn.methodMode() : null); @@ -133,7 +134,7 @@ public abstract class AbstractDirtiesContextTestExecutionListener extends Abstra Class testClass = testContext.getTestClass(); Assert.notNull(testClass, "The test class of the supplied TestContext must not be null"); - DirtiesContext dirtiesContext = AnnotatedElementUtils.findMergedAnnotation(testClass, DirtiesContext.class); + DirtiesContext dirtiesContext = MetaAnnotationUtils.findMergedAnnotation(testClass, DirtiesContext.class); boolean classAnnotated = (dirtiesContext != null); ClassMode classMode = (classAnnotated ? dirtiesContext.classMode() : null); diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java index 040578d7eb..21e3ca26b3 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,6 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.lang.Nullable; import org.springframework.test.context.BootstrapContext; @@ -139,13 +138,11 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot } boolean inheritListeners = testExecutionListeners.inheritListeners(); - AnnotationDescriptor superDescriptor = - MetaAnnotationUtils.findAnnotationDescriptor( - descriptor.getRootDeclaringClass().getSuperclass(), annotationType); + AnnotationDescriptor parentDescriptor = descriptor.next(); // If there are no listeners to inherit, we might need to merge the // locally declared listeners with the defaults. - if ((!inheritListeners || superDescriptor == null) && + if ((!inheritListeners || parentDescriptor == null) && testExecutionListeners.mergeMode() == MergeMode.MERGE_WITH_DEFAULTS) { if (logger.isDebugEnabled()) { logger.debug(String.format("Merging default listeners with listeners configured via " + @@ -157,7 +154,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot classesList.addAll(0, Arrays.asList(testExecutionListeners.listeners())); - descriptor = (inheritListeners ? superDescriptor : null); + descriptor = (inheritListeners ? parentDescriptor : null); } } @@ -265,7 +262,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot return buildDefaultMergedContextConfiguration(testClass, cacheAwareContextLoaderDelegate); } - if (AnnotationUtils.findAnnotation(testClass, ContextHierarchy.class) != null) { + if (MetaAnnotationUtils.findAnnotationDescriptor(testClass, ContextHierarchy.class) != null) { Map> hierarchyMap = ContextLoaderUtils.buildContextHierarchyMap(testClass); MergedContextConfiguration parentConfig = null; diff --git a/spring-test/src/main/java/org/springframework/test/context/support/ActiveProfilesUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/ActiveProfilesUtils.java index f56d935f16..fbd827dfbb 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/ActiveProfilesUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/ActiveProfilesUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,12 +28,13 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeanUtils; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfilesResolver; -import org.springframework.test.util.MetaAnnotationUtils; import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; +import static org.springframework.test.util.MetaAnnotationUtils.findAnnotationDescriptor; + /** * Utility methods for working with {@link ActiveProfiles @ActiveProfiles} and * {@link ActiveProfilesResolver ActiveProfilesResolvers}. @@ -70,25 +71,21 @@ abstract class ActiveProfilesUtils { static String[] resolveActiveProfiles(Class testClass) { Assert.notNull(testClass, "Class must not be null"); - final List profileArrays = new ArrayList<>(); - - Class annotationType = ActiveProfiles.class; - AnnotationDescriptor descriptor = - MetaAnnotationUtils.findAnnotationDescriptor(testClass, annotationType); + List profileArrays = new ArrayList<>(); + AnnotationDescriptor descriptor = findAnnotationDescriptor(testClass, ActiveProfiles.class); if (descriptor == null && logger.isDebugEnabled()) { logger.debug(String.format( "Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]", - annotationType.getName(), testClass.getName())); + ActiveProfiles.class.getName(), testClass.getName())); } while (descriptor != null) { Class rootDeclaringClass = descriptor.getRootDeclaringClass(); - Class declaringClass = descriptor.getDeclaringClass(); ActiveProfiles annotation = descriptor.synthesizeAnnotation(); if (logger.isTraceEnabled()) { logger.trace(String.format("Retrieved @ActiveProfiles [%s] for declaring class [%s]", - annotation, declaringClass.getName())); + annotation, descriptor.getDeclaringClass().getName())); } Class resolverClass = annotation.resolver(); @@ -112,14 +109,13 @@ abstract class ActiveProfilesUtils { profileArrays.add(profiles); } - descriptor = (annotation.inheritProfiles() ? MetaAnnotationUtils.findAnnotationDescriptor( - rootDeclaringClass.getSuperclass(), annotationType) : null); + descriptor = (annotation.inheritProfiles() ? descriptor.next() : null); } // Reverse the list so that we can traverse "down" the hierarchy. Collections.reverse(profileArrays); - final Set activeProfiles = new LinkedHashSet<>(); + Set activeProfiles = new LinkedHashSet<>(); for (String[] profiles : profileArrays) { for (String profile : profiles) { if (StringUtils.hasText(profile)) { diff --git a/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java index 70d70d9f0c..75adfd45cb 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -150,8 +150,8 @@ abstract class ContextLoaderUtils { } hierarchyAttributes.add(0, configAttributesList); - desc = findAnnotationDescriptorForTypes( - rootDeclaringClass.getSuperclass(), contextConfigType, contextHierarchyType); + + desc = desc.next(); } return hierarchyAttributes; @@ -182,7 +182,7 @@ abstract class ContextLoaderUtils { * @see #resolveContextHierarchyAttributes(Class) */ static Map> buildContextHierarchyMap(Class testClass) { - final Map> map = new LinkedHashMap<>(); + Map> map = new LinkedHashMap<>(); int hierarchyLevel = 1; for (List configAttributesList : resolveContextHierarchyAttributes(testClass)) { @@ -237,30 +237,34 @@ abstract class ContextLoaderUtils { static List resolveContextConfigurationAttributes(Class testClass) { Assert.notNull(testClass, "Class must not be null"); - List attributesList = new ArrayList<>(); Class annotationType = ContextConfiguration.class; - AnnotationDescriptor descriptor = findAnnotationDescriptor(testClass, annotationType); Assert.notNull(descriptor, () -> String.format( "Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]", annotationType.getName(), testClass.getName())); - while (descriptor != null) { - convertContextConfigToConfigAttributesAndAddToList(descriptor.synthesizeAnnotation(), - descriptor.getRootDeclaringClass(), attributesList); - descriptor = findAnnotationDescriptor(descriptor.getRootDeclaringClass().getSuperclass(), annotationType); - } - + List attributesList = new ArrayList<>(); + resolveContextConfigurationAttributes(attributesList, descriptor); return attributesList; } + private static void resolveContextConfigurationAttributes(List attributesList, + AnnotationDescriptor descriptor) { + + if (descriptor != null) { + convertContextConfigToConfigAttributesAndAddToList(descriptor.synthesizeAnnotation(), + descriptor.getRootDeclaringClass(), attributesList); + resolveContextConfigurationAttributes(attributesList, descriptor.next()); + } + } + /** * Convenience method for creating a {@link ContextConfigurationAttributes} * instance from the supplied {@link ContextConfiguration} annotation and * declaring class and then adding the attributes to the supplied list. */ private static void convertContextConfigToConfigAttributesAndAddToList(ContextConfiguration contextConfiguration, - Class declaringClass, final List attributesList) { + Class declaringClass, List attributesList) { if (logger.isTraceEnabled()) { logger.trace(String.format("Retrieved @ContextConfiguration [%s] for declaring class [%s].", diff --git a/spring-test/src/main/java/org/springframework/test/context/support/MergedTestPropertySources.java b/spring-test/src/main/java/org/springframework/test/context/support/MergedTestPropertySources.java index ad520a007d..ee7e31d00e 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/MergedTestPropertySources.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/MergedTestPropertySources.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,10 @@ package org.springframework.test.context.support; +import java.util.Arrays; + +import org.springframework.core.style.ToStringCreator; +import org.springframework.lang.Nullable; import org.springframework.test.context.TestPropertySource; import org.springframework.util.Assert; @@ -76,4 +80,55 @@ class MergedTestPropertySources { return this.properties; } + /** + * Determine if the supplied object is equal to this {@code MergedTestPropertySources} + * instance by comparing both object's {@linkplain #getLocations() locations} + * and {@linkplain #getProperties() properties}. + * @since 5.3 + */ + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || other.getClass() != getClass()) { + return false; + } + + MergedTestPropertySources that = (MergedTestPropertySources) other; + if (!Arrays.equals(this.locations, that.locations)) { + return false; + } + if (!Arrays.equals(this.properties, that.properties)) { + return false; + } + + return true; + } + + /** + * Generate a unique hash code for all properties of this + * {@code MergedTestPropertySources} instance. + * @since 5.3 + */ + @Override + public int hashCode() { + int result = Arrays.hashCode(this.locations); + result = 31 * result + Arrays.hashCode(this.properties); + return result; + } + + /** + * Provide a String representation of this {@code MergedTestPropertySources} + * instance. + * @since 5.3 + */ + @Override + public String toString() { + return new ToStringCreator(this) + .append("locations", Arrays.toString(this.locations)) + .append("properties", Arrays.toString(this.properties)) + .toString(); + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/TestConstructorUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/TestConstructorUtils.java index 91e4aec5f4..0061b1c07a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/TestConstructorUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/TestConstructorUtils.java @@ -25,6 +25,7 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.lang.Nullable; import org.springframework.test.context.TestConstructor; import org.springframework.test.context.TestConstructor.AutowireMode; +import org.springframework.test.util.MetaAnnotationUtils; /** * Utility methods for working with {@link TestConstructor @TestConstructor}. @@ -133,7 +134,7 @@ public abstract class TestConstructorUtils { AutowireMode autowireMode = null; // Is the test class annotated with @TestConstructor? - TestConstructor testConstructor = AnnotatedElementUtils.findMergedAnnotation(testClass, TestConstructor.class); + TestConstructor testConstructor = MetaAnnotationUtils.findMergedAnnotation(testClass, TestConstructor.class); if (testConstructor != null) { autowireMode = testConstructor.autowireMode(); } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java index fefe738340..52f14ae139 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ package org.springframework.test.context.support; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.apache.commons.logging.Log; @@ -25,7 +25,6 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.io.ClassPathResource; -import org.springframework.core.log.LogMessage; import org.springframework.core.style.ToStringCreator; import org.springframework.test.context.TestPropertySource; import org.springframework.util.Assert; @@ -52,12 +51,8 @@ class TestPropertySourceAttributes { private static final Log logger = LogFactory.getLog(TestPropertySourceAttributes.class); - private final int aggregateIndex; - private final Class declaringClass; - private final MergedAnnotation rootAnnotation; - private final List locations = new ArrayList<>(); private final boolean inheritLocations; @@ -68,96 +63,24 @@ class TestPropertySourceAttributes { TestPropertySourceAttributes(MergedAnnotation annotation) { - this.aggregateIndex = annotation.getAggregateIndex(); this.declaringClass = declaringClass(annotation); - this.rootAnnotation = annotation.getRoot(); this.inheritLocations = annotation.getBoolean("inheritLocations"); this.inheritProperties = annotation.getBoolean("inheritProperties"); mergePropertiesAndLocations(annotation); } - - /** - * Determine if the annotation represented by this - * {@code TestPropertySourceAttributes} instance can be merged with the - * supplied {@code annotation}. - *

This method effectively checks that two annotations are declared at - * the same level in the type hierarchy (i.e., have the same - * {@linkplain MergedAnnotation#getAggregateIndex() aggregate index}). - * @since 5.2 - * @see #mergeWith(MergedAnnotation) - */ - boolean canMergeWith(MergedAnnotation annotation) { - return annotation.getAggregateIndex() == this.aggregateIndex; - } - - /** - * Merge this {@code TestPropertySourceAttributes} instance with the - * supplied {@code annotation}, asserting that the two sets of test property - * source attributes have identical values for the - * {@link TestPropertySource#inheritLocations} and - * {@link TestPropertySource#inheritProperties} flags and that the two - * underlying annotations were declared on the same class. - *

This method should only be invoked if {@link #canMergeWith(MergedAnnotation)} - * returns {@code true}. - * @since 5.2 - * @see #canMergeWith(MergedAnnotation) - */ - void mergeWith(MergedAnnotation annotation) { - Class source = declaringClass(annotation); - Assert.state(source == this.declaringClass, - () -> "Detected @TestPropertySource declarations within an aggregate index " - + "with different sources: " + this.declaringClass.getName() + " and " - + source.getName()); - logger.trace(LogMessage.format("Retrieved %s for declaring class [%s].", - annotation, this.declaringClass.getName())); - assertSameBooleanAttribute(this.inheritLocations, annotation, "inheritLocations"); - assertSameBooleanAttribute(this.inheritProperties, annotation, "inheritProperties"); - mergePropertiesAndLocations(annotation); - } - - private void assertSameBooleanAttribute(boolean expected, MergedAnnotation annotation, - String attribute) { - - Assert.isTrue(expected == annotation.getBoolean(attribute), () -> String.format( - "@%s on %s and @%s on %s must declare the same value for '%s' as other " + - "directly present or meta-present @TestPropertySource annotations", - this.rootAnnotation.getType().getSimpleName(), this.declaringClass.getSimpleName(), - annotation.getRoot().getType().getSimpleName(), declaringClass(annotation).getSimpleName(), - attribute)); - } - private void mergePropertiesAndLocations(MergedAnnotation annotation) { String[] locations = annotation.getStringArray("locations"); String[] properties = annotation.getStringArray("properties"); - // If the meta-distance is positive, that means the annotation is - // meta-present and should therefore have lower priority than directly - // present annotations (i.e., it should be prepended to the list instead - // of appended). This follows the rule of last-one-wins for overriding - // properties. - boolean prepend = annotation.getDistance() > 0; if (ObjectUtils.isEmpty(locations) && ObjectUtils.isEmpty(properties)) { - addAll(prepend, this.locations, detectDefaultPropertiesFile(annotation)); + Collections.addAll(this.locations, detectDefaultPropertiesFile(annotation)); } else { - addAll(prepend, this.locations, locations); - addAll(prepend, this.properties, properties); + Collections.addAll(this.locations, locations); + Collections.addAll(this.properties, properties); } } - /** - * Add all of the supplied elements to the provided list, honoring the - * {@code prepend} flag. - *

If the {@code prepend} flag is {@code false}, the elements will appended - * to the list. - * @param prepend whether the elements should be prepended to the list - * @param list the list to which to add the elements - * @param elements the elements to add to the list - */ - private void addAll(boolean prepend, List list, String... elements) { - list.addAll((prepend ? 0 : list.size()), Arrays.asList(elements)); - } - private String detectDefaultPropertiesFile(MergedAnnotation annotation) { Class testClass = declaringClass(annotation); String resourcePath = ClassUtils.convertClassNameToResourcePath(testClass.getName()) + ".properties"; diff --git a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java index 913715b951..e5e938633e 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,12 +18,15 @@ package org.springframework.test.context.support; import java.io.IOException; import java.io.StringReader; +import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -42,6 +45,7 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.ResourcePropertySource; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.util.TestContextResourceUtils; +import org.springframework.test.util.MetaAnnotationUtils; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -71,42 +75,22 @@ public abstract class TestPropertySourceUtils { static MergedTestPropertySources buildMergedTestPropertySources(Class testClass) { - MergedAnnotations mergedAnnotations = MergedAnnotations.from(testClass, SearchStrategy.TYPE_HIERARCHY); - return (mergedAnnotations.isPresent(TestPropertySource.class) ? mergeTestPropertySources(mergedAnnotations) : - MergedTestPropertySources.empty()); + return mergeTestPropertySources(findRepeatableAnnotations(testClass, TestPropertySource.class)); } - private static MergedTestPropertySources mergeTestPropertySources(MergedAnnotations mergedAnnotations) { - List attributesList = resolveTestPropertySourceAttributes(mergedAnnotations); + private static MergedTestPropertySources mergeTestPropertySources( + List> mergedAnnotations) { + + if (mergedAnnotations.isEmpty()) { + return MergedTestPropertySources.empty(); + } + + List attributesList = mergedAnnotations.stream() + .map(TestPropertySourceAttributes::new) + .collect(Collectors.toList()); return new MergedTestPropertySources(mergeLocations(attributesList), mergeProperties(attributesList)); } - private static List resolveTestPropertySourceAttributes( - MergedAnnotations mergedAnnotations) { - - List attributesList = new ArrayList<>(); - mergedAnnotations.stream(TestPropertySource.class) - .forEach(annotation -> addOrMergeTestPropertySourceAttributes(attributesList, annotation)); - return attributesList; - } - - private static void addOrMergeTestPropertySourceAttributes(List attributesList, - MergedAnnotation current) { - - if (attributesList.isEmpty()) { - attributesList.add(new TestPropertySourceAttributes(current)); - } - else { - TestPropertySourceAttributes previous = attributesList.get(attributesList.size() - 1); - if (previous.canMergeWith(current)) { - previous.mergeWith(current); - } - else { - attributesList.add(new TestPropertySourceAttributes(current)); - } - } - } - private static String[] mergeLocations(List attributesList) { List locations = new ArrayList<>(); for (TestPropertySourceAttributes attrs : attributesList) { @@ -292,4 +276,53 @@ public abstract class TestPropertySourceUtils { return map; } + private static List> findRepeatableAnnotations( + Class clazz, Class annotationType) { + + List>> listOfLists = new ArrayList<>(); + findRepeatableAnnotations(clazz, annotationType, listOfLists, new int[] {0}); + return listOfLists.stream().flatMap(List::stream).collect(Collectors.toList()); + } + + private static void findRepeatableAnnotations( + Class clazz, Class annotationType, List>> listOfLists, int[] aggregateIndex) { + + MergedAnnotations.from(clazz, SearchStrategy.DIRECT) + .stream(annotationType) + .sorted(highMetaDistancesFirst()) + .forEach(annotation -> { + List> current = null; + if (listOfLists.size() < aggregateIndex[0] + 1) { + current = new ArrayList<>(); + listOfLists.add(current); + } + else { + current = listOfLists.get(aggregateIndex[0]); + } + current.add(0, annotation); + }); + + aggregateIndex[0]++; + + // Declared on an interface? + for (Class ifc : clazz.getInterfaces()) { + findRepeatableAnnotations(ifc, annotationType, listOfLists, aggregateIndex); + } + + // Declared on a superclass? + Class superclass = clazz.getSuperclass(); + if (superclass != null & superclass != Object.class) { + findRepeatableAnnotations(superclass, annotationType, listOfLists, aggregateIndex); + } + + // Declared on an enclosing class of an inner class? + if (MetaAnnotationUtils.searchEnclosingClass(clazz)) { + findRepeatableAnnotations(clazz.getEnclosingClass(), annotationType, listOfLists, aggregateIndex); + } + } + + private static Comparator> highMetaDistancesFirst() { + return Comparator.> comparingInt(MergedAnnotation::getDistance).reversed(); + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java index 220bb62b0a..107f050427 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import org.springframework.test.annotation.Commit; import org.springframework.test.annotation.Rollback; import org.springframework.test.context.TestContext; import org.springframework.test.context.support.AbstractTestExecutionListener; +import org.springframework.test.util.MetaAnnotationUtils; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; @@ -148,7 +149,27 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis private static final Log logger = LogFactory.getLog(TransactionalTestExecutionListener.class); // Do not require @Transactional test methods to be public. - protected final TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource(false); + @SuppressWarnings("serial") + protected final TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource(false) { + + @Override + protected TransactionAttribute findTransactionAttribute(Class clazz) { + // @Transactional present in inheritance hierarchy? + TransactionAttribute result = super.findTransactionAttribute(clazz); + if (result != null) { + return result; + } + // @Transactional present in enclosing class hierarchy? + return findTransactionAttributeInEnclosingClassHierarchy(clazz); + } + + private TransactionAttribute findTransactionAttributeInEnclosingClassHierarchy(Class clazz) { + if (MetaAnnotationUtils.searchEnclosingClass(clazz)) { + return findTransactionAttribute(clazz.getEnclosingClass()); + } + return null; + } + }; /** @@ -376,7 +397,7 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis */ protected final boolean isDefaultRollback(TestContext testContext) throws Exception { Class testClass = testContext.getTestClass(); - Rollback rollback = AnnotatedElementUtils.findMergedAnnotation(testClass, Rollback.class); + Rollback rollback = MetaAnnotationUtils.findMergedAnnotation(testClass, Rollback.class); boolean rollbackPresent = (rollback != null); if (rollbackPresent) { diff --git a/spring-test/src/main/java/org/springframework/test/context/web/WebTestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/web/WebTestContextBootstrapper.java index 04917c90b7..e6ef700831 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/WebTestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/WebTestContextBootstrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,11 @@ package org.springframework.test.context.web; -import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.test.context.ContextLoader; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.TestContextBootstrapper; import org.springframework.test.context.support.DefaultTestContextBootstrapper; +import org.springframework.test.util.MetaAnnotationUtils; /** * Web-specific implementation of the {@link TestContextBootstrapper} SPI. @@ -45,7 +45,7 @@ public class WebTestContextBootstrapper extends DefaultTestContextBootstrapper { */ @Override protected Class getDefaultContextLoaderClass(Class testClass) { - if (AnnotatedElementUtils.hasAnnotation(testClass, WebAppConfiguration.class)) { + if (getWebAppConfiguration(testClass) != null) { return WebDelegatingSmartContextLoader.class; } else { @@ -61,8 +61,7 @@ public class WebTestContextBootstrapper extends DefaultTestContextBootstrapper { */ @Override protected MergedContextConfiguration processMergedContextConfiguration(MergedContextConfiguration mergedConfig) { - WebAppConfiguration webAppConfiguration = - AnnotatedElementUtils.findMergedAnnotation(mergedConfig.getTestClass(), WebAppConfiguration.class); + WebAppConfiguration webAppConfiguration = getWebAppConfiguration(mergedConfig.getTestClass()); if (webAppConfiguration != null) { return new WebMergedContextConfiguration(mergedConfig, webAppConfiguration.value()); } @@ -71,4 +70,8 @@ public class WebTestContextBootstrapper extends DefaultTestContextBootstrapper { } } + private static WebAppConfiguration getWebAppConfiguration(Class testClass) { + return MetaAnnotationUtils.findMergedAnnotation(testClass, WebAppConfiguration.class); + } + } 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 d9c813cc6c..cee1b4d56b 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 @@ -23,15 +23,27 @@ import java.util.Set; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotationCollectors; +import org.springframework.core.annotation.MergedAnnotationPredicates; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.core.annotation.RepeatableContainers; import org.springframework.core.style.ToStringCreator; import org.springframework.lang.Nullable; +import org.springframework.test.context.NestedTestConfiguration; +import org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentLruCache; import org.springframework.util.ObjectUtils; /** * {@code MetaAnnotationUtils} is a collection of utility methods that complements * the standard support already available in {@link AnnotationUtils}. * + *

Mainly for internal use within the framework. + * *

Whereas {@code AnnotationUtils} provides utilities for getting or * finding an annotation, {@code MetaAnnotationUtils} goes a step further * by providing support for determining the root class on which an @@ -56,6 +68,34 @@ import org.springframework.util.ObjectUtils; */ public abstract class MetaAnnotationUtils { + private static final ConcurrentLruCache, SearchStrategy> cachedSearchStrategies = + new ConcurrentLruCache<>(32, MetaAnnotationUtils::lookUpSearchStrategy); + + + /** + * Find the first annotation of the specified {@code annotationType} within + * the annotation hierarchy above the supplied class, merge that + * annotation's attributes with matching attributes from annotations + * in lower levels of the annotation hierarchy, and synthesize the result back + * into an annotation of the specified {@code annotationType}. + *

In the context of this method, the term "above" means within the + * {@linkplain Class#getSuperclass() superclass} hierarchy or within the + * {@linkplain Class#getEnclosingClass() enclosing class} hierarchy of the + * supplied class. The enclosing class hierarchy will only be searched if + * appropriate. + * @param clazz the class to look for annotations on + * @param annotationType the type of annotation to look for + * @return the merged, synthesized {@code Annotation}, or {@code null} if not found + * @since 5.3 + * @see AnnotatedElementUtils#findMergedAnnotation(java.lang.reflect.AnnotatedElement, Class) + * @see #findAnnotationDescriptor(Class, Class) + */ + @Nullable + public static T findMergedAnnotation(Class clazz, Class annotationType) { + AnnotationDescriptor descriptor = findAnnotationDescriptor(clazz, annotationType); + return (descriptor != null ? descriptor.synthesizeAnnotation() : null); + } + /** * Find the {@link AnnotationDescriptor} for the supplied {@code annotationType} * on the supplied {@link Class}, traversing its annotations, interfaces, and @@ -123,7 +163,7 @@ public abstract class MetaAnnotationUtils { } } - // Declared on interface? + // Declared on an interface? for (Class ifc : clazz.getInterfaces()) { AnnotationDescriptor descriptor = findAnnotationDescriptor(ifc, visited, annotationType); if (descriptor != null) { @@ -133,7 +173,21 @@ public abstract class MetaAnnotationUtils { } // Declared on a superclass? - return findAnnotationDescriptor(clazz.getSuperclass(), visited, annotationType); + AnnotationDescriptor descriptor = + findAnnotationDescriptor(clazz.getSuperclass(), visited, annotationType); + if (descriptor != null) { + return descriptor; + } + + // Declared on an enclosing class of an inner class? + if (searchEnclosingClass(clazz)) { + descriptor = findAnnotationDescriptor(clazz.getEnclosingClass(), visited, annotationType); + if (descriptor != null) { + return descriptor; + } + } + + return null; } /** @@ -196,7 +250,7 @@ public abstract class MetaAnnotationUtils { // Declared locally? for (Class annotationType : annotationTypes) { if (AnnotationUtils.isAnnotationDeclaredLocally(annotationType, clazz)) { - return new UntypedAnnotationDescriptor(clazz, clazz.getAnnotation(annotationType)); + return new UntypedAnnotationDescriptor(clazz, clazz.getAnnotation(annotationType), annotationTypes); } } @@ -207,22 +261,75 @@ public abstract class MetaAnnotationUtils { composedAnnotation.annotationType(), visited, annotationTypes); if (descriptor != null) { return new UntypedAnnotationDescriptor(clazz, descriptor.getDeclaringClass(), - composedAnnotation, descriptor.getAnnotation()); + composedAnnotation, descriptor.getAnnotation(), annotationTypes); } } } - // Declared on interface? + // Declared on an interface? for (Class ifc : clazz.getInterfaces()) { UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes(ifc, visited, annotationTypes); if (descriptor != null) { return new UntypedAnnotationDescriptor(clazz, descriptor.getDeclaringClass(), - descriptor.getComposedAnnotation(), descriptor.getAnnotation()); + descriptor.getComposedAnnotation(), descriptor.getAnnotation(), annotationTypes); } } // Declared on a superclass? - return findAnnotationDescriptorForTypes(clazz.getSuperclass(), visited, annotationTypes); + UntypedAnnotationDescriptor descriptor = + findAnnotationDescriptorForTypes(clazz.getSuperclass(), visited, annotationTypes); + if (descriptor != null) { + return descriptor; + } + + // Declared on an enclosing class of an inner class? + if (searchEnclosingClass(clazz)) { + descriptor = findAnnotationDescriptorForTypes(clazz.getEnclosingClass(), visited, annotationTypes); + if (descriptor != null) { + return descriptor; + } + } + + return null; + } + + /** + * Determine if annotations on the enclosing class of the supplied class + * should be searched by algorithms in {@link MetaAnnotationUtils}. + * @param clazz the class whose enclosing class should potentially be searched + * @return {@code true} if the supplied class is an inner class whose enclosing + * class should be searched + * @since 5.3 + * @see ClassUtils#isInnerClass(Class) + * @see #getSearchStrategy(Class) + */ + public static boolean searchEnclosingClass(Class clazz) { + return (ClassUtils.isInnerClass(clazz) && + getSearchStrategy(clazz) == SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES); + } + + /** + * Get the {@link SearchStrategy} for the supplied class. + * @param clazz the class for which the search strategy should be resolved + * @return the resolved search strategy + * @since 5.3 + */ + private static SearchStrategy getSearchStrategy(Class clazz) { + return cachedSearchStrategies.get(clazz); + } + + private static SearchStrategy lookUpSearchStrategy(Class clazz) { + EnclosingConfiguration enclosingConfiguration = + MergedAnnotations.from(clazz, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES) + .stream(NestedTestConfiguration.class) + .map(mergedAnnotation -> mergedAnnotation.getEnum("value", EnclosingConfiguration.class)) + .findFirst() + .orElse(EnclosingConfiguration.OVERRIDE); + // TODO Switch the default EnclosingConfiguration mode to INHERIT. + // TODO Make the default EnclosingConfiguration mode globally configurable via SpringProperties. + return (enclosingConfiguration == EnclosingConfiguration.INHERIT ? + SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES : + SearchStrategy.TYPE_HIERARCHY); } private static void assertNonEmptyAnnotationTypeArray(Class[] annotationTypes, String message) { @@ -358,6 +465,54 @@ public abstract class MetaAnnotationUtils { return (this.composedAnnotation != null ? this.composedAnnotation.annotationType() : null); } + /** + * Find the next {@link AnnotationDescriptor} for the specified + * {@linkplain #getAnnotationType() annotation type} in the hierarchy + * above the {@linkplain #getRootDeclaringClass() root declaring class} + * of this descriptor. + *

If a corresponding annotation is found in the superclass hierarchy + * of the root declaring class, that will be returned. Otherwise, an + * attempt will be made to find a corresponding annotation in the + * {@linkplain Class#getEnclosingClass() enclosing class} hierarchy of + * the root declaring class if + * {@linkplain MetaAnnotationUtils#searchEnclosingClass appropriate}. + * @return the next corresponding annotation descriptor if the annotation + * was found; otherwise {@code null} + * @since 5.3 + */ + @Nullable + @SuppressWarnings("unchecked") + public AnnotationDescriptor next() { + Class annotationType = (Class) getAnnotationType(); + // Declared on a superclass? + AnnotationDescriptor descriptor = + findAnnotationDescriptor(getRootDeclaringClass().getSuperclass(), annotationType); + // Declared on an enclosing class of an inner class? + if (descriptor == null && searchEnclosingClass(getRootDeclaringClass())) { + descriptor = findAnnotationDescriptor(getRootDeclaringClass().getEnclosingClass(), annotationType); + } + return descriptor; + } + + /** + * Find all annotations of the specified + * {@linkplain #getAnnotationType() annotation type} that are present or + * meta-present on the {@linkplain #getRootDeclaringClass() root declaring + * class} of this descriptor. + * @return the set of all merged, synthesized {@code Annotations} found, + * or an empty set if none were found + * @since 5.3 + */ + @SuppressWarnings("unchecked") + public Set findAllLocalMergedAnnotations() { + Class annotationType = (Class) getAnnotationType(); + SearchStrategy searchStrategy = getSearchStrategy(getRootDeclaringClass()); + return MergedAnnotations.from(getRootDeclaringClass(), searchStrategy, RepeatableContainers.none()) + .stream(annotationType) + .filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex)) + .collect(MergedAnnotationCollectors.toAnnotationSet()); + } + /** * Provide a textual representation of this {@code AnnotationDescriptor}. */ @@ -380,20 +535,48 @@ public abstract class MetaAnnotationUtils { */ public static class UntypedAnnotationDescriptor extends AnnotationDescriptor { + @Nullable + private final Class[] annotationTypes; + + /** + * Create a new {@plain UntypedAnnotationDescriptor}. + * @deprecated As of Spring Framework 5.3, in favor of + * {@link UntypedAnnotationDescriptor#UntypedAnnotationDescriptor(Class, Annotation, Class[])} + */ + @Deprecated public UntypedAnnotationDescriptor(Class rootDeclaringClass, Annotation annotation) { - this(rootDeclaringClass, rootDeclaringClass, null, annotation); + this(rootDeclaringClass, annotation, null); } + public UntypedAnnotationDescriptor(Class rootDeclaringClass, Annotation annotation, + @Nullable Class[] annotationTypes) { + + this(rootDeclaringClass, rootDeclaringClass, null, annotation, annotationTypes); + } + + /** + * Create a new {@plain UntypedAnnotationDescriptor}. + * @deprecated As of Spring Framework 5.3, in favor of + * {@link UntypedAnnotationDescriptor#UntypedAnnotationDescriptor(Class, Class, Annotation, Annotation, Class[])} + */ + @Deprecated public UntypedAnnotationDescriptor(Class rootDeclaringClass, Class declaringClass, @Nullable Annotation composedAnnotation, Annotation annotation) { + this(rootDeclaringClass, declaringClass, composedAnnotation, annotation, null); + } + + public UntypedAnnotationDescriptor(Class rootDeclaringClass, Class declaringClass, + @Nullable Annotation composedAnnotation, Annotation annotation, + @Nullable Class[] annotationTypes) { + super(rootDeclaringClass, declaringClass, composedAnnotation, annotation); + this.annotationTypes = annotationTypes; } /** * Throws an {@link UnsupportedOperationException} since the type of annotation - * represented by the {@link #getAnnotationAttributes AnnotationAttributes} in - * an {@code UntypedAnnotationDescriptor} is unknown. + * represented by an {@code UntypedAnnotationDescriptor} is unknown. * @since 4.2 */ @Override @@ -401,6 +584,52 @@ public abstract class MetaAnnotationUtils { throw new UnsupportedOperationException( "synthesizeAnnotation() is unsupported in UntypedAnnotationDescriptor"); } + + /** + * Find the next {@link UntypedAnnotationDescriptor} for the specified + * annotation types in the hierarchy above the + * {@linkplain #getRootDeclaringClass() root declaring class} of this + * descriptor. + *

If one of the corresponding annotations is found in the superclass + * hierarchy of the root declaring class, that will be returned. Otherwise, + * an attempt will be made to find a corresponding annotation in the + * {@linkplain Class#getEnclosingClass() enclosing class} hierarchy of + * the root declaring class if + * {@linkplain MetaAnnotationUtils#searchEnclosingClass appropriate}. + * @return the next corresponding annotation descriptor if one of the + * annotations was found; otherwise {@code null} + * @since 5.3 + * @see AnnotationDescriptor#next() + */ + @Override + @Nullable + public UntypedAnnotationDescriptor next() { + if (ObjectUtils.isEmpty(this.annotationTypes)) { + throw new UnsupportedOperationException( + "next() is unsupported if UntypedAnnotationDescriptor is instantiated without 'annotationTypes'"); + } + + // Declared on a superclass? + UntypedAnnotationDescriptor descriptor = + findAnnotationDescriptorForTypes(getRootDeclaringClass().getSuperclass(), this.annotationTypes); + // Declared on an enclosing class of an inner class? + if (descriptor == null && searchEnclosingClass(getRootDeclaringClass())) { + descriptor = findAnnotationDescriptorForTypes(getRootDeclaringClass().getEnclosingClass(), this.annotationTypes); + } + return descriptor; + } + + /** + * Throws an {@link UnsupportedOperationException} since the type of annotation + * represented by an {@code UntypedAnnotationDescriptor} is unknown. + * @since 5.3 + */ + @Override + public Set findAllLocalMergedAnnotations() { + throw new UnsupportedOperationException( + "findAllLocalMergedAnnotations() is unsupported in UntypedAnnotationDescriptor"); + } + } } diff --git a/spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java index b2cca6ae2a..7ed75786f8 100644 --- a/spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,17 +18,33 @@ package org.springframework.test.context; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.test.context.BootstrapUtilsTests.OuterClass.NestedWithInheritedBootstrapper; +import org.springframework.test.context.BootstrapUtilsTests.OuterClass.NestedWithInheritedBootstrapper.DoubleNestedWithInheritedButOverriddenBootstrapper; +import org.springframework.test.context.BootstrapUtilsTests.OuterClass.NestedWithInheritedBootstrapper.DoubleNestedWithOverriddenBootstrapper; +import org.springframework.test.context.BootstrapUtilsTests.OuterClass.NestedWithInheritedBootstrapper.DoubleNestedWithOverriddenBootstrapper.TripleNestedWithInheritedBootstrapper; +import org.springframework.test.context.BootstrapUtilsTests.OuterClass.NestedWithInheritedBootstrapper.DoubleNestedWithOverriddenBootstrapper.TripleNestedWithInheritedBootstrapperButLocalOverride; +import org.springframework.test.context.BootstrapUtilsTests.WebAppConfigClass.NestedWithInheritedWebConfig; +import org.springframework.test.context.BootstrapUtilsTests.WebAppConfigClass.NestedWithInheritedWebConfig.DoubleNestedWithImplicitlyInheritedWebConfig; +import org.springframework.test.context.BootstrapUtilsTests.WebAppConfigClass.NestedWithInheritedWebConfig.DoubleNestedWithOverriddenWebConfig; +import org.springframework.test.context.BootstrapUtilsTests.WebAppConfigClass.NestedWithInheritedWebConfig.DoubleNestedWithOverriddenWebConfig.TripleNestedWithInheritedOverriddenWebConfig; +import org.springframework.test.context.BootstrapUtilsTests.WebAppConfigClass.NestedWithInheritedWebConfig.DoubleNestedWithOverriddenWebConfig.TripleNestedWithInheritedOverriddenWebConfigAndTestInterface; import org.springframework.test.context.support.DefaultTestContextBootstrapper; -import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.context.web.WebTestContextBootstrapper; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.mockito.Mockito.mock; import static org.springframework.test.context.BootstrapUtils.resolveTestContextBootstrapper; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.INHERIT; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.OVERRIDE; /** * Unit tests for {@link BootstrapUtils}. @@ -65,11 +81,6 @@ class BootstrapUtilsTests { assertBootstrapper(NonAnnotatedClass.class, DefaultTestContextBootstrapper.class); } - @Test - void resolveTestContextBootstrapperForWebAppConfigurationAnnotatedClass() { - assertBootstrapper(WebAppConfigurationAnnotatedClass.class, WebTestContextBootstrapper.class); - } - @Test void resolveTestContextBootstrapperWithDirectBootstrapWithAnnotation() { assertBootstrapper(DirectBootstrapWithAnnotationClass.class, FooBootstrapper.class); @@ -90,6 +101,37 @@ class BootstrapUtilsTests { assertBootstrapper(DuplicateMetaAnnotatedBootstrapWithAnnotationClass.class, FooBootstrapper.class); } + /** + * @since 5.3 + */ + @ParameterizedTest(name = "{0}") + @MethodSource + void resolveTestContextBootstrapperInEnclosingClassHierarchy(String name, Class testClass, Class expectedBootstrapper) { + assertBootstrapper(testClass, expectedBootstrapper); + } + + static Stream resolveTestContextBootstrapperInEnclosingClassHierarchy() { + return Stream.of(// + args(OuterClass.class, FooBootstrapper.class),// + args(NestedWithInheritedBootstrapper.class, FooBootstrapper.class),// + args(DoubleNestedWithInheritedButOverriddenBootstrapper.class, EnigmaBootstrapper.class),// + args(DoubleNestedWithOverriddenBootstrapper.class, BarBootstrapper.class),// + args(TripleNestedWithInheritedBootstrapper.class, BarBootstrapper.class),// + args(TripleNestedWithInheritedBootstrapperButLocalOverride.class, EnigmaBootstrapper.class),// + // @WebAppConfiguration and default bootstrapper + args(WebAppConfigClass.class, WebTestContextBootstrapper.class),// + args(NestedWithInheritedWebConfig.class, WebTestContextBootstrapper.class),// + args(DoubleNestedWithImplicitlyInheritedWebConfig.class, WebTestContextBootstrapper.class),// + args(DoubleNestedWithOverriddenWebConfig.class, DefaultTestContextBootstrapper.class),// + args(TripleNestedWithInheritedOverriddenWebConfig.class, WebTestContextBootstrapper.class),// + args(TripleNestedWithInheritedOverriddenWebConfigAndTestInterface.class, DefaultTestContextBootstrapper.class)// + ); + } + + private static Arguments args(Class testClass, Class expectedBootstrapper) { + return arguments(testClass.getSimpleName(), testClass, expectedBootstrapper); + } + /** * @since 5.1 */ @@ -153,7 +195,62 @@ class BootstrapUtilsTests { @BootstrapWith(EnigmaBootstrapper.class) static class LocalDeclarationAndMetaAnnotatedBootstrapWithAnnotationClass {} - @WebAppConfiguration - static class WebAppConfigurationAnnotatedClass {} + @org.springframework.test.context.web.WebAppConfiguration + static class WebAppConfigClass { + + @NestedTestConfiguration(INHERIT) + class NestedWithInheritedWebConfig { + + class DoubleNestedWithImplicitlyInheritedWebConfig { + } + + @NestedTestConfiguration(OVERRIDE) + class DoubleNestedWithOverriddenWebConfig { + + @NestedTestConfiguration(INHERIT) + @org.springframework.test.context.web.WebAppConfiguration + class TripleNestedWithInheritedOverriddenWebConfig { + } + + @NestedTestConfiguration(INHERIT) + class TripleNestedWithInheritedOverriddenWebConfigAndTestInterface { + } + } + } + + // Intentionally not annotated with @WebAppConfiguration to ensure that + // TripleNestedWithInheritedOverriddenWebConfigAndTestInterface is not + // considered to be annotated with @WebAppConfiguration even though the + // enclosing class for TestInterface is annotated with @WebAppConfiguration. + interface TestInterface { + } + } + + @BootWithFoo + static class OuterClass { + + @NestedTestConfiguration(INHERIT) + class NestedWithInheritedBootstrapper { + + @NestedTestConfiguration(INHERIT) + @BootstrapWith(EnigmaBootstrapper.class) + class DoubleNestedWithInheritedButOverriddenBootstrapper { + } + + @NestedTestConfiguration(OVERRIDE) + @BootWithBar + class DoubleNestedWithOverriddenBootstrapper { + + @NestedTestConfiguration(INHERIT) + class TripleNestedWithInheritedBootstrapper { + } + + @NestedTestConfiguration(INHERIT) + @BootstrapWith(EnigmaBootstrapper.class) + class TripleNestedWithInheritedBootstrapperButLocalOverride { + } + } + } + } } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/ActiveProfilesNestedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/ActiveProfilesNestedTests.java new file mode 100644 index 0000000000..2a999775ef --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/ActiveProfilesNestedTests.java @@ -0,0 +1,211 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.junit.jupiter.nested; + +import java.util.List; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.NestedTestConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.context.junit.jupiter.nested.ActiveProfilesNestedTests.Config1; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.INHERIT; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.OVERRIDE; + +/** + * Integration tests that verify support for {@code @Nested} test classes using + * {@link ActiveProfiles @ActiveProfiles} in conjunction with the + * {@link SpringExtension} in a JUnit Jupiter environment. + * + * @author Sam Brannen + * @since 5.3 + */ +@SpringJUnitConfig(Config1.class) +@ActiveProfiles("1") +class ActiveProfilesNestedTests { + + @Autowired + List strings; + + + @Test + void test() { + assertThat(this.strings).containsExactlyInAnyOrder("X", "A1"); + } + + + @Nested + @NestedTestConfiguration(INHERIT) + class InheritedConfigTests { + + @Autowired + List localStrings; + + + @Test + void test() { + assertThat(strings).containsExactlyInAnyOrder("X", "A1"); + assertThat(this.localStrings).containsExactlyInAnyOrder("X", "A1"); + } + } + + @Nested + @SpringJUnitConfig(Config2.class) + @ActiveProfiles("2") + class ConfigOverriddenByDefaultTests { + + @Autowired + List localStrings; + + + @Test + void test() { + assertThat(strings).containsExactlyInAnyOrder("X", "A1"); + assertThat(this.localStrings).containsExactlyInAnyOrder("Y", "A2"); + } + } + + @Nested + @NestedTestConfiguration(INHERIT) + @ContextConfiguration(classes = Config2.class) + @ActiveProfiles("2") + class InheritedAndExtendedConfigTests { + + @Autowired + List localStrings; + + + @Test + void test() { + assertThat(strings).containsExactlyInAnyOrder("X", "A1"); + assertThat(this.localStrings).containsExactlyInAnyOrder("X", "A1", "Y", "A2"); + } + + + @Nested + @NestedTestConfiguration(OVERRIDE) + @SpringJUnitConfig({ Config1.class, Config2.class, Config3.class }) + @ActiveProfiles("3") + class DoubleNestedWithOverriddenConfigTests { + + @Autowired + List localStrings; + + + @Test + void test() { + assertThat(strings).containsExactlyInAnyOrder("X", "A1"); + assertThat(this.localStrings).containsExactlyInAnyOrder("X", "Y", "Z", "A3"); + } + + + @Nested + @NestedTestConfiguration(INHERIT) + @ActiveProfiles(profiles = "2", inheritProfiles = false) + class TripleNestedWithInheritedConfigButOverriddenProfilesTests { + + @Autowired + List localStrings; + + + @Test + void test() { + assertThat(strings).containsExactlyInAnyOrder("X", "A1"); + assertThat(this.localStrings).containsExactlyInAnyOrder("X", "Y", "Z", "A2"); + } + } + + @Nested + @NestedTestConfiguration(INHERIT) + class TripleNestedWithInheritedConfigAndTestInterfaceTests implements TestInterface { + + @Autowired + List localStrings; + + + @Test + void test() { + assertThat(strings).containsExactlyInAnyOrder("X", "A1"); + assertThat(this.localStrings).containsExactlyInAnyOrder("X", "Y", "Z", "A2", "A3"); + } + } + } + + } + + // ------------------------------------------------------------------------- + + @Configuration + static class Config1 { + + @Bean + String x() { + return "X"; + } + + @Bean + @Profile("1") + String a1() { + return "A1"; + } + } + + @Configuration + static class Config2 { + + @Bean + String y() { + return "Y"; + } + + @Bean + @Profile("2") + String a2() { + return "A2"; + } + } + + @Configuration + static class Config3 { + + @Bean + String z() { + return "Z"; + } + + @Bean + @Profile("3") + String a3() { + return "A3"; + } + } + + @ActiveProfiles("2") + interface TestInterface { + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/NestedTestsWithConstructorInjectionWithSpringAndJUnitJupiterTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/ConstructorInjectionNestedTests.java similarity index 90% rename from spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/NestedTestsWithConstructorInjectionWithSpringAndJUnitJupiterTests.java rename to spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/ConstructorInjectionNestedTests.java index 6478eca91b..c0decfb517 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/NestedTestsWithConstructorInjectionWithSpringAndJUnitJupiterTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/ConstructorInjectionNestedTests.java @@ -28,7 +28,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.test.context.junit.SpringJUnitJupiterTestSuite; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.test.context.junit.jupiter.nested.NestedTestsWithConstructorInjectionWithSpringAndJUnitJupiterTests.TopLevelConfig; +import org.springframework.test.context.junit.jupiter.nested.ConstructorInjectionNestedTests.TopLevelConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -43,15 +43,15 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Sam Brannen * @since 5.0.5 - * @see NestedTestsWithSpringAndJUnitJupiterTests + * @see ContextConfigurationNestedTests * @see org.springframework.test.context.junit4.nested.NestedTestsWithSpringRulesTests */ @SpringJUnitConfig(TopLevelConfig.class) -class NestedTestsWithConstructorInjectionWithSpringAndJUnitJupiterTests { +class ConstructorInjectionNestedTests { final String foo; - NestedTestsWithConstructorInjectionWithSpringAndJUnitJupiterTests(TestInfo testInfo, @Autowired String foo) { + ConstructorInjectionNestedTests(TestInfo testInfo, @Autowired String foo) { this.foo = foo; } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/ContextConfigurationNestedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/ContextConfigurationNestedTests.java new file mode 100644 index 0000000000..80770ba7e4 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/ContextConfigurationNestedTests.java @@ -0,0 +1,212 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.junit.jupiter.nested; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.NestedTestConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.context.junit.jupiter.nested.ContextConfigurationNestedTests.TopLevelConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.INHERIT; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.OVERRIDE; + +/** + * Integration tests that verify support for {@code @Nested} test classes using + * {@link ContextConfiguration @ContextConfiguration} in conjunction with the + * {@link SpringExtension} in a JUnit Jupiter environment. + * + * @author Sam Brannen + * @since 5.0 + * @see ConstructorInjectionNestedTests + * @see org.springframework.test.context.junit4.nested.NestedTestsWithSpringRulesTests + */ +@SpringJUnitConfig(TopLevelConfig.class) +class ContextConfigurationNestedTests { + + private static final String FOO = "foo"; + private static final String BAR = "bar"; + private static final String BAZ = "baz"; + + @Autowired + String foo; + + + @Test + void topLevelTest() { + assertThat(foo).isEqualTo(FOO); + } + + + @Nested + @SpringJUnitConfig(NestedConfig.class) + class NestedTests { + + @Autowired(required = false) + @Qualifier("foo") + String localFoo; + + @Autowired + String bar; + + + @Test + void test() { + // In contrast to nested test classes running in JUnit 4, the foo + // field in the outer instance should have been injected from the + // test ApplicationContext for the outer instance. + assertThat(foo).isEqualTo(FOO); + assertThat(this.localFoo).as("foo bean should not be present").isNull(); + assertThat(this.bar).isEqualTo(BAR); + } + } + + @Nested + @NestedTestConfiguration(INHERIT) + class NestedTestCaseWithInheritedConfigTests { + + @Autowired(required = false) + @Qualifier("foo") + String localFoo; + + @Autowired + String bar; + + + @Test + void test() { + // Since the configuration is inherited, the foo field in the outer instance + // and the bar field in the inner instance should both have been injected + // from the test ApplicationContext for the outer instance. + assertThat(foo).isEqualTo(FOO); + assertThat(this.localFoo).isEqualTo(FOO); + assertThat(this.bar).isEqualTo(FOO); + } + + + @Nested + @NestedTestConfiguration(OVERRIDE) + @SpringJUnitConfig(NestedConfig.class) + class DoubleNestedWithOverriddenConfigTests { + + @Autowired(required = false) + @Qualifier("foo") + String localFoo; + + @Autowired + String bar; + + + @Test + void test() { + // In contrast to nested test classes running in JUnit 4, the foo + // field in the outer instance should have been injected from the + // test ApplicationContext for the outer instance. + assertThat(foo).isEqualTo(FOO); + assertThat(this.localFoo).as("foo bean should not be present").isNull(); + assertThat(this.bar).isEqualTo(BAR); + } + + + @Nested + @NestedTestConfiguration(INHERIT) + class TripleNestedWithInheritedConfigTests { + + @Autowired(required = false) + @Qualifier("foo") + String localFoo; + + @Autowired + String bar; + + + @Test + void test() { + assertThat(foo).isEqualTo(FOO); + assertThat(this.localFoo).as("foo bean should not be present").isNull(); + assertThat(this.bar).isEqualTo(BAR); + } + } + + @Nested + @NestedTestConfiguration(INHERIT) + class TripleNestedWithInheritedConfigAndTestInterfaceTests implements TestInterface { + + @Autowired(required = false) + @Qualifier("foo") + String localFoo; + + @Autowired + String bar; + + @Autowired + String baz; + + + @Test + void test() { + assertThat(foo).isEqualTo(FOO); + assertThat(this.localFoo).as("foo bean should not be present").isNull(); + assertThat(this.bar).isEqualTo(BAR); + assertThat(this.baz).isEqualTo(BAZ); + } + } + } + } + + // ------------------------------------------------------------------------- + + @Configuration + static class TopLevelConfig { + + @Bean + String foo() { + return FOO; + } + } + + @Configuration + static class NestedConfig { + + @Bean + String bar() { + return BAR; + } + } + + @Configuration + static class TestInterfaceConfig { + + @Bean + String baz() { + return BAZ; + } + } + + @ContextConfiguration(classes = TestInterfaceConfig.class) + interface TestInterface { + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/ContextHierarchyNestedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/ContextHierarchyNestedTests.java new file mode 100644 index 0000000000..909af00a37 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/ContextHierarchyNestedTests.java @@ -0,0 +1,235 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.junit.jupiter.nested; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.NestedTestConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.junit.jupiter.nested.ContextHierarchyNestedTests.ParentConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.INHERIT; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.OVERRIDE; + +/** + * Integration tests that verify support for {@code @Nested} test classes using + * {@link ContextHierarchy @ContextHierarchy} in conjunction with the + * {@link SpringExtension} in a JUnit Jupiter environment. + * + * @author Sam Brannen + * @since 5.3 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy(@ContextConfiguration(classes = ParentConfig.class)) +class ContextHierarchyNestedTests { + + private static final String FOO = "foo"; + private static final String BAR = "bar"; + private static final String BAZ = "baz"; + private static final String QUX = "qux"; + + @Autowired + String foo; + + @Autowired + ApplicationContext context; + + + @Test + void topLevelTest() { + assertThat(this.context).as("local ApplicationContext").isNotNull(); + assertThat(this.context.getParent()).as("parent ApplicationContext").isNull(); + + assertThat(foo).isEqualTo(FOO); + } + + @Nested + @ContextConfiguration(classes = NestedConfig.class) + class NestedTests { + + @Autowired + String bar; + + @Autowired + ApplicationContext context; + + + @Test + void nestedTest() throws Exception { + assertThat(this.context).as("local ApplicationContext").isNotNull(); + assertThat(this.context.getParent()).as("parent ApplicationContext").isNull(); + + // In contrast to nested test classes running in JUnit 4, the foo + // field in the outer instance should have been injected from the + // test ApplicationContext for the outer instance. + assertThat(foo).isEqualTo(FOO); + assertThat(this.bar).isEqualTo(BAR); + } + } + + @Nested + @NestedTestConfiguration(INHERIT) + @ContextConfiguration(classes = Child1Config.class) + class NestedTestCaseWithInheritedConfigTests { + + @Autowired + String bar; + + @Autowired + ApplicationContext context; + + + @Test + void nestedTest() throws Exception { + assertThat(this.context).as("local ApplicationContext").isNotNull(); + assertThat(this.context.getParent()).as("parent ApplicationContext").isNotNull(); + + // Since the configuration is inherited, the foo field in the outer instance + // and the bar field in the inner instance should both have been injected + // from the test ApplicationContext for the outer instance. + assertThat(foo).isEqualTo(FOO); + assertThat(this.bar).isEqualTo(BAZ + 1); + assertThat(this.context.getBean("foo", String.class)).as("child foo").isEqualTo(QUX + 1); + } + + @Nested + @NestedTestConfiguration(OVERRIDE) + @ContextHierarchy({ + @ContextConfiguration(classes = ParentConfig.class), + @ContextConfiguration(classes = Child2Config.class) + }) + class DoubleNestedTestCaseWithOverriddenConfigTests { + + @Autowired + String bar; + + @Autowired + ApplicationContext context; + + + @Test + void nestedTest() throws Exception { + assertThat(this.context).as("local ApplicationContext").isNotNull(); + assertThat(this.context.getParent()).as("parent ApplicationContext").isNotNull(); + + assertThat(foo).isEqualTo(FOO); + assertThat(this.bar).isEqualTo(BAZ + 2); + assertThat(this.context.getBean("foo", String.class)).as("child foo").isEqualTo(QUX + 2); + } + + @Nested + @NestedTestConfiguration(INHERIT) + class TripleNestedWithInheritedConfigAndTestInterfaceTests implements TestInterface { + + @Autowired + @Qualifier("foo") + String localFoo; + + @Autowired + String bar; + + @Autowired + ApplicationContext context; + + + @Test + void nestedTest() throws Exception { + assertThat(this.context).as("local ApplicationContext").isNotNull(); + assertThat(this.context.getParent()).as("parent ApplicationContext").isNotNull(); + assertThat(this.context.getParent().getParent()).as("grandparent ApplicationContext").isNotNull(); + + assertThat(foo).isEqualTo(FOO); + assertThat(this.localFoo).isEqualTo("test interface"); + assertThat(this.bar).isEqualTo(BAZ + 2); + assertThat(this.context.getParent().getBean("foo", String.class)).as("child foo").isEqualTo(QUX + 2); + } + } + } + } + + // ------------------------------------------------------------------------- + + @Configuration + static class ParentConfig { + + @Bean + String foo() { + return FOO; + } + } + + @Configuration + static class Child1Config { + + @Bean + String foo() { + return QUX + 1; + } + + @Bean + String bar() { + return BAZ + 1; + } + } + + @Configuration + static class Child2Config { + + @Bean + String foo() { + return QUX + 2; + } + + @Bean + String bar() { + return BAZ + 2; + } + } + + @Configuration + static class NestedConfig { + + @Bean + String bar() { + return BAR; + } + } + + @Configuration + static class TestInterfaceConfig { + + @Bean + String foo() { + return "test interface"; + } + } + + @ContextConfiguration(classes = TestInterfaceConfig.class) + interface TestInterface { + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/NestedTestsWithSpringAndJUnitJupiterTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/NestedTestsWithSpringAndJUnitJupiterTests.java deleted file mode 100644 index b6a8c554fd..0000000000 --- a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/NestedTestsWithSpringAndJUnitJupiterTests.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2002-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.test.context.junit.jupiter.nested; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.junit.SpringJUnitJupiterTestSuite; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.test.context.junit.jupiter.nested.NestedTestsWithSpringAndJUnitJupiterTests.TopLevelConfig; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Integration tests that verify support for {@code @Nested} test classes - * in conjunction with the {@link SpringExtension} in a JUnit Jupiter environment. - * - *

To run these tests in an IDE that does not have built-in support for the JUnit - * Platform, simply run {@link SpringJUnitJupiterTestSuite} as a JUnit 4 test. - * - * @author Sam Brannen - * @since 5.0 - * @see NestedTestsWithConstructorInjectionWithSpringAndJUnitJupiterTests - * @see org.springframework.test.context.junit4.nested.NestedTestsWithSpringRulesTests - */ -@SpringJUnitConfig(TopLevelConfig.class) -class NestedTestsWithSpringAndJUnitJupiterTests { - - @Autowired - String foo; - - - @Test - void topLevelTest() { - assertThat(foo).isEqualTo("foo"); - } - - - @Nested - @SpringJUnitConfig(NestedConfig.class) - class NestedTests { - - @Autowired - String bar; - - - @Test - void nestedTest() throws Exception { - // In contrast to nested test classes running in JUnit 4, the foo - // field in the outer instance should have been injected from the - // test ApplicationContext for the outer instance. - assertThat(foo).isEqualTo("foo"); - assertThat(bar).isEqualTo("bar"); - } - } - - // ------------------------------------------------------------------------- - - @Configuration - static class TopLevelConfig { - - @Bean - String foo() { - return "foo"; - } - } - - @Configuration - static class NestedConfig { - - @Bean - String bar() { - return "bar"; - } - } - -} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/NestedTestsWithSqlScriptsAndJUnitJupiterTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/SqlScriptNestedTests.java similarity index 89% rename from spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/NestedTestsWithSqlScriptsAndJUnitJupiterTests.java rename to spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/SqlScriptNestedTests.java index d0174f06e5..bea6c8dc52 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/NestedTestsWithSqlScriptsAndJUnitJupiterTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/SqlScriptNestedTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ import static org.assertj.core.api.Assertions.assertThat; @SpringJUnitConfig(PopulatedSchemaDatabaseConfig.class) @Transactional @TestInstance(Lifecycle.PER_CLASS) -class NestedTestsWithSqlScriptsAndJUnitJupiterTests { +class SqlScriptNestedTests { @Autowired JdbcTemplate jdbcTemplate; @@ -67,6 +67,10 @@ class NestedTestsWithSqlScriptsAndJUnitJupiterTests { } @Nested + // NOTE: the following @SpringJUnitConfig declaration must NOT be removed. + // This was added before the TestContext framework looked up configuration + // on enclosing classes for @Nested test classes. As such, this serves as a + // regression test and cannot be changed. @SpringJUnitConfig(PopulatedSchemaDatabaseConfig.class) @Transactional class NestedTests { diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/TestConstructorNestedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/TestConstructorNestedTests.java new file mode 100644 index 0000000000..26aee5af30 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/TestConstructorNestedTests.java @@ -0,0 +1,169 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.junit.jupiter.nested; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.NestedTestConfiguration; +import org.springframework.test.context.TestConstructor; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.INHERIT; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.OVERRIDE; +import static org.springframework.test.context.TestConstructor.AutowireMode.ALL; +import static org.springframework.test.context.TestConstructor.AutowireMode.ANNOTATED; + +/** + * Integration tests that verify support for {@code @Nested} test classes using + * {@link TestConstructor @TestConstructor} in conjunction with the + * {@link SpringExtension} in a JUnit Jupiter environment. + * + * @author Sam Brannen + * @since 5.3 + */ +@SpringJUnitConfig +@TestConstructor(autowireMode = ALL) +class TestConstructorNestedTests { + + TestConstructorNestedTests(String text) { + assertThat(text).isEqualTo("enigma"); + } + + @Test + void test() { + } + + + @Nested + @SpringJUnitConfig(Config.class) + @TestConstructor(autowireMode = ANNOTATED) + class ConfigOverriddenByDefaultTests { + + @Autowired + ConfigOverriddenByDefaultTests(String text) { + assertThat(text).isEqualTo("enigma"); + } + + @Test + void test() { + } + } + + @Nested + @NestedTestConfiguration(INHERIT) + class InheritedConfigTests { + + InheritedConfigTests(String text) { + assertThat(text).isEqualTo("enigma"); + } + + @Test + void test() { + } + + + @Nested + class DoubleNestedWithImplicitlyInheritedConfigTests { + + DoubleNestedWithImplicitlyInheritedConfigTests(String text) { + assertThat(text).isEqualTo("enigma"); + } + + @Test + void test() { + } + + + @Nested + class TripleNestedWithImplicitlyInheritedConfigTests { + + TripleNestedWithImplicitlyInheritedConfigTests(String text) { + assertThat(text).isEqualTo("enigma"); + } + + @Test + void test() { + } + } + } + + @Nested + @NestedTestConfiguration(OVERRIDE) + @SpringJUnitConfig(Config.class) + @TestConstructor(autowireMode = ANNOTATED) + class DoubleNestedWithOverriddenConfigTests { + + DoubleNestedWithOverriddenConfigTests(@Autowired String text) { + assertThat(text).isEqualTo("enigma"); + } + + @Test + void test() { + } + + + @Nested + @NestedTestConfiguration(INHERIT) + class TripleNestedWithInheritedConfigTests { + + @Autowired + TripleNestedWithInheritedConfigTests(String text) { + assertThat(text).isEqualTo("enigma"); + } + + @Test + void test() { + } + } + + @Nested + @NestedTestConfiguration(INHERIT) + class TripleNestedWithInheritedConfigAndTestInterfaceTests implements TestInterface { + + TripleNestedWithInheritedConfigAndTestInterfaceTests(String text) { + assertThat(text).isEqualTo("enigma"); + } + + @Test + void test() { + } + } + } + } + + // ------------------------------------------------------------------------- + + @Configuration + static class Config { + + @Bean + String text() { + return "enigma"; + } + } + + @TestConstructor(autowireMode = ALL) + interface TestInterface { + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/TestExecutionListenersNestedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/TestExecutionListenersNestedTests.java new file mode 100644 index 0000000000..c41d1b036f --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/TestExecutionListenersNestedTests.java @@ -0,0 +1,192 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.junit.jupiter.nested; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.NestedTestConfiguration; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.context.junit.jupiter.nested.TestExecutionListenersNestedTests.FooTestExecutionListener; +import org.springframework.test.context.support.AbstractTestExecutionListener; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.INHERIT; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.OVERRIDE; + +/** + * Integration tests that verify support for {@code @Nested} test classes using + * {@link TestExecutionListeners @TestExecutionListeners} in conjunction with the + * {@link SpringExtension} in a JUnit Jupiter environment. + * + * @author Sam Brannen + * @since 5.3 + */ +@SpringJUnitConfig +@TestExecutionListeners(FooTestExecutionListener.class) +class TestExecutionListenersNestedTests { + + private static final String FOO = "foo"; + private static final String BAR = "bar"; + private static final String BAZ = "baz"; + private static final String QUX = "qux"; + + private static final List listeners = new ArrayList<>(); + + + @AfterEach + void resetListeners() { + listeners.clear(); + } + + @Test + void test() { + assertThat(listeners).containsExactly(FOO); + } + + + @Nested + @NestedTestConfiguration(INHERIT) + class InheritedConfigTests { + + @Test + void test() { + assertThat(listeners).containsExactly(FOO); + } + } + + @Nested + @SpringJUnitConfig(Config.class) + @TestExecutionListeners(BarTestExecutionListener.class) + class ConfigOverriddenByDefaultTests { + + @Test + void test() { + assertThat(listeners).containsExactly(BAR); + } + } + + @Nested + @NestedTestConfiguration(INHERIT) + @SpringJUnitConfig(Config.class) + @TestExecutionListeners(BarTestExecutionListener.class) + class InheritedAndExtendedConfigTests { + + @Test + void test() { + assertThat(listeners).containsExactly(FOO, BAR); + } + + + @Nested + @NestedTestConfiguration(OVERRIDE) + @SpringJUnitConfig(Config.class) + @TestExecutionListeners(BazTestExecutionListener.class) + class DoubleNestedWithOverriddenConfigTests { + + @Test + void test() { + assertThat(listeners).containsExactly(BAZ); + } + + + @Nested + @NestedTestConfiguration(INHERIT) + @TestExecutionListeners(listeners = BarTestExecutionListener.class, inheritListeners = false) + class TripleNestedWithInheritedConfigButOverriddenListenersTests { + + @Test + void test() { + assertThat(listeners).containsExactly(BAR); + } + } + + @Nested + @NestedTestConfiguration(INHERIT) + class TripleNestedWithInheritedConfigAndTestInterfaceTests implements TestInterface { + + @Test + void test() { + assertThat(listeners).containsExactly(BAZ, QUX); + } + } + } + + } + + // ------------------------------------------------------------------------- + + @Configuration + static class Config { + /* no user beans required for these tests */ + } + + private static abstract class BaseTestExecutionListener extends AbstractTestExecutionListener { + + protected abstract String name(); + + @Override + public final void beforeTestClass(TestContext testContext) { + listeners.add(name()); + } + } + + static class FooTestExecutionListener extends BaseTestExecutionListener { + + @Override + protected String name() { + return FOO; + } + } + + static class BarTestExecutionListener extends BaseTestExecutionListener { + + @Override + protected String name() { + return BAR; + } + } + + static class BazTestExecutionListener extends BaseTestExecutionListener { + + @Override + protected String name() { + return BAZ; + } + } + + static class QuxTestExecutionListener extends BaseTestExecutionListener { + + @Override + protected String name() { + return QUX; + } + } + + @TestExecutionListeners(QuxTestExecutionListener.class) + interface TestInterface { + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/TestPropertySourceNestedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/TestPropertySourceNestedTests.java new file mode 100644 index 0000000000..2423b63f3c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/TestPropertySourceNestedTests.java @@ -0,0 +1,195 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.junit.jupiter.nested; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.test.context.NestedTestConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.context.junit.jupiter.nested.TestPropertySourceNestedTests.Config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.INHERIT; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.OVERRIDE; + +/** + * Integration tests that verify support for {@code @Nested} test classes using + * {@link TestPropertySource @TestPropertySource} in conjunction with the + * {@link SpringExtension} in a JUnit Jupiter environment. + * + * @author Sam Brannen + * @since 5.3 + */ +@SpringJUnitConfig(Config.class) +@TestPropertySource(properties = "p1 = v1") +class TestPropertySourceNestedTests { + + @Autowired + Environment env1; + + + @Test + void propertiesInEnvironment() { + assertThat(env1.getProperty("p1")).isEqualTo("v1"); + } + + + @Nested + @NestedTestConfiguration(INHERIT) + class InheritedConfigTests { + + @Autowired + Environment env2; + + + @Test + void propertiesInEnvironment() { + assertThat(env1.getProperty("p1")).isEqualTo("v1"); + assertThat(env2.getProperty("p1")).isEqualTo("v1"); + assertThat(env1).isSameAs(env2); + } + } + + @Nested + @SpringJUnitConfig(Config.class) + @TestPropertySource(properties = "p2 = v2") + class ConfigOverriddenByDefaultTests { + + @Autowired + Environment env2; + + + @Test + void propertiesInEnvironment() { + assertThat(env1.getProperty("p1")).isEqualTo("v1"); + assertThat(env1).isNotSameAs(env2); + assertThat(env2.getProperty("p1")).isNull(); + assertThat(env2.getProperty("p2")).isEqualTo("v2"); + } + } + + @Nested + @NestedTestConfiguration(INHERIT) + @TestPropertySource(properties = "p2a = v2a") + @TestPropertySource(properties = "p2b = v2b") + class InheritedAndExtendedConfigTests { + + @Autowired + Environment env2; + + + @Test + void propertiesInEnvironment() { + assertThat(env1.getProperty("p1")).isEqualTo("v1"); + assertThat(env1).isNotSameAs(env2); + assertThat(env2.getProperty("p1")).isEqualTo("v1"); + assertThat(env2.getProperty("p2a")).isEqualTo("v2a"); + assertThat(env2.getProperty("p2b")).isEqualTo("v2b"); + } + + + @Nested + @NestedTestConfiguration(OVERRIDE) + @SpringJUnitConfig(Config.class) + @TestPropertySource(properties = "p3 = v3") + class L3WithOverriddenConfigTests { + + @Autowired + Environment env3; + + + @Test + void propertiesInEnvironment() { + assertThat(env1.getProperty("p1")).isEqualTo("v1"); + assertThat(env1).isNotSameAs(env2); + assertThat(env2.getProperty("p1")).isEqualTo("v1"); + assertThat(env2.getProperty("p2a")).isEqualTo("v2a"); + assertThat(env2.getProperty("p2b")).isEqualTo("v2b"); + assertThat(env2).isNotSameAs(env3); + assertThat(env3.getProperty("p1")).isNull(); + assertThat(env3.getProperty("p2")).isNull(); + assertThat(env3.getProperty("p3")).isEqualTo("v3"); + } + + + @Nested + @NestedTestConfiguration(INHERIT) + @TestPropertySource(properties = {"p3 = v34", "p4 = v4"}, inheritProperties = false) + class L4WithInheritedConfigButOverriddenTestPropertiesTests { + + @Autowired + Environment env4; + + + @Test + void propertiesInEnvironment() { + assertThat(env1.getProperty("p1")).isEqualTo("v1"); + assertThat(env1).isNotSameAs(env2); + assertThat(env2.getProperty("p1")).isEqualTo("v1"); + assertThat(env2.getProperty("p2a")).isEqualTo("v2a"); + assertThat(env2.getProperty("p2b")).isEqualTo("v2b"); + assertThat(env2).isNotSameAs(env3); + assertThat(env3.getProperty("p1")).isNull(); + assertThat(env3.getProperty("p2")).isNull(); + assertThat(env3.getProperty("p3")).isEqualTo("v3"); + assertThat(env3).isNotSameAs(env4); + assertThat(env4.getProperty("p1")).isNull(); + assertThat(env4.getProperty("p2")).isNull(); + assertThat(env4.getProperty("p3")).isEqualTo("v34"); + assertThat(env4.getProperty("p4")).isEqualTo("v4"); + } + + @Nested + class L5WithInheritedConfigAndTestInterfaceTests implements TestInterface { + + @Autowired + Environment env5; + + + @Test + void propertiesInEnvironment() { + assertThat(env4).isNotSameAs(env5); + assertThat(env5.getProperty("foo")).isEqualTo("bar"); + assertThat(env5.getProperty("enigma")).isEqualTo("42"); + assertThat(env5.getProperty("p1")).isNull(); + assertThat(env5.getProperty("p2")).isNull(); + assertThat(env5.getProperty("p3")).isEqualTo("v34"); + assertThat(env5.getProperty("p4")).isEqualTo("v4"); + } + } + } + } + } + + // ------------------------------------------------------------------------- + + @Configuration + static class Config { + /* no user beans required for these tests */ + } + + @TestPropertySource(properties = { "foo = bar", "enigma: 42" }) + interface TestInterface { + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/TransactionalNestedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/TransactionalNestedTests.java new file mode 100644 index 0000000000..281c4cdcd8 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/TransactionalNestedTests.java @@ -0,0 +1,185 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.junit.jupiter.nested; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.test.annotation.Commit; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.context.NestedTestConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.context.transaction.TestTransaction; +import org.springframework.transaction.TransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.INHERIT; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.OVERRIDE; +import static org.springframework.test.transaction.TransactionAssert.assertThatTransaction; +import static org.springframework.transaction.annotation.Propagation.NOT_SUPPORTED; + +/** + * Integration tests that verify support for {@code @Nested} test classes using + * {@link Transactional @Transactional} in conjunction with the + * {@link SpringExtension} in a JUnit Jupiter environment. + * + * @author Sam Brannen + * @since 5.3 + */ +@SpringJUnitConfig +@Transactional +@Commit +class TransactionalNestedTests { + + @Test + void transactional(@Autowired DataSource dataSource) { + assertThatTransaction().isActive(); + assertThat(dataSource).isNotNull(); + assertCommit(); + } + + + @Nested + @SpringJUnitConfig(Config.class) + class ConfigOverriddenByDefaultTests { + + @Test + void notTransactional(@Autowired DataSource dataSource) { + assertThatTransaction().isNotActive(); + assertThat(dataSource).isNotNull(); + } + } + + @Nested + @NestedTestConfiguration(INHERIT) + class InheritedConfigTests { + + @Test + void transactional(@Autowired DataSource dataSource) { + assertThatTransaction().isActive(); + assertThat(dataSource).isNotNull(); + assertCommit(); + } + + + @Nested + class DoubleNestedWithImplicitlyInheritedConfigTests { + + @Test + void transactional(@Autowired DataSource dataSource) { + assertThatTransaction().isActive(); + assertThat(dataSource).isNotNull(); + assertCommit(); + } + + + @Nested + @Rollback + class TripleNestedWithImplicitlyInheritedConfigTests { + + @Test + void transactional(@Autowired DataSource dataSource) { + assertThatTransaction().isActive(); + assertThat(dataSource).isNotNull(); + assertRollback(); + } + } + } + + @Nested + @NestedTestConfiguration(OVERRIDE) + @SpringJUnitConfig(Config.class) + @Transactional + @Rollback + class DoubleNestedWithOverriddenConfigTests { + + @Test + void transactional(@Autowired DataSource dataSource) { + assertThatTransaction().isActive(); + assertThat(dataSource).isNotNull(); + assertRollback(); + } + + + @Nested + @NestedTestConfiguration(INHERIT) + @Commit + class TripleNestedWithInheritedConfigTests { + + @Test + void transactional(@Autowired DataSource dataSource) { + assertThatTransaction().isActive(); + assertThat(dataSource).isNotNull(); + assertCommit(); + } + } + + @Nested + @NestedTestConfiguration(INHERIT) + class TripleNestedWithInheritedConfigAndTestInterfaceTests implements TestInterface { + + @Test + void notTransactional(@Autowired DataSource dataSource) { + assertThatTransaction().isNotActive(); + assertThat(dataSource).isNotNull(); + } + } + } + } + + + private void assertCommit() { + assertThat(TestTransaction.isFlaggedForRollback()).as("flagged for commit").isFalse(); + } + + private void assertRollback() { + assertThat(TestTransaction.isFlaggedForRollback()).as("flagged for rollback").isTrue(); + } + + // ------------------------------------------------------------------------- + + + @Configuration + @EnableTransactionManagement + static class Config { + + @Bean + TransactionManager transactionManager(DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } + + @Bean + DataSource dataSource() { + return new EmbeddedDatabaseBuilder().generateUniqueName(true).build(); + } + } + + @Transactional(propagation = NOT_SUPPORTED) + interface TestInterface { + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/WebAppConfigurationNestedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/WebAppConfigurationNestedTests.java new file mode 100644 index 0000000000..d62de1b2af --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/WebAppConfigurationNestedTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.junit.jupiter.nested; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.NestedTestConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.context.junit.jupiter.nested.WebAppConfigurationNestedTests.Config; +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.web.context.WebApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.INHERIT; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.OVERRIDE; + +/** + * Integration tests that verify support for {@code @Nested} test classes using + * {@link WebAppConfiguration @WebAppConfiguration} in conjunction with the + * {@link SpringExtension} in a JUnit Jupiter environment. + * + * @author Sam Brannen + * @since 5.0 + * @see ConstructorInjectionNestedTests + * @see org.springframework.test.context.junit4.nested.NestedTestsWithSpringRulesTests + */ +@SpringJUnitWebConfig(Config.class) +class WebAppConfigurationNestedTests { + + @Test + void test(ApplicationContext context) { + assertThat(context).isInstanceOf(WebApplicationContext.class); + } + + + @Nested + @SpringJUnitConfig(Config.class) + class ConfigOverriddenByDefaultTests { + + @Test + void test(ApplicationContext context) { + assertThat(context).isNotInstanceOf(WebApplicationContext.class); + } + } + + @Nested + @SpringJUnitWebConfig(Config.class) + class ConfigOverriddenByDefaultWebTests { + + @Test + void test(ApplicationContext context) { + assertThat(context).isInstanceOf(WebApplicationContext.class); + } + } + + @Nested + @NestedTestConfiguration(INHERIT) + class NestedWithInheritedConfigTests { + + @Test + void test(ApplicationContext context) { + assertThat(context).isInstanceOf(WebApplicationContext.class); + } + + + @Nested + class DoubleNestedWithImplicitlyInheritedConfigWebTests { + + @Test + void test(ApplicationContext context) { + assertThat(context).isInstanceOf(WebApplicationContext.class); + } + } + + @Nested + @NestedTestConfiguration(OVERRIDE) + @SpringJUnitConfig(Config.class) + class DoubleNestedWithOverriddenConfigWebTests { + + @Test + void test(ApplicationContext context) { + assertThat(context).isNotInstanceOf(WebApplicationContext.class); + } + + + @Nested + @NestedTestConfiguration(INHERIT) + class TripleNestedWithInheritedConfigWebTests { + + @Test + void test(ApplicationContext context) { + assertThat(context).isNotInstanceOf(WebApplicationContext.class); + } + } + + @Nested + @NestedTestConfiguration(INHERIT) + class TripleNestedWithInheritedConfigAndTestInterfaceTests implements TestInterface { + + @Test + void test(ApplicationContext context) { + assertThat(context).isInstanceOf(WebApplicationContext.class); + } + } + } + } + + // ------------------------------------------------------------------------- + + @Configuration + static class Config { + } + + @WebAppConfiguration + interface TestInterface { + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/support/AbstractContextConfigurationUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/support/AbstractContextConfigurationUtilsTests.java index f4c843f90e..d7a35fe437 100644 --- a/spring-test/src/test/java/org/springframework/test/context/support/AbstractContextConfigurationUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/AbstractContextConfigurationUtilsTests.java @@ -33,12 +33,16 @@ import org.springframework.test.context.BootstrapTestUtils; import org.springframework.test.context.CacheAwareContextLoaderDelegate; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.ContextLoader; import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.NestedTestConfiguration; import org.springframework.test.context.TestContextBootstrapper; import org.springframework.test.context.web.WebAppConfiguration; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.INHERIT; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.OVERRIDE; /** * Abstract base class for tests involving {@link ContextLoaderUtils}, @@ -68,11 +72,13 @@ abstract class AbstractContextConfigurationUtilsTests { String[] expectedLocations, Class[] expectedClasses, Class expectedContextLoaderClass, boolean expectedInheritLocations) { - assertThat(attributes.getDeclaringClass()).as("declaring class").isEqualTo(expectedDeclaringClass); - assertThat(attributes.getLocations()).as("locations").isEqualTo(expectedLocations); - assertThat(attributes.getClasses()).as("classes").isEqualTo(expectedClasses); - assertThat(attributes.isInheritLocations()).as("inherit locations").isEqualTo(expectedInheritLocations); - assertThat(attributes.getContextLoaderClass()).as("context loader").isEqualTo(expectedContextLoaderClass); + assertSoftly(softly -> { + softly.assertThat(attributes.getDeclaringClass()).as("declaring class").isEqualTo(expectedDeclaringClass); + softly.assertThat(attributes.getLocations()).as("locations").isEqualTo(expectedLocations); + softly.assertThat(attributes.getClasses()).as("classes").isEqualTo(expectedClasses); + softly.assertThat(attributes.isInheritLocations()).as("inherit locations").isEqualTo(expectedInheritLocations); + softly.assertThat(attributes.getContextLoaderClass()).as("context loader").isEqualTo(expectedContextLoaderClass); + }); } void assertMergedConfig(MergedContextConfiguration mergedConfig, Class expectedTestClass, @@ -91,21 +97,22 @@ abstract class AbstractContextConfigurationUtilsTests { Set>> expectedInitializerClasses, Class expectedContextLoaderClass) { - assertThat(mergedConfig).isNotNull(); - assertThat(mergedConfig.getTestClass()).isEqualTo(expectedTestClass); - assertThat(mergedConfig.getLocations()).isNotNull(); - assertThat(mergedConfig.getLocations()).isEqualTo(expectedLocations); - assertThat(mergedConfig.getClasses()).isNotNull(); - assertThat(mergedConfig.getClasses()).isEqualTo(expectedClasses); - assertThat(mergedConfig.getActiveProfiles()).isNotNull(); - if (expectedContextLoaderClass == null) { - assertThat(mergedConfig.getContextLoader()).isNull(); - } - else { - assertThat(mergedConfig.getContextLoader().getClass()).isEqualTo(expectedContextLoaderClass); - } - assertThat(mergedConfig.getContextInitializerClasses()).isNotNull(); - assertThat(mergedConfig.getContextInitializerClasses()).isEqualTo(expectedInitializerClasses); + assertSoftly(softly -> { + softly.assertThat(mergedConfig).as("merged config").isNotNull(); + softly.assertThat(mergedConfig.getTestClass()).as("test class").isEqualTo(expectedTestClass); + softly.assertThat(mergedConfig.getLocations()).as("locations").containsExactly(expectedLocations); + softly.assertThat(mergedConfig.getClasses()).as("classes").containsExactly(expectedClasses); + softly.assertThat(mergedConfig.getActiveProfiles()).as("active profiles").isNotNull(); + + if (expectedContextLoaderClass == null) { + softly.assertThat(mergedConfig.getContextLoader()).as("context loader").isNull(); + } + else { + softly.assertThat(mergedConfig.getContextLoader().getClass()).as("context loader").isEqualTo(expectedContextLoaderClass); + } + softly.assertThat(mergedConfig.getContextInitializerClasses()).as("context initializers").isNotNull(); + softly.assertThat(mergedConfig.getContextInitializerClasses()).as("context initializers").isEqualTo(expectedInitializerClasses); + }); } @SafeVarargs @@ -130,6 +137,14 @@ abstract class AbstractContextConfigurationUtilsTests { static class BarConfig { } + @Configuration + static class BazConfig { + } + + @Configuration + static class QuuxConfig { + } + @ContextConfiguration("/foo.xml") @ActiveProfiles(profiles = "foo") @Retention(RetentionPolicy.RUNTIME) @@ -219,4 +234,46 @@ abstract class AbstractContextConfigurationUtilsTests { static class PropertiesClassesFoo { } + @ContextConfiguration(classes = FooConfig.class, loader = AnnotationConfigContextLoader.class) + @NestedTestConfiguration(INHERIT) + static class OuterTestCase { + + class NestedTestCaseWithInheritedConfig { + } + + @ContextConfiguration(classes = BarConfig.class) + class NestedTestCaseWithMergedInheritedConfig { + } + + @NestedTestConfiguration(OVERRIDE) + @ContextConfiguration(classes = BarConfig.class) + class NestedTestCaseWithOverriddenConfig { + + @NestedTestConfiguration(INHERIT) + class DoubleNestedTestCaseWithInheritedOverriddenConfig { + } + } + + } + + @ContextHierarchy({ // + @ContextConfiguration(classes = FooConfig.class, loader = AnnotationConfigContextLoader.class, name = "foo"), // + @ContextConfiguration(classes = BarConfig.class, loader = AnnotationConfigContextLoader.class, name = "bar")// + }) + @NestedTestConfiguration(INHERIT) + static class ContextHierarchyOuterTestCase { + + class NestedTestCaseWithInheritedConfig { + } + + @ContextConfiguration(classes = BazConfig.class, loader = AnnotationConfigContextLoader.class, name = "bar") + class NestedTestCaseWithMergedInheritedConfig { + } + + @ContextConfiguration(classes = QuuxConfig.class, loader = AnnotationConfigContextLoader.class, name = "foo", inheritLocations = false) + class NestedTestCaseWithOverriddenConfig { + } + + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/support/BootstrapTestUtilsMergedConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/support/BootstrapTestUtilsMergedConfigTests.java index c235e7f2ca..8462555acb 100644 --- a/spring-test/src/test/java/org/springframework/test/context/support/BootstrapTestUtilsMergedConfigTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/BootstrapTestUtilsMergedConfigTests.java @@ -21,6 +21,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.assertj.core.api.AssertionsForClassTypes; import org.junit.jupiter.api.Test; import org.springframework.test.context.BootstrapTestUtils; @@ -220,6 +221,137 @@ class BootstrapTestUtilsMergedConfigTests extends AbstractContextConfigurationUt assertThat(mergedConfig.getPropertySourceProperties()).isEmpty(); } + /** + * @since 5.3 + */ + @Test + public void buildMergedConfigForNestedTestClassWithInheritedConfig() { + Class testClass = OuterTestCase.NestedTestCaseWithInheritedConfig.class; + Class[] expectedClasses = array(FooConfig.class); + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); + + assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, expectedClasses, + AnnotationConfigContextLoader.class); + } + + /** + * @since 5.3 + */ + @Test + public void buildMergedConfigForNestedTestClassWithMergedInheritedConfig() { + Class testClass = OuterTestCase.NestedTestCaseWithMergedInheritedConfig.class; + Class[] expectedClasses = array(FooConfig.class, BarConfig.class); + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); + + assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, expectedClasses, + AnnotationConfigContextLoader.class); + } + + /** + * @since 5.3 + */ + @Test + public void buildMergedConfigForNestedTestClassWithOverriddenConfig() { + Class testClass = OuterTestCase.NestedTestCaseWithOverriddenConfig.class; + Class[] expectedClasses = array(BarConfig.class); + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); + + assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, expectedClasses, + DelegatingSmartContextLoader.class); + } + + /** + * @since 5.3 + */ + @Test + public void buildMergedConfigForDoubleNestedTestClassWithInheritedOverriddenConfig() { + Class testClass = OuterTestCase.NestedTestCaseWithOverriddenConfig.DoubleNestedTestCaseWithInheritedOverriddenConfig.class; + Class[] expectedClasses = array(BarConfig.class); + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); + + assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, expectedClasses, + DelegatingSmartContextLoader.class); + } + + /** + * @since 5.3 + */ + @Test + public void buildMergedConfigForContextHierarchy() { + Class testClass = ContextHierarchyOuterTestCase.class; + Class[] expectedClasses = array(BarConfig.class); + + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); + assertThat(mergedConfig).as("merged config").isNotNull(); + + MergedContextConfiguration parent = mergedConfig.getParent(); + assertThat(parent).as("parent config").isNotNull(); + // The following does not work -- at least not in Eclipse. + // asssertThat(parent.getClasses())... + // So we use AssertionsForClassTypes directly. + AssertionsForClassTypes.assertThat(parent.getClasses()).containsExactly(FooConfig.class); + + assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, expectedClasses, + AnnotationConfigContextLoader.class); + } + + /** + * @since 5.3 + */ + @Test + public void buildMergedConfigForNestedTestClassWithInheritedConfigForContextHierarchy() { + Class enclosingTestClass = ContextHierarchyOuterTestCase.class; + Class testClass = ContextHierarchyOuterTestCase.NestedTestCaseWithInheritedConfig.class; + Class[] expectedClasses = array(BarConfig.class); + + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); + assertThat(mergedConfig).as("merged config").isNotNull(); + + MergedContextConfiguration parent = mergedConfig.getParent(); + assertThat(parent).as("parent config").isNotNull(); + AssertionsForClassTypes.assertThat(parent.getClasses()).containsExactly(FooConfig.class); + + assertMergedConfig(mergedConfig, enclosingTestClass, EMPTY_STRING_ARRAY, expectedClasses, + AnnotationConfigContextLoader.class); + } + + /** + * @since 5.3 + */ + @Test + public void buildMergedConfigForNestedTestClassWithMergedInheritedConfigForContextHierarchy() { + Class testClass = ContextHierarchyOuterTestCase.NestedTestCaseWithMergedInheritedConfig.class; + Class[] expectedClasses = array(BarConfig.class, BazConfig.class); + + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); + assertThat(mergedConfig).as("merged config").isNotNull(); + + MergedContextConfiguration parent = mergedConfig.getParent(); + assertThat(parent).as("parent config").isNotNull(); + AssertionsForClassTypes.assertThat(parent.getClasses()).containsExactly(FooConfig.class); + + assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, expectedClasses, + AnnotationConfigContextLoader.class); + } + + /** + * @since 5.3 + */ + @Test + public void buildMergedConfigForNestedTestClassWithOverriddenConfigForContextHierarchy() { + Class testClass = ContextHierarchyOuterTestCase.NestedTestCaseWithOverriddenConfig.class; + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); + assertThat(mergedConfig).as("merged config").isNotNull(); + + MergedContextConfiguration parent = mergedConfig.getParent(); + assertThat(parent).as("parent config").isNotNull(); + + assertMergedConfig(parent, testClass, EMPTY_STRING_ARRAY, array(QuuxConfig.class), + AnnotationConfigContextLoader.class); + assertMergedConfig(mergedConfig, ContextHierarchyOuterTestCase.class, EMPTY_STRING_ARRAY, + array(BarConfig.class), AnnotationConfigContextLoader.class); + } + @ContextConfiguration @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-test/src/test/java/org/springframework/test/context/support/DirtiesContextTestExecutionListenerTests.java b/spring-test/src/test/java/org/springframework/test/context/support/DirtiesContextTestExecutionListenerTests.java index 1096f4758c..f79dfd46a1 100644 --- a/spring-test/src/test/java/org/springframework/test/context/support/DirtiesContextTestExecutionListenerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/DirtiesContextTestExecutionListenerTests.java @@ -19,12 +19,15 @@ package org.springframework.test.context.support; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.BDDMockito; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; +import org.springframework.test.context.NestedTestConfiguration; import org.springframework.test.context.TestContext; import org.springframework.test.context.TestExecutionListener; @@ -40,6 +43,8 @@ import static org.springframework.test.annotation.DirtiesContext.ClassMode.BEFOR import static org.springframework.test.annotation.DirtiesContext.HierarchyMode.CURRENT_LEVEL; import static org.springframework.test.annotation.DirtiesContext.HierarchyMode.EXHAUSTIVE; import static org.springframework.test.annotation.DirtiesContext.MethodMode.BEFORE_METHOD; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.INHERIT; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.OVERRIDE; /** * Unit tests for {@link DirtiesContextBeforeModesTestExecutionListener}. @@ -48,6 +53,7 @@ import static org.springframework.test.annotation.DirtiesContext.MethodMode.BEFO * @author Sam Brannen * @since 4.0 */ +@DisplayName("@DirtiesContext TestExecutionListener tests") class DirtiesContextTestExecutionListenerTests { private final TestExecutionListener beforeListener = new DirtiesContextBeforeModesTestExecutionListener(); @@ -55,12 +61,261 @@ class DirtiesContextTestExecutionListenerTests { private final TestContext testContext = mock(TestContext.class); - @Test - void beforeAndAfterTestMethodForDirtiesContextDeclaredLocallyOnMethodWithBeforeMethodMode() throws Exception { - Class clazz = getClass(); + @Nested + @DisplayName("Before and after test method") + class BeforeAndAfterTestMethodTests { + + @Test + void declaredLocallyOnMethodWithBeforeMethodMode() throws Exception { + Class clazz = getClass().getEnclosingClass(); + BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); + given(testContext.getTestMethod()).willReturn( + clazz.getDeclaredMethod("dirtiesContextDeclaredLocallyWithBeforeMethodMode")); + beforeListener.beforeTestMethod(testContext); + afterListener.beforeTestMethod(testContext); + verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); + afterListener.afterTestMethod(testContext); + beforeListener.afterTestMethod(testContext); + verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); + } + + @Test + void declaredLocallyOnMethodWithAfterMethodMode() throws Exception { + Class clazz = getClass().getEnclosingClass(); + BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); + given(testContext.getTestMethod()).willReturn( + clazz.getDeclaredMethod("dirtiesContextDeclaredLocallyWithAfterMethodMode")); + beforeListener.beforeTestMethod(testContext); + afterListener.beforeTestMethod(testContext); + verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); + afterListener.afterTestMethod(testContext); + beforeListener.afterTestMethod(testContext); + verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); + } + + @Test + void declaredOnMethodViaMetaAnnotationWithAfterMethodMode() throws Exception { + Class clazz = getClass().getEnclosingClass(); + BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); + given(testContext.getTestMethod()).willReturn( + clazz.getDeclaredMethod("dirtiesContextDeclaredViaMetaAnnotationWithAfterMethodMode")); + beforeListener.beforeTestMethod(testContext); + afterListener.beforeTestMethod(testContext); + verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); + afterListener.afterTestMethod(testContext); + beforeListener.afterTestMethod(testContext); + verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); + } + + @Test + void declaredLocallyOnClassBeforeEachTestMethod() throws Exception { + assertBeforeMethod(DirtiesContextDeclaredLocallyBeforeEachTestMethod.class); + } + + @Test + void declaredLocallyOnClassAfterEachTestMethod() throws Exception { + assertAfterMethod(DirtiesContextDeclaredLocallyAfterEachTestMethod.class); + } + + @Test + void declaredViaMetaAnnotationOnClassAfterEachTestMethod() throws Exception { + assertAfterMethod(DirtiesContextDeclaredViaMetaAnnotationAfterEachTestMethod.class); + } + + @Test + void declaredLocallyOnClassBeforeClass() throws Exception { + Class clazz = DirtiesContextDeclaredLocallyBeforeClass.class; + BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); + given(testContext.getTestMethod()).willReturn(clazz.getDeclaredMethod("test")); + beforeListener.beforeTestMethod(testContext); + afterListener.beforeTestMethod(testContext); + afterListener.afterTestMethod(testContext); + beforeListener.afterTestMethod(testContext); + verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); + } + + @Test + void declaredLocallyOnClassAfterClass() throws Exception { + Class clazz = DirtiesContextDeclaredLocallyAfterClass.class; + BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); + given(testContext.getTestMethod()).willReturn(clazz.getDeclaredMethod("test")); + beforeListener.beforeTestMethod(testContext); + afterListener.beforeTestMethod(testContext); + afterListener.afterTestMethod(testContext); + beforeListener.afterTestMethod(testContext); + verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); + } + + @Test + void declaredViaMetaAnnotationOnClassAfterClass() throws Exception { + Class clazz = DirtiesContextDeclaredViaMetaAnnotationAfterClass.class; + BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); + given(testContext.getTestMethod()).willReturn(clazz.getDeclaredMethod("test")); + beforeListener.beforeTestMethod(testContext); + afterListener.beforeTestMethod(testContext); + afterListener.afterTestMethod(testContext); + beforeListener.afterTestMethod(testContext); + verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); + } + + @Test + void beforeAndAfterTestMethodForDirtiesContextViaMetaAnnotationWithOverrides() throws Exception { + Class clazz = DirtiesContextViaMetaAnnotationWithOverrides.class; + BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); + given(testContext.getTestMethod()).willReturn(clazz.getDeclaredMethod("test")); + beforeListener.beforeTestMethod(testContext); + afterListener.beforeTestMethod(testContext); + verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); + afterListener.afterTestMethod(testContext); + beforeListener.afterTestMethod(testContext); + verify(testContext, times(1)).markApplicationContextDirty(CURRENT_LEVEL); + } + } + + @Nested + @DisplayName("Before and after test class") + class BeforeAndAfterTestClassTests { + + @Test + void declaredLocallyOnMethod() throws Exception { + Class clazz = getClass().getEnclosingClass(); + BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); + beforeListener.beforeTestClass(testContext); + afterListener.beforeTestClass(testContext); + afterListener.afterTestClass(testContext); + beforeListener.afterTestClass(testContext); + verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); + } + + @Test + void declaredLocallyOnClassBeforeEachTestMethod() throws Exception { + Class clazz = DirtiesContextDeclaredLocallyBeforeEachTestMethod.class; + BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); + beforeListener.beforeTestClass(testContext); + afterListener.beforeTestClass(testContext); + afterListener.afterTestClass(testContext); + beforeListener.afterTestClass(testContext); + verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); + } + + @Test + void declaredLocallyOnClassAfterEachTestMethod() throws Exception { + Class clazz = DirtiesContextDeclaredLocallyAfterEachTestMethod.class; + BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); + beforeListener.beforeTestClass(testContext); + afterListener.beforeTestClass(testContext); + afterListener.afterTestClass(testContext); + beforeListener.afterTestClass(testContext); + verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); + } + + @Test + void declaredViaMetaAnnotationOnClassAfterEachTestMethod() throws Exception { + Class clazz = DirtiesContextDeclaredViaMetaAnnotationAfterEachTestMethod.class; + BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); + beforeListener.beforeTestClass(testContext); + afterListener.beforeTestClass(testContext); + afterListener.afterTestClass(testContext); + beforeListener.afterTestClass(testContext); + verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); + } + + @Test + void declaredLocallyOnClassBeforeClass() throws Exception { + assertBeforeClass(DirtiesContextDeclaredLocallyBeforeClass.class); + } + + @Test + void declaredLocallyOnClassAfterClass() throws Exception { + assertAfterClass(DirtiesContextDeclaredLocallyAfterClass.class); + } + + @Test + void declaredViaMetaAnnotationOnClassAfterClass() throws Exception { + assertAfterClass(DirtiesContextDeclaredViaMetaAnnotationAfterClass.class); + } + + @Test + void declaredViaMetaAnnotationWithOverrides() throws Exception { + Class clazz = DirtiesContextViaMetaAnnotationWithOverrides.class; + BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); + beforeListener.beforeTestClass(testContext); + afterListener.beforeTestClass(testContext); + afterListener.afterTestClass(testContext); + beforeListener.afterTestClass(testContext); + verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); + } + + @Test + void declaredViaMetaAnnotationWithOverriddenAttributes() throws Exception { + assertAfterClass(DirtiesContextViaMetaAnnotationWithOverridenAttributes.class); + } + } + + @Nested + @DisplayName("Nested config - before and after test method modes") + class NestedBeforeAndAfterTestMethodTests { + + @Test + void onTopLevelClassWithBeforeEachTestMethod() throws Exception { + assertBeforeMethod(BeforeAndAfterTestMethodTopLevelClass.class); + } + + @Test + void onNestedClassWithConfigOverriddenByDefaultWithAfterClass() throws Exception { + assertAfterMethod(BeforeAndAfterTestMethodTopLevelClass.ConfigOverriddenByDefault.class); + } + + @Test + void onNestedClassWithInheritedConfigWithBeforeEachTestMethod() throws Exception { + assertBeforeMethod(BeforeAndAfterTestMethodTopLevelClass.InheritedConfig.class); + } + + @Test + void onNestedClassWithOverriddenConfigWithAfterClass() throws Exception { + assertAfterMethod(BeforeAndAfterTestMethodTopLevelClass.OverriddenConfig.class); + } + + @Test + void onNestedClassWithInheritedConfigButOverriddenWithBeforeEachTestMethod() throws Exception { + assertBeforeMethod(BeforeAndAfterTestMethodTopLevelClass.OverriddenConfig.InheritedConfigButOverridden.class); + } + } + + @Nested + @DisplayName("Nested config - before and after test class modes") + class NestedBeforeAndAfterTestClassTests { + + @Test + void onTopLevelClassWithBeforeClass() throws Exception { + assertBeforeClass(BeforeAndAfterTestClassTopLevelClass.class); + } + + @Test + void onNestedClassWithConfigOverriddenByDefaultWithAfterClass() throws Exception { + assertAfterClass(BeforeAndAfterTestClassTopLevelClass.ConfigOverriddenByDefault.class); + } + + @Test + void onNestedClassWithInheritedConfigWithBeforeClass() throws Exception { + assertBeforeClass(BeforeAndAfterTestClassTopLevelClass.InheritedConfig.class); + } + + @Test + void onNestedClassWithOverriddenConfigWithAfterClass() throws Exception { + assertAfterClass(BeforeAndAfterTestClassTopLevelClass.OverriddenConfig.class); + } + + @Test + void onNestedClassWithInheritedConfigButOverriddenWithBeforeClass() throws Exception { + assertBeforeClass(BeforeAndAfterTestClassTopLevelClass.OverriddenConfig.InheritedConfigButOverridden.class); + } + } + + + private void assertBeforeMethod(Class clazz) throws Exception { BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); - given(testContext.getTestMethod()).willReturn( - clazz.getDeclaredMethod("dirtiesContextDeclaredLocallyWithBeforeMethodMode")); + given(testContext.getTestMethod()).willReturn(clazz.getDeclaredMethod("test")); beforeListener.beforeTestMethod(testContext); afterListener.beforeTestMethod(testContext); verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); @@ -69,12 +324,9 @@ class DirtiesContextTestExecutionListenerTests { verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); } - @Test - void beforeAndAfterTestMethodForDirtiesContextDeclaredLocallyOnMethodWithAfterMethodMode() throws Exception { - Class clazz = getClass(); + private void assertAfterMethod(Class clazz) throws NoSuchMethodException, Exception { BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); - given(testContext.getTestMethod()).willReturn( - clazz.getDeclaredMethod("dirtiesContextDeclaredLocallyWithAfterMethodMode")); + given(testContext.getTestMethod()).willReturn(clazz.getDeclaredMethod("test")); beforeListener.beforeTestMethod(testContext); afterListener.beforeTestMethod(testContext); verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); @@ -83,160 +335,7 @@ class DirtiesContextTestExecutionListenerTests { verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); } - @Test - void beforeAndAfterTestMethodForDirtiesContextDeclaredOnMethodViaMetaAnnotationWithAfterMethodMode() - throws Exception { - Class clazz = getClass(); - BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); - given(testContext.getTestMethod()).willReturn( - clazz.getDeclaredMethod("dirtiesContextDeclaredViaMetaAnnotationWithAfterMethodMode")); - beforeListener.beforeTestMethod(testContext); - afterListener.beforeTestMethod(testContext); - verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); - afterListener.afterTestMethod(testContext); - beforeListener.afterTestMethod(testContext); - verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); - } - - @Test - void beforeAndAfterTestMethodForDirtiesContextDeclaredLocallyOnClassBeforeEachTestMethod() throws Exception { - Class clazz = DirtiesContextDeclaredLocallyBeforeEachTestMethod.class; - BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); - given(testContext.getTestMethod()).willReturn(clazz.getDeclaredMethod("clean")); - beforeListener.beforeTestMethod(testContext); - afterListener.beforeTestMethod(testContext); - verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); - afterListener.afterTestMethod(testContext); - beforeListener.afterTestMethod(testContext); - verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); - } - - @Test - void beforeAndAfterTestMethodForDirtiesContextDeclaredLocallyOnClassAfterEachTestMethod() throws Exception { - Class clazz = DirtiesContextDeclaredLocallyAfterEachTestMethod.class; - BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); - given(testContext.getTestMethod()).willReturn(clazz.getDeclaredMethod("clean")); - beforeListener.beforeTestMethod(testContext); - afterListener.beforeTestMethod(testContext); - verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); - afterListener.afterTestMethod(testContext); - beforeListener.afterTestMethod(testContext); - verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); - } - - @Test - void beforeAndAfterTestMethodForDirtiesContextDeclaredViaMetaAnnotationOnClassAfterEachTestMethod() - throws Exception { - Class clazz = DirtiesContextDeclaredViaMetaAnnotationAfterEachTestMethod.class; - BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); - given(testContext.getTestMethod()).willReturn(clazz.getDeclaredMethod("clean")); - beforeListener.beforeTestMethod(testContext); - afterListener.beforeTestMethod(testContext); - verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); - afterListener.afterTestMethod(testContext); - beforeListener.afterTestMethod(testContext); - verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); - } - - @Test - void beforeAndAfterTestMethodForDirtiesContextDeclaredLocallyOnClassBeforeClass() throws Exception { - Class clazz = DirtiesContextDeclaredLocallyBeforeClass.class; - BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); - given(testContext.getTestMethod()).willReturn(clazz.getDeclaredMethod("clean")); - beforeListener.beforeTestMethod(testContext); - afterListener.beforeTestMethod(testContext); - afterListener.afterTestMethod(testContext); - beforeListener.afterTestMethod(testContext); - verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); - } - - @Test - void beforeAndAfterTestMethodForDirtiesContextDeclaredLocallyOnClassAfterClass() throws Exception { - Class clazz = DirtiesContextDeclaredLocallyAfterClass.class; - BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); - given(testContext.getTestMethod()).willReturn(clazz.getDeclaredMethod("clean")); - beforeListener.beforeTestMethod(testContext); - afterListener.beforeTestMethod(testContext); - afterListener.afterTestMethod(testContext); - beforeListener.afterTestMethod(testContext); - verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); - } - - @Test - void beforeAndAfterTestMethodForDirtiesContextDeclaredViaMetaAnnotationOnClassAfterClass() throws Exception { - Class clazz = DirtiesContextDeclaredViaMetaAnnotationAfterClass.class; - BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); - given(testContext.getTestMethod()).willReturn(clazz.getDeclaredMethod("clean")); - beforeListener.beforeTestMethod(testContext); - afterListener.beforeTestMethod(testContext); - afterListener.afterTestMethod(testContext); - beforeListener.afterTestMethod(testContext); - verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); - } - - @Test - void beforeAndAfterTestMethodForDirtiesContextViaMetaAnnotationWithOverrides() throws Exception { - Class clazz = DirtiesContextViaMetaAnnotationWithOverrides.class; - BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); - given(testContext.getTestMethod()).willReturn(clazz.getDeclaredMethod("clean")); - beforeListener.beforeTestMethod(testContext); - afterListener.beforeTestMethod(testContext); - verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); - afterListener.afterTestMethod(testContext); - beforeListener.afterTestMethod(testContext); - verify(testContext, times(1)).markApplicationContextDirty(CURRENT_LEVEL); - } - - // ------------------------------------------------------------------------- - - @Test - void beforeAndAfterTestClassForDirtiesContextDeclaredLocallyOnMethod() throws Exception { - Class clazz = getClass(); - BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); - beforeListener.beforeTestClass(testContext); - afterListener.beforeTestClass(testContext); - afterListener.afterTestClass(testContext); - beforeListener.afterTestClass(testContext); - verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); - } - - @Test - void beforeAndAfterTestClassForDirtiesContextDeclaredLocallyOnClassBeforeEachTestMethod() throws Exception { - Class clazz = DirtiesContextDeclaredLocallyBeforeEachTestMethod.class; - BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); - beforeListener.beforeTestClass(testContext); - afterListener.beforeTestClass(testContext); - afterListener.afterTestClass(testContext); - beforeListener.afterTestClass(testContext); - verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); - } - - @Test - void beforeAndAfterTestClassForDirtiesContextDeclaredLocallyOnClassAfterEachTestMethod() throws Exception { - Class clazz = DirtiesContextDeclaredLocallyAfterEachTestMethod.class; - BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); - beforeListener.beforeTestClass(testContext); - afterListener.beforeTestClass(testContext); - afterListener.afterTestClass(testContext); - beforeListener.afterTestClass(testContext); - verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); - } - - @Test - void beforeAndAfterTestClassForDirtiesContextDeclaredViaMetaAnnotationOnClassAfterEachTestMethod() - throws Exception { - Class clazz = DirtiesContextDeclaredViaMetaAnnotationAfterEachTestMethod.class; - BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); - beforeListener.beforeTestClass(testContext); - afterListener.beforeTestClass(testContext); - afterListener.afterTestClass(testContext); - beforeListener.afterTestClass(testContext); - verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); - } - - @Test - void beforeAndAfterTestClassForDirtiesContextDeclaredLocallyOnClassBeforeClass() throws Exception { - Class clazz = DirtiesContextDeclaredLocallyBeforeClass.class; + private void assertBeforeClass(Class clazz) throws Exception { BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); beforeListener.beforeTestClass(testContext); afterListener.beforeTestClass(testContext); @@ -246,45 +345,7 @@ class DirtiesContextTestExecutionListenerTests { verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); } - @Test - void beforeAndAfterTestClassForDirtiesContextDeclaredLocallyOnClassAfterClass() throws Exception { - Class clazz = DirtiesContextDeclaredLocallyAfterClass.class; - BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); - beforeListener.beforeTestClass(testContext); - afterListener.beforeTestClass(testContext); - verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); - afterListener.afterTestClass(testContext); - beforeListener.afterTestClass(testContext); - verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); - } - - @Test - void beforeAndAfterTestClassForDirtiesContextDeclaredViaMetaAnnotationOnClassAfterClass() throws Exception { - Class clazz = DirtiesContextDeclaredViaMetaAnnotationAfterClass.class; - BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); - beforeListener.beforeTestClass(testContext); - afterListener.beforeTestClass(testContext); - verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); - afterListener.afterTestClass(testContext); - beforeListener.afterTestClass(testContext); - verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); - } - - @Test - void beforeAndAfterTestClassForDirtiesContextDeclaredViaMetaAnnotationWithOverrides() throws Exception { - Class clazz = DirtiesContextViaMetaAnnotationWithOverrides.class; - BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); - beforeListener.beforeTestClass(testContext); - afterListener.beforeTestClass(testContext); - afterListener.afterTestClass(testContext); - beforeListener.afterTestClass(testContext); - verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); - } - - @Test - void beforeAndAfterTestClassForDirtiesContextDeclaredViaMetaAnnotationWithOverriddenAttributes() - throws Exception { - Class clazz = DirtiesContextViaMetaAnnotationWithOverridenAttributes.class; + private void assertAfterClass(Class clazz) throws Exception { BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); beforeListener.beforeTestClass(testContext); afterListener.beforeTestClass(testContext); @@ -298,54 +359,49 @@ class DirtiesContextTestExecutionListenerTests { @DirtiesContext(methodMode = BEFORE_METHOD) void dirtiesContextDeclaredLocallyWithBeforeMethodMode() { - /* no-op */ } @DirtiesContext void dirtiesContextDeclaredLocallyWithAfterMethodMode() { - /* no-op */ } @MetaDirtyAfterMethod void dirtiesContextDeclaredViaMetaAnnotationWithAfterMethodMode() { - /* no-op */ } @DirtiesContext @Retention(RetentionPolicy.RUNTIME) - static @interface MetaDirtyAfterMethod { + @interface MetaDirtyAfterMethod { } @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) @Retention(RetentionPolicy.RUNTIME) - static @interface MetaDirtyAfterEachTestMethod { + @interface MetaDirtyAfterEachTestMethod { } @DirtiesContext(classMode = AFTER_CLASS) @Retention(RetentionPolicy.RUNTIME) - static @interface MetaDirtyAfterClass { + @interface MetaDirtyAfterClass { } @DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) static class DirtiesContextDeclaredLocallyBeforeEachTestMethod { - void clean() { - /* no-op */ + void test() { } } @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) static class DirtiesContextDeclaredLocallyAfterEachTestMethod { - void clean() { - /* no-op */ + void test() { } } @DirtiesContext @Retention(RetentionPolicy.RUNTIME) - static @interface MetaDirtyWithOverrides { + @interface MetaDirtyWithOverrides { ClassMode classMode() default AFTER_EACH_TEST_METHOD; @@ -355,48 +411,116 @@ class DirtiesContextTestExecutionListenerTests { @MetaDirtyAfterEachTestMethod static class DirtiesContextDeclaredViaMetaAnnotationAfterEachTestMethod { - void clean() { - /* no-op */ + void test() { } } @DirtiesContext(classMode = BEFORE_CLASS) static class DirtiesContextDeclaredLocallyBeforeClass { - void clean() { - /* no-op */ + void test() { } } @DirtiesContext(classMode = AFTER_CLASS) static class DirtiesContextDeclaredLocallyAfterClass { - void clean() { - /* no-op */ + void test() { } } @MetaDirtyAfterClass static class DirtiesContextDeclaredViaMetaAnnotationAfterClass { - void clean() { - /* no-op */ + void test() { } } @MetaDirtyWithOverrides static class DirtiesContextViaMetaAnnotationWithOverrides { - void clean() { - /* no-op */ + void test() { } } @MetaDirtyWithOverrides(classMode = AFTER_CLASS, hierarchyMode = EXHAUSTIVE) static class DirtiesContextViaMetaAnnotationWithOverridenAttributes { - void clean() { - /* no-op */ + void test() { + } + } + + @DirtiesContext(classMode = BEFORE_CLASS) + static class BeforeAndAfterTestClassTopLevelClass { + + void test() { + } + + + @DirtiesContext(classMode = AFTER_CLASS) + class ConfigOverriddenByDefault { + + void test() { + } + } + + @NestedTestConfiguration(INHERIT) + class InheritedConfig { + + void test() { + } + } + + @NestedTestConfiguration(OVERRIDE) + @DirtiesContext(classMode = AFTER_CLASS) + class OverriddenConfig { + + @NestedTestConfiguration(INHERIT) + @DirtiesContext(classMode = BEFORE_CLASS) + class InheritedConfigButOverridden { + + void test() { + } + } + } + } + + @DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) + static class BeforeAndAfterTestMethodTopLevelClass { + + void test() { + } + + + @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) + class ConfigOverriddenByDefault { + + void test() { + } + } + + @NestedTestConfiguration(INHERIT) + class InheritedConfig { + + void test() { + } + } + + @NestedTestConfiguration(OVERRIDE) + @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) + class OverriddenConfig { + + void test() { + } + + + @NestedTestConfiguration(INHERIT) + @DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) + class InheritedConfigButOverridden { + + void test() { + } + } } } diff --git a/spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java index cf859b3111..385180cae2 100644 --- a/spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java @@ -20,6 +20,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Map; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.context.ConfigurableApplicationContext; @@ -80,6 +81,7 @@ class TestPropertySourceUtilsTests { } @Test + @Disabled("Validation for repeated @TestPropertySource annotations has been removed") void repeatedTestPropertySourcesWithConflictingInheritLocationsFlags() { assertThatIllegalArgumentException() .isThrownBy(() -> buildMergedTestPropertySources(RepeatedPropertySourcesWithConflictingInheritLocationsFlags.class)) @@ -89,6 +91,7 @@ class TestPropertySourceUtilsTests { } @Test + @Disabled("Validation for repeated @TestPropertySource annotations has been removed") void repeatedTestPropertySourcesWithConflictingInheritPropertiesFlags() { assertThatIllegalArgumentException() .isThrownBy(() -> buildMergedTestPropertySources(RepeatedPropertySourcesWithConflictingInheritPropertiesFlags.class)) 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 c0a7e22209..2deea9a7b6 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,13 @@ package org.springframework.test.util; import java.lang.annotation.Annotation; -import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.core.annotation.AnnotationUtils; @@ -47,489 +48,489 @@ import static org.springframework.test.util.MetaAnnotationUtils.findAnnotationDe */ class MetaAnnotationUtilsTests { - private void assertAtComponentOnComposedAnnotation( - Class rootDeclaringClass, String name, Class composedAnnotationType) { + @Nested + @DisplayName("findAnnotationDescriptor() tests") + class FindAnnotationDescriptorTests { - assertAtComponentOnComposedAnnotation(rootDeclaringClass, rootDeclaringClass, name, composedAnnotationType); - } + @Test + void findAnnotationDescriptorWithNoAnnotationPresent() { + assertThat(findAnnotationDescriptor(NonAnnotatedInterface.class, Transactional.class)).isNull(); + assertThat(findAnnotationDescriptor(NonAnnotatedClass.class, Transactional.class)).isNull(); + } - private void assertAtComponentOnComposedAnnotation( - Class startClass, Class rootDeclaringClass, String name, Class composedAnnotationType) { + @Test + void findAnnotationDescriptorWithInheritedAnnotationOnClass() { + // Note: @Transactional is inherited + assertThat(findAnnotationDescriptor(InheritedAnnotationClass.class, Transactional.class).getRootDeclaringClass()).isEqualTo(InheritedAnnotationClass.class); + assertThat(findAnnotationDescriptor(SubInheritedAnnotationClass.class, Transactional.class).getRootDeclaringClass()).isEqualTo(InheritedAnnotationClass.class); + } - assertAtComponentOnComposedAnnotation(startClass, rootDeclaringClass, composedAnnotationType, name, composedAnnotationType); - } + @Test + void findAnnotationDescriptorWithInheritedAnnotationOnInterface() { + // Note: @Transactional is inherited + Transactional rawAnnotation = InheritedAnnotationInterface.class.getAnnotation(Transactional.class); + AnnotationDescriptor descriptor = + findAnnotationDescriptor(InheritedAnnotationInterface.class, Transactional.class); + assertThat(descriptor).isNotNull(); + assertThat(descriptor.getRootDeclaringClass()).isEqualTo(InheritedAnnotationInterface.class); + assertThat(descriptor.getDeclaringClass()).isEqualTo(InheritedAnnotationInterface.class); + assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); - private void assertAtComponentOnComposedAnnotation(Class startClass, Class rootDeclaringClass, - Class declaringClass, String name, Class composedAnnotationType) { + descriptor = findAnnotationDescriptor(SubInheritedAnnotationInterface.class, Transactional.class); + assertThat(descriptor).isNotNull(); + assertThat(descriptor.getRootDeclaringClass()).isEqualTo(SubInheritedAnnotationInterface.class); + assertThat(descriptor.getDeclaringClass()).isEqualTo(InheritedAnnotationInterface.class); + assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); - AnnotationDescriptor descriptor = findAnnotationDescriptor(startClass, Component.class); - assertThat(descriptor).as("AnnotationDescriptor should not be null").isNotNull(); - assertThat(descriptor.getRootDeclaringClass()).as("rootDeclaringClass").isEqualTo(rootDeclaringClass); - assertThat(descriptor.getDeclaringClass()).as("declaringClass").isEqualTo(declaringClass); - assertThat(descriptor.getAnnotationType()).as("annotationType").isEqualTo(Component.class); - assertThat(descriptor.getAnnotation().value()).as("component name").isEqualTo(name); - assertThat(descriptor.getComposedAnnotation()).as("composedAnnotation should not be null").isNotNull(); - assertThat(descriptor.getComposedAnnotationType()).as("composedAnnotationType").isEqualTo(composedAnnotationType); - } + descriptor = findAnnotationDescriptor(SubSubInheritedAnnotationInterface.class, Transactional.class); + assertThat(descriptor).isNotNull(); + assertThat(descriptor.getRootDeclaringClass()).isEqualTo(SubSubInheritedAnnotationInterface.class); + assertThat(descriptor.getDeclaringClass()).isEqualTo(InheritedAnnotationInterface.class); + assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); + } - private void assertAtComponentOnComposedAnnotationForMultipleCandidateTypes( - Class startClass, String name, Class composedAnnotationType) { + @Test + void findAnnotationDescriptorForNonInheritedAnnotationOnClass() { + // Note: @Order is not inherited. + assertThat(findAnnotationDescriptor(NonInheritedAnnotationClass.class, Order.class).getRootDeclaringClass()).isEqualTo(NonInheritedAnnotationClass.class); + assertThat(findAnnotationDescriptor(SubNonInheritedAnnotationClass.class, Order.class).getRootDeclaringClass()).isEqualTo(NonInheritedAnnotationClass.class); + } - assertAtComponentOnComposedAnnotationForMultipleCandidateTypes( - startClass, startClass, name, composedAnnotationType); - } + @Test + void findAnnotationDescriptorForNonInheritedAnnotationOnInterface() { + // Note: @Order is not inherited. + Order rawAnnotation = NonInheritedAnnotationInterface.class.getAnnotation(Order.class); - private void assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(Class startClass, - Class rootDeclaringClass, String name, Class composedAnnotationType) { + AnnotationDescriptor descriptor = + findAnnotationDescriptor(NonInheritedAnnotationInterface.class, Order.class); + assertThat(descriptor).isNotNull(); + assertThat(descriptor.getRootDeclaringClass()).isEqualTo(NonInheritedAnnotationInterface.class); + assertThat(descriptor.getDeclaringClass()).isEqualTo(NonInheritedAnnotationInterface.class); + assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); - assertAtComponentOnComposedAnnotationForMultipleCandidateTypes( - startClass, rootDeclaringClass, composedAnnotationType, name, composedAnnotationType); - } + descriptor = findAnnotationDescriptor(SubNonInheritedAnnotationInterface.class, Order.class); + assertThat(descriptor).isNotNull(); + assertThat(descriptor.getRootDeclaringClass()).isEqualTo(SubNonInheritedAnnotationInterface.class); + assertThat(descriptor.getDeclaringClass()).isEqualTo(NonInheritedAnnotationInterface.class); + assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); + } - @SuppressWarnings("unchecked") - private void assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(Class startClass, - Class rootDeclaringClass, Class declaringClass, String name, - Class composedAnnotationType) { + @Test + void findAnnotationDescriptorWithMetaComponentAnnotation() { + assertAtComponentOnComposedAnnotation(HasMetaComponentAnnotation.class, "meta1", Meta1.class); + } - Class annotationType = Component.class; - UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes( - startClass, Service.class, annotationType, Order.class, Transactional.class); - - assertThat(descriptor).as("UntypedAnnotationDescriptor should not be null").isNotNull(); - assertThat(descriptor.getRootDeclaringClass()).as("rootDeclaringClass").isEqualTo(rootDeclaringClass); - assertThat(descriptor.getDeclaringClass()).as("declaringClass").isEqualTo(declaringClass); - assertThat(descriptor.getAnnotationType()).as("annotationType").isEqualTo(annotationType); - assertThat(((Component) descriptor.getAnnotation()).value()).as("component name").isEqualTo(name); - assertThat(descriptor.getComposedAnnotation()).as("composedAnnotation should not be null").isNotNull(); - assertThat(descriptor.getComposedAnnotationType()).as("composedAnnotationType").isEqualTo(composedAnnotationType); - } - - @Test - void findAnnotationDescriptorWithNoAnnotationPresent() { - assertThat(findAnnotationDescriptor(NonAnnotatedInterface.class, Transactional.class)).isNull(); - assertThat(findAnnotationDescriptor(NonAnnotatedClass.class, Transactional.class)).isNull(); - } - - @Test - void findAnnotationDescriptorWithInheritedAnnotationOnClass() { - // Note: @Transactional is inherited - assertThat(findAnnotationDescriptor(InheritedAnnotationClass.class, Transactional.class).getRootDeclaringClass()).isEqualTo(InheritedAnnotationClass.class); - assertThat(findAnnotationDescriptor(SubInheritedAnnotationClass.class, Transactional.class).getRootDeclaringClass()).isEqualTo(InheritedAnnotationClass.class); - } - - @Test - void findAnnotationDescriptorWithInheritedAnnotationOnInterface() { - // Note: @Transactional is inherited - Transactional rawAnnotation = InheritedAnnotationInterface.class.getAnnotation(Transactional.class); - - AnnotationDescriptor descriptor = - findAnnotationDescriptor(InheritedAnnotationInterface.class, Transactional.class); - assertThat(descriptor).isNotNull(); - assertThat(descriptor.getRootDeclaringClass()).isEqualTo(InheritedAnnotationInterface.class); - assertThat(descriptor.getDeclaringClass()).isEqualTo(InheritedAnnotationInterface.class); - assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); - - descriptor = findAnnotationDescriptor(SubInheritedAnnotationInterface.class, Transactional.class); - assertThat(descriptor).isNotNull(); - assertThat(descriptor.getRootDeclaringClass()).isEqualTo(SubInheritedAnnotationInterface.class); - assertThat(descriptor.getDeclaringClass()).isEqualTo(InheritedAnnotationInterface.class); - assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); - - descriptor = findAnnotationDescriptor(SubSubInheritedAnnotationInterface.class, Transactional.class); - assertThat(descriptor).isNotNull(); - assertThat(descriptor.getRootDeclaringClass()).isEqualTo(SubSubInheritedAnnotationInterface.class); - assertThat(descriptor.getDeclaringClass()).isEqualTo(InheritedAnnotationInterface.class); - assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); - } - - @Test - void findAnnotationDescriptorForNonInheritedAnnotationOnClass() { - // Note: @Order is not inherited. - assertThat(findAnnotationDescriptor(NonInheritedAnnotationClass.class, Order.class).getRootDeclaringClass()).isEqualTo(NonInheritedAnnotationClass.class); - assertThat(findAnnotationDescriptor(SubNonInheritedAnnotationClass.class, Order.class).getRootDeclaringClass()).isEqualTo(NonInheritedAnnotationClass.class); - } - - @Test - void findAnnotationDescriptorForNonInheritedAnnotationOnInterface() { - // Note: @Order is not inherited. - Order rawAnnotation = NonInheritedAnnotationInterface.class.getAnnotation(Order.class); - - AnnotationDescriptor descriptor = - findAnnotationDescriptor(NonInheritedAnnotationInterface.class, Order.class); - assertThat(descriptor).isNotNull(); - assertThat(descriptor.getRootDeclaringClass()).isEqualTo(NonInheritedAnnotationInterface.class); - assertThat(descriptor.getDeclaringClass()).isEqualTo(NonInheritedAnnotationInterface.class); - assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); - - descriptor = findAnnotationDescriptor(SubNonInheritedAnnotationInterface.class, Order.class); - assertThat(descriptor).isNotNull(); - assertThat(descriptor.getRootDeclaringClass()).isEqualTo(SubNonInheritedAnnotationInterface.class); - assertThat(descriptor.getDeclaringClass()).isEqualTo(NonInheritedAnnotationInterface.class); - assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); - } - - @Test - void findAnnotationDescriptorWithMetaComponentAnnotation() { - assertAtComponentOnComposedAnnotation(HasMetaComponentAnnotation.class, "meta1", Meta1.class); - } - - @Test - void findAnnotationDescriptorWithLocalAndMetaComponentAnnotation() { - Class annotationType = Component.class; - AnnotationDescriptor descriptor = findAnnotationDescriptor( + @Test + void findAnnotationDescriptorWithLocalAndMetaComponentAnnotation() { + Class annotationType = Component.class; + AnnotationDescriptor descriptor = findAnnotationDescriptor( HasLocalAndMetaComponentAnnotation.class, annotationType); - assertThat(descriptor.getRootDeclaringClass()).isEqualTo(HasLocalAndMetaComponentAnnotation.class); - assertThat(descriptor.getAnnotationType()).isEqualTo(annotationType); - assertThat(descriptor.getComposedAnnotation()).isNull(); - assertThat(descriptor.getComposedAnnotationType()).isNull(); - } + assertThat(descriptor.getRootDeclaringClass()).isEqualTo(HasLocalAndMetaComponentAnnotation.class); + assertThat(descriptor.getAnnotationType()).isEqualTo(annotationType); + assertThat(descriptor.getComposedAnnotation()).isNull(); + assertThat(descriptor.getComposedAnnotationType()).isNull(); + } - @Test - void findAnnotationDescriptorForInterfaceWithMetaAnnotation() { - assertAtComponentOnComposedAnnotation(InterfaceWithMetaAnnotation.class, "meta1", Meta1.class); - } + @Test + void findAnnotationDescriptorForInterfaceWithMetaAnnotation() { + assertAtComponentOnComposedAnnotation(InterfaceWithMetaAnnotation.class, "meta1", Meta1.class); + } - @Test - void findAnnotationDescriptorForClassWithMetaAnnotatedInterface() { - Component rawAnnotation = AnnotationUtils.findAnnotation(ClassWithMetaAnnotatedInterface.class, Component.class); - AnnotationDescriptor descriptor = - findAnnotationDescriptor(ClassWithMetaAnnotatedInterface.class, Component.class); + @Test + void findAnnotationDescriptorForClassWithMetaAnnotatedInterface() { + Component rawAnnotation = AnnotationUtils.findAnnotation(ClassWithMetaAnnotatedInterface.class, Component.class); + AnnotationDescriptor descriptor = + findAnnotationDescriptor(ClassWithMetaAnnotatedInterface.class, Component.class); - assertThat(descriptor).isNotNull(); - assertThat(descriptor.getRootDeclaringClass()).isEqualTo(ClassWithMetaAnnotatedInterface.class); - assertThat(descriptor.getDeclaringClass()).isEqualTo(Meta1.class); - assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); - assertThat(descriptor.getComposedAnnotation().annotationType()).isEqualTo(Meta1.class); - } + assertThat(descriptor).isNotNull(); + assertThat(descriptor.getRootDeclaringClass()).isEqualTo(ClassWithMetaAnnotatedInterface.class); + assertThat(descriptor.getDeclaringClass()).isEqualTo(Meta1.class); + assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); + assertThat(descriptor.getComposedAnnotation().annotationType()).isEqualTo(Meta1.class); + } - @Test - void findAnnotationDescriptorForClassWithLocalMetaAnnotationAndAnnotatedSuperclass() { - AnnotationDescriptor descriptor = findAnnotationDescriptor( + @Test + void findAnnotationDescriptorForClassWithLocalMetaAnnotationAndAnnotatedSuperclass() { + AnnotationDescriptor descriptor = findAnnotationDescriptor( MetaAnnotatedAndSuperAnnotatedContextConfigClass.class, ContextConfiguration.class); - assertThat(descriptor).as("AnnotationDescriptor should not be null").isNotNull(); - assertThat(descriptor.getRootDeclaringClass()).as("rootDeclaringClass").isEqualTo(MetaAnnotatedAndSuperAnnotatedContextConfigClass.class); - assertThat(descriptor.getDeclaringClass()).as("declaringClass").isEqualTo(MetaConfig.class); - assertThat(descriptor.getAnnotationType()).as("annotationType").isEqualTo(ContextConfiguration.class); - assertThat(descriptor.getComposedAnnotation()).as("composedAnnotation should not be null").isNotNull(); - assertThat(descriptor.getComposedAnnotationType()).as("composedAnnotationType").isEqualTo(MetaConfig.class); + assertThat(descriptor).as("AnnotationDescriptor should not be null").isNotNull(); + assertThat(descriptor.getRootDeclaringClass()).as("rootDeclaringClass").isEqualTo(MetaAnnotatedAndSuperAnnotatedContextConfigClass.class); + assertThat(descriptor.getDeclaringClass()).as("declaringClass").isEqualTo(MetaConfig.class); + assertThat(descriptor.getAnnotationType()).as("annotationType").isEqualTo(ContextConfiguration.class); + assertThat(descriptor.getComposedAnnotation()).as("composedAnnotation should not be null").isNotNull(); + assertThat(descriptor.getComposedAnnotationType()).as("composedAnnotationType").isEqualTo(MetaConfig.class); - assertThat(descriptor.getAnnotationAttributes().getClassArray("classes")).as("configured classes").isEqualTo(new Class[] {String.class}); - } + assertThat(descriptor.getAnnotationAttributes().getClassArray("classes")).as("configured classes").isEqualTo(new Class[] {String.class}); + } - @Test - void findAnnotationDescriptorForClassWithLocalMetaAnnotationAndMetaAnnotatedInterface() { - assertAtComponentOnComposedAnnotation(ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, "meta2", Meta2.class); - } + @Test + void findAnnotationDescriptorForClassWithLocalMetaAnnotationAndMetaAnnotatedInterface() { + assertAtComponentOnComposedAnnotation(ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, "meta2", Meta2.class); + } - @Test - void findAnnotationDescriptorForSubClassWithLocalMetaAnnotationAndMetaAnnotatedInterface() { - assertAtComponentOnComposedAnnotation(SubClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, + @Test + void findAnnotationDescriptorForSubClassWithLocalMetaAnnotationAndMetaAnnotatedInterface() { + assertAtComponentOnComposedAnnotation(SubClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, "meta2", Meta2.class); - } + } - /** - * @since 4.0.3 - */ - @Test - void findAnnotationDescriptorOnMetaMetaAnnotatedClass() { - Class startClass = MetaMetaAnnotatedClass.class; - assertAtComponentOnComposedAnnotation(startClass, startClass, Meta2.class, "meta2", MetaMeta.class); - } + /** + * @since 4.0.3 + */ + @Test + void findAnnotationDescriptorOnMetaMetaAnnotatedClass() { + Class startClass = MetaMetaAnnotatedClass.class; + assertAtComponentOnComposedAnnotation(startClass, startClass, Meta2.class, "meta2", MetaMeta.class); + } - /** - * @since 4.0.3 - */ - @Test - void findAnnotationDescriptorOnMetaMetaMetaAnnotatedClass() { - Class startClass = MetaMetaMetaAnnotatedClass.class; - assertAtComponentOnComposedAnnotation(startClass, startClass, Meta2.class, "meta2", MetaMetaMeta.class); - } + /** + * @since 4.0.3 + */ + @Test + void findAnnotationDescriptorOnMetaMetaMetaAnnotatedClass() { + Class startClass = MetaMetaMetaAnnotatedClass.class; + assertAtComponentOnComposedAnnotation(startClass, startClass, Meta2.class, "meta2", MetaMetaMeta.class); + } - /** - * @since 4.0.3 - */ - @Test - void findAnnotationDescriptorOnAnnotatedClassWithMissingTargetMetaAnnotation() { - // InheritedAnnotationClass is NOT annotated or meta-annotated with @Component - AnnotationDescriptor descriptor = findAnnotationDescriptor( + /** + * @since 4.0.3 + */ + @Test + void findAnnotationDescriptorOnAnnotatedClassWithMissingTargetMetaAnnotation() { + // InheritedAnnotationClass is NOT annotated or meta-annotated with @Component + AnnotationDescriptor descriptor = findAnnotationDescriptor( InheritedAnnotationClass.class, Component.class); - assertThat(descriptor).as("Should not find @Component on InheritedAnnotationClass").isNull(); - } + assertThat(descriptor).as("Should not find @Component on InheritedAnnotationClass").isNull(); + } - /** - * @since 4.0.3 - */ - @Test - void findAnnotationDescriptorOnMetaCycleAnnotatedClassWithMissingTargetMetaAnnotation() { - AnnotationDescriptor descriptor = findAnnotationDescriptor( + /** + * @since 4.0.3 + */ + @Test + void findAnnotationDescriptorOnMetaCycleAnnotatedClassWithMissingTargetMetaAnnotation() { + AnnotationDescriptor descriptor = findAnnotationDescriptor( MetaCycleAnnotatedClass.class, Component.class); - assertThat(descriptor).as("Should not find @Component on MetaCycleAnnotatedClass").isNull(); + assertThat(descriptor).as("Should not find @Component on MetaCycleAnnotatedClass").isNull(); + } + + private void assertAtComponentOnComposedAnnotation( + Class rootDeclaringClass, String name, Class composedAnnotationType) { + + assertAtComponentOnComposedAnnotation(rootDeclaringClass, rootDeclaringClass, name, composedAnnotationType); + } + + private void assertAtComponentOnComposedAnnotation( + Class startClass, Class rootDeclaringClass, String name, Class composedAnnotationType) { + + assertAtComponentOnComposedAnnotation(startClass, rootDeclaringClass, composedAnnotationType, name, composedAnnotationType); + } + + private void assertAtComponentOnComposedAnnotation(Class startClass, Class rootDeclaringClass, + Class declaringClass, String name, Class composedAnnotationType) { + + AnnotationDescriptor descriptor = findAnnotationDescriptor(startClass, Component.class); + assertThat(descriptor).as("AnnotationDescriptor should not be null").isNotNull(); + assertThat(descriptor.getRootDeclaringClass()).as("rootDeclaringClass").isEqualTo(rootDeclaringClass); + assertThat(descriptor.getDeclaringClass()).as("declaringClass").isEqualTo(declaringClass); + assertThat(descriptor.getAnnotationType()).as("annotationType").isEqualTo(Component.class); + assertThat(descriptor.getAnnotation().value()).as("component name").isEqualTo(name); + assertThat(descriptor.getComposedAnnotation()).as("composedAnnotation should not be null").isNotNull(); + assertThat(descriptor.getComposedAnnotationType()).as("composedAnnotationType").isEqualTo(composedAnnotationType); + } + } - // ------------------------------------------------------------------------- + @Nested + @DisplayName("findAnnotationDescriptorForTypes() tests") + class FindAnnotationDescriptorForTypesTests { + + @Test + @SuppressWarnings("unchecked") + void findAnnotationDescriptorForTypesWithNoAnnotationPresent() { + assertThat(findAnnotationDescriptorForTypes(NonAnnotatedInterface.class, Transactional.class, Component.class)).isNull(); + assertThat(findAnnotationDescriptorForTypes(NonAnnotatedClass.class, Transactional.class, Order.class)).isNull(); + } + + @Test + @SuppressWarnings("unchecked") + void findAnnotationDescriptorForTypesWithInheritedAnnotationOnClass() { + // Note: @Transactional is inherited + assertThat(findAnnotationDescriptorForTypes(InheritedAnnotationClass.class, Transactional.class).getRootDeclaringClass()).isEqualTo(InheritedAnnotationClass.class); + assertThat(findAnnotationDescriptorForTypes(SubInheritedAnnotationClass.class, Transactional.class).getRootDeclaringClass()).isEqualTo(InheritedAnnotationClass.class); + } + + @Test + @SuppressWarnings("unchecked") + void findAnnotationDescriptorForTypesWithInheritedAnnotationOnInterface() { + // Note: @Transactional is inherited + Transactional rawAnnotation = InheritedAnnotationInterface.class.getAnnotation(Transactional.class); + + UntypedAnnotationDescriptor descriptor = + findAnnotationDescriptorForTypes(InheritedAnnotationInterface.class, Transactional.class); + assertThat(descriptor).isNotNull(); + assertThat(descriptor.getRootDeclaringClass()).isEqualTo(InheritedAnnotationInterface.class); + assertThat(descriptor.getDeclaringClass()).isEqualTo(InheritedAnnotationInterface.class); + assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); + + descriptor = findAnnotationDescriptorForTypes(SubInheritedAnnotationInterface.class, Transactional.class); + assertThat(descriptor).isNotNull(); + assertThat(descriptor.getRootDeclaringClass()).isEqualTo(SubInheritedAnnotationInterface.class); + assertThat(descriptor.getDeclaringClass()).isEqualTo(InheritedAnnotationInterface.class); + assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); + + descriptor = findAnnotationDescriptorForTypes(SubSubInheritedAnnotationInterface.class, Transactional.class); + assertThat(descriptor).isNotNull(); + assertThat(descriptor.getRootDeclaringClass()).isEqualTo(SubSubInheritedAnnotationInterface.class); + assertThat(descriptor.getDeclaringClass()).isEqualTo(InheritedAnnotationInterface.class); + assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); + } + + @Test + @SuppressWarnings("unchecked") + void findAnnotationDescriptorForTypesForNonInheritedAnnotationOnClass() { + // Note: @Order is not inherited. + assertThat(findAnnotationDescriptorForTypes(NonInheritedAnnotationClass.class, Order.class).getRootDeclaringClass()).isEqualTo(NonInheritedAnnotationClass.class); + assertThat(findAnnotationDescriptorForTypes(SubNonInheritedAnnotationClass.class, Order.class).getRootDeclaringClass()).isEqualTo(NonInheritedAnnotationClass.class); + } + + @Test + @SuppressWarnings("unchecked") + void findAnnotationDescriptorForTypesForNonInheritedAnnotationOnInterface() { + // Note: @Order is not inherited. + Order rawAnnotation = NonInheritedAnnotationInterface.class.getAnnotation(Order.class); + + UntypedAnnotationDescriptor descriptor = + findAnnotationDescriptorForTypes(NonInheritedAnnotationInterface.class, Order.class); + assertThat(descriptor).isNotNull(); + assertThat(descriptor.getRootDeclaringClass()).isEqualTo(NonInheritedAnnotationInterface.class); + assertThat(descriptor.getDeclaringClass()).isEqualTo(NonInheritedAnnotationInterface.class); + assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); + + descriptor = findAnnotationDescriptorForTypes(SubNonInheritedAnnotationInterface.class, Order.class); + assertThat(descriptor).isNotNull(); + assertThat(descriptor.getRootDeclaringClass()).isEqualTo(SubNonInheritedAnnotationInterface.class); + assertThat(descriptor.getDeclaringClass()).isEqualTo(NonInheritedAnnotationInterface.class); + assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); + } + + @Test + @SuppressWarnings("unchecked") + void findAnnotationDescriptorForTypesWithLocalAndMetaComponentAnnotation() { + Class annotationType = Component.class; + UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes( + HasLocalAndMetaComponentAnnotation.class, Transactional.class, annotationType, Order.class); + assertThat(descriptor.getRootDeclaringClass()).isEqualTo(HasLocalAndMetaComponentAnnotation.class); + assertThat(descriptor.getAnnotationType()).isEqualTo(annotationType); + assertThat(descriptor.getComposedAnnotation()).isNull(); + assertThat(descriptor.getComposedAnnotationType()).isNull(); + } + + @Test + void findAnnotationDescriptorForTypesWithMetaComponentAnnotation() { + Class startClass = HasMetaComponentAnnotation.class; + assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(startClass, "meta1", Meta1.class); + } + + @Test + @SuppressWarnings("unchecked") + void findAnnotationDescriptorForTypesWithMetaAnnotationWithDefaultAttributes() { + Class startClass = MetaConfigWithDefaultAttributesTestCase.class; + Class annotationType = ContextConfiguration.class; + + UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes(startClass, + Service.class, ContextConfiguration.class, Order.class, Transactional.class); + + assertThat(descriptor).isNotNull(); + assertThat(descriptor.getRootDeclaringClass()).isEqualTo(startClass); + assertThat(descriptor.getAnnotationType()).isEqualTo(annotationType); + assertThat(((ContextConfiguration) descriptor.getAnnotation()).value()).isEqualTo(new Class[] {}); + assertThat(descriptor.getAnnotationAttributes().getClassArray("classes")).isEqualTo(new Class[] {MetaConfig.DevConfig.class, MetaConfig.ProductionConfig.class}); + assertThat(descriptor.getComposedAnnotation()).isNotNull(); + assertThat(descriptor.getComposedAnnotationType()).isEqualTo(MetaConfig.class); + } + + @Test + @SuppressWarnings("unchecked") + void findAnnotationDescriptorForTypesWithMetaAnnotationWithOverriddenAttributes() { + Class startClass = MetaConfigWithOverriddenAttributesTestCase.class; + Class annotationType = ContextConfiguration.class; + + UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes( + startClass, Service.class, ContextConfiguration.class, Order.class, Transactional.class); + + assertThat(descriptor).isNotNull(); + assertThat(descriptor.getRootDeclaringClass()).isEqualTo(startClass); + assertThat(descriptor.getAnnotationType()).isEqualTo(annotationType); + assertThat(((ContextConfiguration) descriptor.getAnnotation()).value()).isEqualTo(new Class[] {}); + assertThat(descriptor.getAnnotationAttributes().getClassArray("classes")).isEqualTo(new Class[] {MetaAnnotationUtilsTests.class}); + assertThat(descriptor.getComposedAnnotation()).isNotNull(); + assertThat(descriptor.getComposedAnnotationType()).isEqualTo(MetaConfig.class); + } + + @Test + void findAnnotationDescriptorForTypesForInterfaceWithMetaAnnotation() { + Class startClass = InterfaceWithMetaAnnotation.class; + assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(startClass, "meta1", Meta1.class); + } + + @Test + @SuppressWarnings("unchecked") + void findAnnotationDescriptorForTypesForClassWithMetaAnnotatedInterface() { + Component rawAnnotation = AnnotationUtils.findAnnotation(ClassWithMetaAnnotatedInterface.class, Component.class); + + UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes( + ClassWithMetaAnnotatedInterface.class, Service.class, Component.class, Order.class, Transactional.class); + + assertThat(descriptor).isNotNull(); + assertThat(descriptor.getRootDeclaringClass()).isEqualTo(ClassWithMetaAnnotatedInterface.class); + assertThat(descriptor.getDeclaringClass()).isEqualTo(Meta1.class); + assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); + assertThat(descriptor.getComposedAnnotation().annotationType()).isEqualTo(Meta1.class); + } + + @Test + void findAnnotationDescriptorForTypesForClassWithLocalMetaAnnotationAndMetaAnnotatedInterface() { + Class startClass = ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class; + assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(startClass, "meta2", Meta2.class); + } + + @Test + void findAnnotationDescriptorForTypesForSubClassWithLocalMetaAnnotationAndMetaAnnotatedInterface() { + assertAtComponentOnComposedAnnotationForMultipleCandidateTypes( + SubClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, + ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, "meta2", Meta2.class); + } + + /** + * @since 4.0.3 + */ + @Test + void findAnnotationDescriptorForTypesOnMetaMetaAnnotatedClass() { + Class startClass = MetaMetaAnnotatedClass.class; + assertAtComponentOnComposedAnnotationForMultipleCandidateTypes( + startClass, startClass, Meta2.class, "meta2", MetaMeta.class); + } + + /** + * @since 4.0.3 + */ + @Test + void findAnnotationDescriptorForTypesOnMetaMetaMetaAnnotatedClass() { + Class startClass = MetaMetaMetaAnnotatedClass.class; + assertAtComponentOnComposedAnnotationForMultipleCandidateTypes( + startClass, startClass, Meta2.class, "meta2", MetaMetaMeta.class); + } + + /** + * @since 4.0.3 + */ + @Test + @SuppressWarnings("unchecked") + void findAnnotationDescriptorForTypesOnAnnotatedClassWithMissingTargetMetaAnnotation() { + // InheritedAnnotationClass is NOT annotated or meta-annotated with @Component, + // @Service, or @Order, but it is annotated with @Transactional. + UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes( + InheritedAnnotationClass.class, Service.class, Component.class, Order.class); + assertThat(descriptor).as("Should not find @Component on InheritedAnnotationClass").isNull(); + } + + /** + * @since 4.0.3 + */ + @Test + @SuppressWarnings("unchecked") + void findAnnotationDescriptorForTypesOnMetaCycleAnnotatedClassWithMissingTargetMetaAnnotation() { + UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes( + MetaCycleAnnotatedClass.class, Service.class, Component.class, Order.class); + assertThat(descriptor).as("Should not find @Component on MetaCycleAnnotatedClass").isNull(); + } + + private void assertAtComponentOnComposedAnnotationForMultipleCandidateTypes( + Class startClass, String name, Class composedAnnotationType) { + + assertAtComponentOnComposedAnnotationForMultipleCandidateTypes( + startClass, startClass, name, composedAnnotationType); + } + + private void assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(Class startClass, + Class rootDeclaringClass, String name, Class composedAnnotationType) { + + assertAtComponentOnComposedAnnotationForMultipleCandidateTypes( + startClass, rootDeclaringClass, composedAnnotationType, name, composedAnnotationType); + } + + @SuppressWarnings("unchecked") + private void assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(Class startClass, + Class rootDeclaringClass, Class declaringClass, String name, + Class composedAnnotationType) { + + Class annotationType = Component.class; + UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes( + startClass, Service.class, annotationType, Order.class, Transactional.class); + + assertThat(descriptor).as("UntypedAnnotationDescriptor should not be null").isNotNull(); + assertThat(descriptor.getRootDeclaringClass()).as("rootDeclaringClass").isEqualTo(rootDeclaringClass); + assertThat(descriptor.getDeclaringClass()).as("declaringClass").isEqualTo(declaringClass); + assertThat(descriptor.getAnnotationType()).as("annotationType").isEqualTo(annotationType); + assertThat(((Component) descriptor.getAnnotation()).value()).as("component name").isEqualTo(name); + assertThat(descriptor.getComposedAnnotation()).as("composedAnnotation should not be null").isNotNull(); + assertThat(descriptor.getComposedAnnotationType()).as("composedAnnotationType").isEqualTo(composedAnnotationType); + } - @Test - @SuppressWarnings("unchecked") - void findAnnotationDescriptorForTypesWithNoAnnotationPresent() { - assertThat(findAnnotationDescriptorForTypes(NonAnnotatedInterface.class, Transactional.class, Component.class)).isNull(); - assertThat(findAnnotationDescriptorForTypes(NonAnnotatedClass.class, Transactional.class, Order.class)).isNull(); } - @Test - @SuppressWarnings("unchecked") - void findAnnotationDescriptorForTypesWithInheritedAnnotationOnClass() { - // Note: @Transactional is inherited - assertThat(findAnnotationDescriptorForTypes(InheritedAnnotationClass.class, Transactional.class).getRootDeclaringClass()).isEqualTo(InheritedAnnotationClass.class); - assertThat(findAnnotationDescriptorForTypes(SubInheritedAnnotationClass.class, Transactional.class).getRootDeclaringClass()).isEqualTo(InheritedAnnotationClass.class); - } - - @Test - @SuppressWarnings("unchecked") - void findAnnotationDescriptorForTypesWithInheritedAnnotationOnInterface() { - // Note: @Transactional is inherited - Transactional rawAnnotation = InheritedAnnotationInterface.class.getAnnotation(Transactional.class); - - UntypedAnnotationDescriptor descriptor = - findAnnotationDescriptorForTypes(InheritedAnnotationInterface.class, Transactional.class); - assertThat(descriptor).isNotNull(); - assertThat(descriptor.getRootDeclaringClass()).isEqualTo(InheritedAnnotationInterface.class); - assertThat(descriptor.getDeclaringClass()).isEqualTo(InheritedAnnotationInterface.class); - assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); - - descriptor = findAnnotationDescriptorForTypes(SubInheritedAnnotationInterface.class, Transactional.class); - assertThat(descriptor).isNotNull(); - assertThat(descriptor.getRootDeclaringClass()).isEqualTo(SubInheritedAnnotationInterface.class); - assertThat(descriptor.getDeclaringClass()).isEqualTo(InheritedAnnotationInterface.class); - assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); - - descriptor = findAnnotationDescriptorForTypes(SubSubInheritedAnnotationInterface.class, Transactional.class); - assertThat(descriptor).isNotNull(); - assertThat(descriptor.getRootDeclaringClass()).isEqualTo(SubSubInheritedAnnotationInterface.class); - assertThat(descriptor.getDeclaringClass()).isEqualTo(InheritedAnnotationInterface.class); - assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); - } - - @Test - @SuppressWarnings("unchecked") - void findAnnotationDescriptorForTypesForNonInheritedAnnotationOnClass() { - // Note: @Order is not inherited. - assertThat(findAnnotationDescriptorForTypes(NonInheritedAnnotationClass.class, Order.class).getRootDeclaringClass()).isEqualTo(NonInheritedAnnotationClass.class); - assertThat(findAnnotationDescriptorForTypes(SubNonInheritedAnnotationClass.class, Order.class).getRootDeclaringClass()).isEqualTo(NonInheritedAnnotationClass.class); - } - - @Test - @SuppressWarnings("unchecked") - void findAnnotationDescriptorForTypesForNonInheritedAnnotationOnInterface() { - // Note: @Order is not inherited. - Order rawAnnotation = NonInheritedAnnotationInterface.class.getAnnotation(Order.class); - - UntypedAnnotationDescriptor descriptor = - findAnnotationDescriptorForTypes(NonInheritedAnnotationInterface.class, Order.class); - assertThat(descriptor).isNotNull(); - assertThat(descriptor.getRootDeclaringClass()).isEqualTo(NonInheritedAnnotationInterface.class); - assertThat(descriptor.getDeclaringClass()).isEqualTo(NonInheritedAnnotationInterface.class); - assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); - - descriptor = findAnnotationDescriptorForTypes(SubNonInheritedAnnotationInterface.class, Order.class); - assertThat(descriptor).isNotNull(); - assertThat(descriptor.getRootDeclaringClass()).isEqualTo(SubNonInheritedAnnotationInterface.class); - assertThat(descriptor.getDeclaringClass()).isEqualTo(NonInheritedAnnotationInterface.class); - assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); - } - - @Test - @SuppressWarnings("unchecked") - void findAnnotationDescriptorForTypesWithLocalAndMetaComponentAnnotation() { - Class annotationType = Component.class; - UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes( - HasLocalAndMetaComponentAnnotation.class, Transactional.class, annotationType, Order.class); - assertThat(descriptor.getRootDeclaringClass()).isEqualTo(HasLocalAndMetaComponentAnnotation.class); - assertThat(descriptor.getAnnotationType()).isEqualTo(annotationType); - assertThat(descriptor.getComposedAnnotation()).isNull(); - assertThat(descriptor.getComposedAnnotationType()).isNull(); - } - - @Test - void findAnnotationDescriptorForTypesWithMetaComponentAnnotation() { - Class startClass = HasMetaComponentAnnotation.class; - assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(startClass, "meta1", Meta1.class); - } - - @Test - @SuppressWarnings("unchecked") - void findAnnotationDescriptorForTypesWithMetaAnnotationWithDefaultAttributes() { - Class startClass = MetaConfigWithDefaultAttributesTestCase.class; - Class annotationType = ContextConfiguration.class; - - UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes(startClass, - Service.class, ContextConfiguration.class, Order.class, Transactional.class); - - assertThat(descriptor).isNotNull(); - assertThat(descriptor.getRootDeclaringClass()).isEqualTo(startClass); - assertThat(descriptor.getAnnotationType()).isEqualTo(annotationType); - assertThat(((ContextConfiguration) descriptor.getAnnotation()).value()).isEqualTo(new Class[] {}); - assertThat(descriptor.getAnnotationAttributes().getClassArray("classes")).isEqualTo(new Class[] {MetaConfig.DevConfig.class, MetaConfig.ProductionConfig.class}); - assertThat(descriptor.getComposedAnnotation()).isNotNull(); - assertThat(descriptor.getComposedAnnotationType()).isEqualTo(MetaConfig.class); - } - - @Test - @SuppressWarnings("unchecked") - void findAnnotationDescriptorForTypesWithMetaAnnotationWithOverriddenAttributes() { - Class startClass = MetaConfigWithOverriddenAttributesTestCase.class; - Class annotationType = ContextConfiguration.class; - - UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes( - startClass, Service.class, ContextConfiguration.class, Order.class, Transactional.class); - - assertThat(descriptor).isNotNull(); - assertThat(descriptor.getRootDeclaringClass()).isEqualTo(startClass); - assertThat(descriptor.getAnnotationType()).isEqualTo(annotationType); - assertThat(((ContextConfiguration) descriptor.getAnnotation()).value()).isEqualTo(new Class[] {}); - assertThat(descriptor.getAnnotationAttributes().getClassArray("classes")).isEqualTo(new Class[] {MetaAnnotationUtilsTests.class}); - assertThat(descriptor.getComposedAnnotation()).isNotNull(); - assertThat(descriptor.getComposedAnnotationType()).isEqualTo(MetaConfig.class); - } - - @Test - void findAnnotationDescriptorForTypesForInterfaceWithMetaAnnotation() { - Class startClass = InterfaceWithMetaAnnotation.class; - assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(startClass, "meta1", Meta1.class); - } - - @Test - @SuppressWarnings("unchecked") - void findAnnotationDescriptorForTypesForClassWithMetaAnnotatedInterface() { - Component rawAnnotation = AnnotationUtils.findAnnotation(ClassWithMetaAnnotatedInterface.class, Component.class); - - UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes( - ClassWithMetaAnnotatedInterface.class, Service.class, Component.class, Order.class, Transactional.class); - - assertThat(descriptor).isNotNull(); - assertThat(descriptor.getRootDeclaringClass()).isEqualTo(ClassWithMetaAnnotatedInterface.class); - assertThat(descriptor.getDeclaringClass()).isEqualTo(Meta1.class); - assertThat(descriptor.getAnnotation()).isEqualTo(rawAnnotation); - assertThat(descriptor.getComposedAnnotation().annotationType()).isEqualTo(Meta1.class); - } - - @Test - void findAnnotationDescriptorForTypesForClassWithLocalMetaAnnotationAndMetaAnnotatedInterface() { - Class startClass = ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class; - assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(startClass, "meta2", Meta2.class); - } - - @Test - void findAnnotationDescriptorForTypesForSubClassWithLocalMetaAnnotationAndMetaAnnotatedInterface() { - assertAtComponentOnComposedAnnotationForMultipleCandidateTypes( - SubClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, - ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, "meta2", Meta2.class); - } - - /** - * @since 4.0.3 - */ - @Test - void findAnnotationDescriptorForTypesOnMetaMetaAnnotatedClass() { - Class startClass = MetaMetaAnnotatedClass.class; - assertAtComponentOnComposedAnnotationForMultipleCandidateTypes( - startClass, startClass, Meta2.class, "meta2", MetaMeta.class); - } - - /** - * @since 4.0.3 - */ - @Test - void findAnnotationDescriptorForTypesOnMetaMetaMetaAnnotatedClass() { - Class startClass = MetaMetaMetaAnnotatedClass.class; - assertAtComponentOnComposedAnnotationForMultipleCandidateTypes( - startClass, startClass, Meta2.class, "meta2", MetaMetaMeta.class); - } - - /** - * @since 4.0.3 - */ - @Test - @SuppressWarnings("unchecked") - void findAnnotationDescriptorForTypesOnAnnotatedClassWithMissingTargetMetaAnnotation() { - // InheritedAnnotationClass is NOT annotated or meta-annotated with @Component, - // @Service, or @Order, but it is annotated with @Transactional. - UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes( - InheritedAnnotationClass.class, Service.class, Component.class, Order.class); - assertThat(descriptor).as("Should not find @Component on InheritedAnnotationClass").isNull(); - } - - /** - * @since 4.0.3 - */ - @Test - @SuppressWarnings("unchecked") - void findAnnotationDescriptorForTypesOnMetaCycleAnnotatedClassWithMissingTargetMetaAnnotation() { - UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes( - MetaCycleAnnotatedClass.class, Service.class, Component.class, Order.class); - assertThat(descriptor).as("Should not find @Component on MetaCycleAnnotatedClass").isNull(); - } - - // ------------------------------------------------------------------------- @Component(value = "meta1") @Order @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) - @Documented - static @interface Meta1 { + @interface Meta1 { } @Component(value = "meta2") @Transactional @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) - @Documented - static @interface Meta2 { + @interface Meta2 { } @Meta2 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) - @Documented @interface MetaMeta { } @MetaMeta @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) - @Documented @interface MetaMetaMeta { } @MetaCycle3 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) - @Documented @interface MetaCycle1 { } @MetaCycle1 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) - @Documented @interface MetaCycle2 { } @MetaCycle2 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) - @Documented @interface MetaCycle3 { } @ContextConfiguration @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) - @Documented - static @interface MetaConfig { + @interface MetaConfig { static class DevConfig { } @@ -554,7 +555,7 @@ class MetaAnnotationUtilsTests { } @Meta1 - static interface InterfaceWithMetaAnnotation { + interface InterfaceWithMetaAnnotation { } static class ClassWithMetaAnnotatedInterface implements InterfaceWithMetaAnnotation { @@ -591,26 +592,26 @@ class MetaAnnotationUtilsTests { // ------------------------------------------------------------------------- @Transactional - static interface InheritedAnnotationInterface { + interface InheritedAnnotationInterface { } - static interface SubInheritedAnnotationInterface extends InheritedAnnotationInterface { + interface SubInheritedAnnotationInterface extends InheritedAnnotationInterface { } - static interface SubSubInheritedAnnotationInterface extends SubInheritedAnnotationInterface { + interface SubSubInheritedAnnotationInterface extends SubInheritedAnnotationInterface { } @Order - static interface NonInheritedAnnotationInterface { + interface NonInheritedAnnotationInterface { } - static interface SubNonInheritedAnnotationInterface extends NonInheritedAnnotationInterface { + interface SubNonInheritedAnnotationInterface extends NonInheritedAnnotationInterface { } static class NonAnnotatedClass { } - static interface NonAnnotatedInterface { + interface NonAnnotatedInterface { } @Transactional