From e68440d4192a7e0b3ac155b6772bf66f751e6ffd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 9 Apr 2026 15:55:30 +0100 Subject: [PATCH] Consider enclosing classes when finding imports on test classes Fixes gh-49860 --- .../context/ImportsContextCustomizer.java | 52 ++++++++---- .../context/SpringBootTestImportTests.java | 84 +++++++++++++++++++ 2 files changed, 118 insertions(+), 18 deletions(-) create mode 100644 spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestImportTests.java diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java index ccba2ca316a..6865660c8de 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java @@ -18,9 +18,11 @@ package org.springframework.boot.test.context; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -51,6 +53,7 @@ import org.springframework.core.style.ToStringCreator; import org.springframework.core.type.AnnotationMetadata; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.TestContextAnnotationUtils; import org.springframework.util.ReflectionUtils; /** @@ -64,17 +67,30 @@ import org.springframework.util.ReflectionUtils; */ class ImportsContextCustomizer implements ContextCustomizer { - private static final String TEST_CLASS_NAME_ATTRIBUTE = "testClassName"; + private static final String TEST_CLASS_NAMES_ATTRIBUTE = "testClassNames"; - private final String testClassName; + private final String[] testClassNames; private final ContextCustomizerKey key; ImportsContextCustomizer(Class testClass) { - this.testClassName = testClass.getName(); + this.testClassNames = collectClassNames(testClass); this.key = new ContextCustomizerKey(testClass); } + private static String[] collectClassNames(Class source) { + List classNames = new ArrayList<>(); + collectClassNames(source, classNames); + return classNames.toArray(new String[0]); + } + + private static void collectClassNames(Class source, List classNames) { + classNames.add(source.getName()); + if (TestContextAnnotationUtils.searchEnclosingClass(source)) { + collectClassNames(source.getEnclosingClass(), classNames); + } + } + @Override public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedContextConfiguration) { @@ -88,13 +104,13 @@ class ImportsContextCustomizer implements ContextCustomizer { BeanDefinition definition = registerBean(registry, reader, ImportsCleanupPostProcessor.BEAN_NAME, ImportsCleanupPostProcessor.class); definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - definition.getConstructorArgumentValues().addIndexedArgumentValue(0, this.testClassName); + definition.getConstructorArgumentValues().addIndexedArgumentValue(0, this.testClassNames); } private void registerImportsConfiguration(BeanDefinitionRegistry registry, AnnotatedBeanDefinitionReader reader) { BeanDefinition definition = registerBean(registry, reader, ImportsConfiguration.BEAN_NAME, ImportsConfiguration.class); - definition.setAttribute(TEST_CLASS_NAME_ATTRIBUTE, this.testClassName); + definition.setAttribute(TEST_CLASS_NAMES_ATTRIBUTE, this.testClassNames); } private BeanDefinitionRegistry getBeanDefinitionRegistry(ApplicationContext context) { @@ -166,8 +182,8 @@ class ImportsContextCustomizer implements ContextCustomizer { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { BeanDefinition definition = this.beanFactory.getBeanDefinition(ImportsConfiguration.BEAN_NAME); - Object testClassName = definition.getAttribute(TEST_CLASS_NAME_ATTRIBUTE); - return (testClassName != null) ? new String[] { (String) testClassName } : NO_IMPORTS; + Object testClassNames = definition.getAttribute(TEST_CLASS_NAMES_ATTRIBUTE); + return (testClassNames != null) ? (String[]) testClassNames : NO_IMPORTS; } } @@ -181,10 +197,10 @@ class ImportsContextCustomizer implements ContextCustomizer { static final String BEAN_NAME = ImportsCleanupPostProcessor.class.getName(); - private final String testClassName; + private final String[] testClassNames; - ImportsCleanupPostProcessor(String testClassName) { - this.testClassName = testClassName; + ImportsCleanupPostProcessor(String[] testClassNames) { + this.testClassNames = testClassNames; } @Override @@ -193,15 +209,15 @@ class ImportsContextCustomizer implements ContextCustomizer { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + for (String testClassName : this.testClassNames) { + removeBean(testClassName, registry); + } + removeBean(ImportsConfiguration.BEAN_NAME, registry); + } + + private void removeBean(String beanName, BeanDefinitionRegistry registry) { try { - String[] names = registry.getBeanDefinitionNames(); - for (String name : names) { - BeanDefinition definition = registry.getBeanDefinition(name); - if (this.testClassName.equals(definition.getBeanClassName())) { - registry.removeBeanDefinition(name); - } - } - registry.removeBeanDefinition(ImportsConfiguration.BEAN_NAME); + registry.removeBeanDefinition(beanName); } catch (NoSuchBeanDefinitionException ex) { // Ignore diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestImportTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestImportTests.java new file mode 100644 index 00000000000..549dc58e237 --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestImportTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-present 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.boot.test.context; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTestImportTests.ImportedByContainingTests; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.env.Environment; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SpringBootTest @SpringBootTest} with {@link Import @Import}. + * + * @author Andy Wilkinson + */ +@TestPropertySource(properties = "a=alpha") +@Import(ImportedByContainingTests.class) +class SpringBootTestImportTests { + + @Nested + @SpringBootTest(classes = Config.class) + @Import(ImportedByNestedTests.class) + class NestedTests { + + @Autowired(required = false) + private ImportedByContainingTests importedByContainingTests; + + @Autowired(required = false) + private ImportedByNestedTests importedByNestedTests; + + @Autowired + private Environment environment; + + @Test + void sameClassImportIsHonored() { + assertThat(this.importedByNestedTests).isNotNull(); + } + + @Test + void containingClassImportIsHonored() { + assertThat(this.importedByContainingTests).isNotNull(); + } + + @Test + void containingClassTestPropertySourceIsHonored() { + assertThat(this.environment.getProperty("a")).isEqualTo("alpha"); + } + + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + } + + static class ImportedByNestedTests { + + } + + static class ImportedByContainingTests { + + } + +}