diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/annotation/StandardAnnotationCustomizableTypeExcludeFilter.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/annotation/StandardAnnotationCustomizableTypeExcludeFilter.java
index 3c68d037402..79da575ad36 100644
--- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/annotation/StandardAnnotationCustomizableTypeExcludeFilter.java
+++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/annotation/StandardAnnotationCustomizableTypeExcludeFilter.java
@@ -27,6 +27,7 @@ import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
+import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.util.Assert;
/**
@@ -55,8 +56,10 @@ public abstract class StandardAnnotationCustomizableTypeExcludeFilter annotation;
protected StandardAnnotationCustomizableTypeExcludeFilter(Class> testClass) {
- this.annotation = MergedAnnotations.from(testClass, SearchStrategy.INHERITED_ANNOTATIONS)
- .get(getAnnotationType());
+ this.annotation = MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY)
+ .withEnclosingClasses(TestContextAnnotationUtils::searchEnclosingClass)
+ .from(testClass)
+ .get(this.getAnnotationType());
}
protected final MergedAnnotation getAnnotation() {
diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/annotation/TypeExcludeFiltersContextCustomizerFactory.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/annotation/TypeExcludeFiltersContextCustomizerFactory.java
index 04b91385e2e..87817eeeef2 100644
--- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/annotation/TypeExcludeFiltersContextCustomizerFactory.java
+++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/annotation/TypeExcludeFiltersContextCustomizerFactory.java
@@ -19,16 +19,20 @@ package org.springframework.boot.test.context.filter.annotation;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.AotDetector;
import org.springframework.boot.context.TypeExcludeFilter;
+import org.springframework.core.annotation.MergedAnnotation;
+import org.springframework.core.annotation.MergedAnnotations;
+import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.ContextCustomizerFactory;
import org.springframework.test.context.TestContextAnnotationUtils;
-import org.springframework.test.context.TestContextAnnotationUtils.AnnotationDescriptor;
/**
* {@link ContextCustomizerFactory} to support
@@ -45,19 +49,17 @@ class TypeExcludeFiltersContextCustomizerFactory implements ContextCustomizerFac
if (AotDetector.useGeneratedArtifacts()) {
return null;
}
- AnnotationDescriptor descriptor = TestContextAnnotationUtils
- .findAnnotationDescriptor(testClass, TypeExcludeFilters.class);
- if (descriptor == null) {
+ Set> filterClasses = MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY)
+ .withEnclosingClasses(TestContextAnnotationUtils::searchEnclosingClass)
+ .from(testClass)
+ .stream(TypeExcludeFilters.class)
+ .map(MergedAnnotation::synthesize)
+ .flatMap((annotation) -> Arrays.stream(annotation.value()))
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ if (filterClasses.isEmpty()) {
return null;
}
- Class>[] filterClasses = descriptor.getAnnotation().value();
- return createContextCustomizer(descriptor.getRootDeclaringClass(), filterClasses);
- }
-
- @SuppressWarnings("unchecked")
- private ContextCustomizer createContextCustomizer(Class> testClass, Class>[] filterClasses) {
- return new TypeExcludeFiltersContextCustomizer(testClass,
- new LinkedHashSet<>(Arrays.asList((Class extends TypeExcludeFilter>[]) filterClasses)));
+ return new TypeExcludeFiltersContextCustomizer(testClass, filterClasses);
}
}
diff --git a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/filter/annotation/TypeExcludeFiltersContextCustomizerFactoryTests.java b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/filter/annotation/TypeExcludeFiltersContextCustomizerFactoryTests.java
index 6bc3411eada..8b6a9fe8d98 100644
--- a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/filter/annotation/TypeExcludeFiltersContextCustomizerFactoryTests.java
+++ b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/filter/annotation/TypeExcludeFiltersContextCustomizerFactoryTests.java
@@ -16,20 +16,26 @@
package org.springframework.boot.test.context.filter.annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
import java.util.Collections;
+import org.assertj.core.api.InstanceOfAssertFactories;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.TypeExcludeFilter;
-import org.springframework.boot.test.context.filter.annotation.TypeExcludeFiltersContextCustomizerFactoryTests.EnclosingClass.WithEnclosingClassExcludeFilters;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
-import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.MergedContextConfiguration;
+import org.springframework.test.context.NestedTestConfiguration;
+import org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
@@ -63,11 +69,18 @@ class TypeExcludeFiltersContextCustomizerFactoryTests {
@Test
void getContextCustomizerWhenEnclosingClassHasAnnotationShouldReturnCustomizer() {
- ContextCustomizer customizer = this.factory.createContextCustomizer(WithEnclosingClassExcludeFilters.class,
- Collections.emptyList());
+ ContextCustomizer customizer = this.factory
+ .createContextCustomizer(EnclosingClass.WithEnclosingClassExcludeFilters.class, Collections.emptyList());
assertThat(customizer).isNotNull();
}
+ @Test
+ void getContextCustomizerWhenEnclosingClassHasAnnotationButNestedConfigurationIsOverrideShouldReturnNull() {
+ ContextCustomizer customizer = this.factory
+ .createContextCustomizer(EnclosingWithOverride.InnerWithOverride.class, Collections.emptyList());
+ assertThat(customizer).isNull();
+ }
+
@Test
void hashCodeAndEquals() {
ContextCustomizer customizer1 = this.factory.createContextCustomizer(WithExcludeFilters.class,
@@ -82,19 +95,54 @@ class TypeExcludeFiltersContextCustomizerFactoryTests {
@Test
void getContextCustomizerShouldAddExcludeFilters() throws Exception {
- ContextCustomizer customizer = this.factory.createContextCustomizer(WithExcludeFilters.class,
+ typeExcludeFiltersFor(WithExcludeFilters.class).doesNotMatch(NoAnnotation.class)
+ .matches(SimpleExclude.class, TestClassAwareExclude.class);
+ }
+
+ @Test
+ void getContextCustomizerWhenEnclosingClassHasAnnotationShouldAddExcludeFilters() throws Exception {
+ typeExcludeFiltersFor(EnclosingClass.WithEnclosingClassExcludeFilters.class).matches(SimpleExclude.class,
+ TestClassAwareExclude.class);
+ }
+
+ @Test
+ void getContextCustomizerWhenHasDuplicateSliceExcludeFilterShouldInstantiateItOnce() {
+ ContextCustomizer customizer = this.factory.createContextCustomizer(WithDuplicateSliceExclude.class,
Collections.emptyList());
+ assertThat(customizer).extracting("filters", InstanceOfAssertFactories.collection(TypeExcludeFilter.class))
+ .hasSize(1);
+ }
+
+ @Test
+ void getContextCustomizerWhenEnclosingClassHasAnnotationsTypeExcludeFilters() throws Exception {
+ typeExcludeFiltersFor(WithMultipleExcludeFilterAnnotations.WithEnclosingClassExcludeFilters.class)
+ .matches(FirstSliceExclude.class, SecondSliceExclude.class);
+
+ }
+
+ @Test
+ void getContextCustomizerWhenSuperclassHasAnnotationShouldIncludeTypeExcludeFilters() throws Exception {
+ typeExcludeFiltersFor(WithMixedInheritance.class).matches(TestClassAwareExclude.class, FirstSliceExclude.class,
+ SecondSliceExclude.class, ThirdSliceExclude.class);
+ }
+
+ @Test
+ void getContextCustomizerWhenHasNestedComposedAnnotationShouldIncludeTypeExcludeFilters() throws Exception {
+ typeExcludeFiltersFor(WithComposedAnnotation.class).matches(FirstSliceExclude.class);
+ }
+
+ @Test
+ void getContextCustomizerWhenDeeplyNestedShouldIncludeAllEnclosingExcludeFilters() throws Exception {
+ typeExcludeFiltersFor(GrandparentEnclosing.ParentEnclosing.DeepInnerClass.class)
+ .matches(FirstSliceExclude.class, SecondSliceExclude.class, ThirdSliceExclude.class);
+ }
+
+ private TypeExcludeFilterAssert typeExcludeFiltersFor(Class> testClass) {
+ ContextCustomizer customizer = this.factory.createContextCustomizer(testClass, Collections.emptyList());
assertThat(customizer).isNotNull();
customizer.customizeContext(this.context, this.mergedContextConfiguration);
this.context.refresh();
- TypeExcludeFilter filter = this.context.getBean(TypeExcludeFilter.class);
- MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory();
- MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(NoAnnotation.class.getName());
- assertThat(filter.match(metadataReader, metadataReaderFactory)).isFalse();
- metadataReader = metadataReaderFactory.getMetadataReader(SimpleExclude.class.getName());
- assertThat(filter.match(metadataReader, metadataReaderFactory)).isTrue();
- metadataReader = metadataReaderFactory.getMetadataReader(TestClassAwareExclude.class.getName());
- assertThat(filter.match(metadataReader, metadataReaderFactory)).isTrue();
+ return new TypeExcludeFilterAssert(this.context.getBean(TypeExcludeFilter.class));
}
static class NoAnnotation {
@@ -152,4 +200,147 @@ class TypeExcludeFiltersContextCustomizerFactoryTests {
}
+ @FirstTestSlice
+ @TypeExcludeFilters(SecondSliceExclude.class)
+ static class WithMultipleExcludeFilterAnnotations {
+
+ class WithEnclosingClassExcludeFilters {
+
+ }
+
+ }
+
+ @TypeExcludeFilters(TestClassAwareExclude.class)
+ static class WithMixedInheritance extends WithFirstTestSliceExclude {
+
+ }
+
+ @FirstTestSlice
+ static class WithFirstTestSliceExclude implements WithSecondTestSliceExclude {
+
+ }
+
+ @SecondTestSlice
+ interface WithSecondTestSliceExclude extends WithThirdTestSliceExclude {
+
+ }
+
+ @TypeExcludeFilters(ThirdSliceExclude.class)
+ interface WithThirdTestSliceExclude {
+
+ }
+
+ @ComposedFirstTestSlice
+ static class WithComposedAnnotation {
+
+ }
+
+ @FirstTestSlice
+ @TypeExcludeFilters(FirstSliceExclude.class)
+ static class WithDuplicateSliceExclude {
+
+ }
+
+ @Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
+ @Retention(RetentionPolicy.RUNTIME)
+ @TypeExcludeFilters(FirstSliceExclude.class)
+ @interface FirstTestSlice {
+
+ }
+
+ @Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
+ @Retention(RetentionPolicy.RUNTIME)
+ @TypeExcludeFilters(SecondSliceExclude.class)
+ @interface SecondTestSlice {
+
+ }
+
+ @Target(ElementType.TYPE)
+ @Retention(RetentionPolicy.RUNTIME)
+ @FirstTestSlice
+ @interface ComposedFirstTestSlice {
+
+ }
+
+ @TypeExcludeFilters(FirstSliceExclude.class)
+ static class EnclosingWithOverride {
+
+ @NestedTestConfiguration(EnclosingConfiguration.OVERRIDE)
+ class InnerWithOverride {
+
+ }
+
+ }
+
+ @TypeExcludeFilters(FirstSliceExclude.class)
+ static class GrandparentEnclosing {
+
+ @TypeExcludeFilters(SecondSliceExclude.class)
+ class ParentEnclosing {
+
+ @TypeExcludeFilters(ThirdSliceExclude.class)
+ class DeepInnerClass {
+
+ }
+
+ }
+
+ }
+
+ static class FirstSliceExclude extends TestClassAwareExclude {
+
+ FirstSliceExclude(Class> testClass) {
+ super(testClass);
+ }
+
+ }
+
+ static class SecondSliceExclude extends TestClassAwareExclude {
+
+ SecondSliceExclude(Class> testClass) {
+ super(testClass);
+ }
+
+ }
+
+ static class ThirdSliceExclude extends TestClassAwareExclude {
+
+ ThirdSliceExclude(Class> testClass) {
+ super(testClass);
+ }
+
+ }
+
+ private static final class TypeExcludeFilterAssert {
+
+ private final TypeExcludeFilter filter;
+
+ private final MetadataReaderFactory metadataReaderFactory = MetadataReaderFactory
+ .create(new DefaultResourceLoader());
+
+ private TypeExcludeFilterAssert(TypeExcludeFilter filter) {
+ this.filter = filter;
+ }
+
+ TypeExcludeFilterAssert matches(Class>... types) throws Exception {
+ for (Class> type : types) {
+ assertThat(matches(type)).as("Filter should match %s", type.getName()).isTrue();
+ }
+ return this;
+ }
+
+ TypeExcludeFilterAssert doesNotMatch(Class>... types) throws Exception {
+ for (Class> type : types) {
+ assertThat(matches(type)).as("Filter should not match %s", type.getName()).isFalse();
+ }
+ return this;
+ }
+
+ private boolean matches(Class> type) throws Exception {
+ MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(type.getName());
+ return this.filter.match(metadataReader, this.metadataReaderFactory);
+ }
+
+ }
+
}