diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java b/spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java
index f3a5d9c377d..2d6ace8f5d8 100644
--- a/spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java
+++ b/spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java
@@ -29,7 +29,6 @@ import org.springframework.core.annotation.AnnotatedElementUtils;
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;
@@ -571,14 +570,21 @@ public abstract class TestContextAnnotationUtils {
* that are present or meta-present on the {@linkplain #getRootDeclaringClass()
* root declaring class} of this descriptor or on any interfaces that the
* root declaring class implements.
+ *
Annotations are returned in the order they are discovered: annotations
+ * on the root declaring class appear first, followed by annotations on
+ * implemented interfaces in the order they are declared in the
+ * {@code implements} clause.
* @return the set of all merged, synthesized {@code Annotations} found,
* or an empty set if none were found
*/
public Set findAllLocalMergedAnnotations() {
- SearchStrategy searchStrategy = SearchStrategy.TYPE_HIERARCHY;
- return MergedAnnotations.from(getRootDeclaringClass(), searchStrategy, RepeatableContainers.none())
+ Class> rootDeclaringClass = getRootDeclaringClass();
+ Set> localSources = new HashSet<>();
+ localSources.add(rootDeclaringClass);
+ Collections.addAll(localSources, rootDeclaringClass.getInterfaces());
+ return MergedAnnotations.from(rootDeclaringClass, SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.none())
.stream(getAnnotationType())
- .filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex))
+ .filter(mergedAnnotation -> localSources.contains(mergedAnnotation.getSource()))
.collect(MergedAnnotationCollectors.toAnnotationSet());
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/TestContextAnnotationUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/TestContextAnnotationUtilsTests.java
index 258e591577d..d9f9a2bb067 100644
--- a/spring-test/src/test/java/org/springframework/test/context/TestContextAnnotationUtilsTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/TestContextAnnotationUtilsTests.java
@@ -274,6 +274,73 @@ class TestContextAnnotationUtilsTests {
}
+ @Nested
+ @DisplayName("findAllLocalMergedAnnotations() tests")
+ class FindAllLocalMergedAnnotationsTests {
+
+ @Test
+ void annotationOnClassOnly() {
+ var descriptor = findAnnotationDescriptor(AnnotatedClass.class, ContextConfiguration.class);
+ assertThat(descriptor).isNotNull();
+ assertThat(descriptor.findAllLocalMergedAnnotations())
+ .singleElement()
+ .satisfies(config -> assertThat(config.classes()).containsExactly(Config1.class));
+ }
+
+ @Test
+ void annotationOnInterfaceOnly() {
+ var descriptor = findAnnotationDescriptor(ClassImplementingAnnotatedInterface.class, ContextConfiguration.class);
+ assertThat(descriptor).isNotNull();
+ assertThat(descriptor.findAllLocalMergedAnnotations())
+ .singleElement()
+ .satisfies(config -> assertThat(config.classes()).containsExactly(Config2.class));
+ }
+
+ @Test
+ void annotationOnClassAndInterface() {
+ var descriptor = findAnnotationDescriptor(AnnotatedClassImplementingAnnotatedInterface.class, ContextConfiguration.class);
+ assertThat(descriptor).isNotNull();
+ assertThat(descriptor.findAllLocalMergedAnnotations())
+ .satisfiesExactly(
+ config1 -> assertThat(config1.classes()).containsExactly(Config1.class),
+ config2 -> assertThat(config2.classes()).containsExactly(Config2.class)
+ );
+ }
+
+ @Test
+ void annotationOnTwoInterfaces() {
+ var descriptor = findAnnotationDescriptor(ClassImplementingTwoAnnotatedInterfaces.class, ContextConfiguration.class);
+ assertThat(descriptor).isNotNull();
+ assertThat(descriptor.findAllLocalMergedAnnotations())
+ .satisfiesExactly(
+ config1 -> assertThat(config1.classes()).containsExactly(Config2.class),
+ config2 -> assertThat(config2.classes()).containsExactly(Config3.class)
+ );
+ }
+
+ @Test
+ void annotationOnSuperclassIsExcluded() {
+ var descriptor = findAnnotationDescriptor(SubAnnotatedClass.class, ContextConfiguration.class);
+ assertThat(descriptor).isNotNull();
+ assertThat(descriptor.getRootDeclaringClass()).isEqualTo(AnnotatedClass.class);
+ assertThat(descriptor.findAllLocalMergedAnnotations())
+ .singleElement()
+ .satisfies(config -> assertThat(config.classes()).containsExactly(Config1.class));
+ }
+
+ @Test
+ void metaAnnotationOnClassAndAnnotationOnInterface() {
+ var descriptor = findAnnotationDescriptor(MetaAnnotatedClassImplementingAnnotatedInterface.class, ContextConfiguration.class);
+ assertThat(descriptor).isNotNull();
+ assertThat(descriptor.findAllLocalMergedAnnotations())
+ .satisfiesExactly(
+ config1 -> assertThat(config1.classes()).containsExactly(DevConfig.class, ProductionConfig.class),
+ config2 -> assertThat(config2.classes()).containsExactly(Config2.class)
+ );
+ }
+
+ }
+
@Nested
@DisplayName("findAnnotationDescriptorForTypes() tests")
class FindAnnotationDescriptorForTypesTests {
@@ -377,7 +444,7 @@ class TestContextAnnotationUtilsTests {
assertThat(descriptor.getAnnotationType()).isEqualTo(annotationType);
assertThat(((ContextConfiguration) descriptor.getAnnotation()).value()).isEmpty();
assertThat(((ContextConfiguration) descriptor.getAnnotation()).classes())
- .containsExactly(MetaConfig.DevConfig.class, MetaConfig.ProductionConfig.class);
+ .containsExactly(DevConfig.class, ProductionConfig.class);
}
@Test
@@ -559,12 +626,12 @@ class TestContextAnnotationUtilsTests {
@AliasFor(annotation = ContextConfiguration.class)
Class>[] classes() default { DevConfig.class, ProductionConfig.class };
- class DevConfig {
- }
+ }
- class ProductionConfig {
- }
+ static class DevConfig {
+ }
+ static class ProductionConfig {
}
// -------------------------------------------------------------------------
@@ -668,4 +735,45 @@ class TestContextAnnotationUtilsTests {
}
}
+ // -------------------------------------------------------------------------
+ // Fixtures for FindAllLocalMergedAnnotationsTests
+
+ static class Config1 {
+ }
+
+ static class Config2 {
+ }
+
+ static class Config3 {
+ }
+
+ @ContextConfiguration(classes = Config2.class)
+ interface AnnotatedInterface {
+ }
+
+ @ContextConfiguration(classes = Config3.class)
+ interface AnotherAnnotatedInterface {
+ }
+
+ @ContextConfiguration(classes = Config1.class)
+ static class AnnotatedClass {
+ }
+
+ static class SubAnnotatedClass extends AnnotatedClass {
+ }
+
+ static class ClassImplementingAnnotatedInterface implements AnnotatedInterface {
+ }
+
+ @ContextConfiguration(classes = Config1.class)
+ static class AnnotatedClassImplementingAnnotatedInterface implements AnnotatedInterface {
+ }
+
+ static class ClassImplementingTwoAnnotatedInterfaces implements AnnotatedInterface, AnotherAnnotatedInterface {
+ }
+
+ @MetaConfig
+ static class MetaAnnotatedClassImplementingAnnotatedInterface implements AnnotatedInterface {
+ }
+
}