diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/AotMergedContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/cache/AotMergedContextConfiguration.java
new file mode 100644
index 00000000000..6a6d4e68137
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/cache/AotMergedContextConfiguration.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2002-2022 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.cache;
+
+import java.util.Collections;
+
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.core.style.ToStringCreator;
+import org.springframework.lang.Nullable;
+import org.springframework.test.context.CacheAwareContextLoaderDelegate;
+import org.springframework.test.context.MergedContextConfiguration;
+
+/**
+ * {@link MergedContextConfiguration} implementation based on an AOT-generated
+ * {@link ApplicationContextInitializer} that is used to load an AOT-optimized
+ * {@link org.springframework.context.ApplicationContext ApplicationContext}.
+ *
+ *
An {@code ApplicationContext} should not be loaded using the metadata in
+ * this {@code AotMergedContextConfiguration}. Rather the metadata from the
+ * {@linkplain #getOriginal() original} {@code MergedContextConfiguration} must
+ * be used.
+ *
+ * @author Sam Brannen
+ * @since 6.0
+ */
+class AotMergedContextConfiguration extends MergedContextConfiguration {
+
+ private static final long serialVersionUID = 1963364911008547843L;
+
+ private final Class extends ApplicationContextInitializer>> contextInitializerClass;
+
+ private final MergedContextConfiguration original;
+
+
+ AotMergedContextConfiguration(Class> testClass,
+ Class extends ApplicationContextInitializer>> contextInitializerClass,
+ MergedContextConfiguration original,
+ CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) {
+
+ super(testClass, null, null, Collections.singleton(contextInitializerClass), null,
+ original.getContextLoader(), cacheAwareContextLoaderDelegate, original.getParent());
+ this.contextInitializerClass = contextInitializerClass;
+ this.original = original;
+ }
+
+
+ /**
+ * Get the original {@link MergedContextConfiguration} that this
+ * {@code AotMergedContextConfiguration} was created for.
+ */
+ MergedContextConfiguration getOriginal() {
+ return this.original;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || other.getClass() != getClass()) {
+ return false;
+ }
+ AotMergedContextConfiguration that = (AotMergedContextConfiguration) other;
+ if (!this.contextInitializerClass.equals(that.contextInitializerClass)) {
+ return false;
+ }
+ if (!nullSafeClassName(getContextLoader()).equals(nullSafeClassName(that.getContextLoader()))) {
+ return false;
+ }
+ if (getParent() == null) {
+ if (that.getParent() != null) {
+ return false;
+ }
+ }
+ else if (!getParent().equals(that.getParent())) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = this.contextInitializerClass.hashCode();
+ result = 31 * result + nullSafeClassName(getContextLoader()).hashCode();
+ result = 31 * result + (getParent() != null ? getParent().hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringCreator(this)
+ .append("testClass", getTestClass().getName())
+ .append("contextInitializerClass", this.contextInitializerClass.getName())
+ .append("original", this.original)
+ .toString();
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java
index a2919db9341..4f9f6784f29 100644
--- a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java
+++ b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java
@@ -86,18 +86,19 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext
@Override
public boolean isContextLoaded(MergedContextConfiguration mergedContextConfiguration) {
synchronized (this.contextCache) {
- return this.contextCache.contains(mergedContextConfiguration);
+ return this.contextCache.contains(replaceIfNecessary(mergedContextConfiguration));
}
}
@Override
public ApplicationContext loadContext(MergedContextConfiguration mergedContextConfiguration) {
+ mergedContextConfiguration = replaceIfNecessary(mergedContextConfiguration);
synchronized (this.contextCache) {
ApplicationContext context = this.contextCache.get(mergedContextConfiguration);
if (context == null) {
try {
- if (runningInAotMode(mergedContextConfiguration.getTestClass())) {
- context = loadContextInAotMode(mergedContextConfiguration);
+ if (mergedContextConfiguration instanceof AotMergedContextConfiguration aotMergedConfig) {
+ context = loadContextInAotMode(aotMergedConfig);
}
else {
context = loadContextInternal(mergedContextConfiguration);
@@ -129,7 +130,7 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext
@Override
public void closeContext(MergedContextConfiguration mergedContextConfiguration, @Nullable HierarchyMode hierarchyMode) {
synchronized (this.contextCache) {
- this.contextCache.remove(mergedContextConfiguration, hierarchyMode);
+ this.contextCache.remove(replaceIfNecessary(mergedContextConfiguration), hierarchyMode);
}
}
@@ -163,23 +164,23 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext
}
}
- protected ApplicationContext loadContextInAotMode(MergedContextConfiguration mergedConfig) throws Exception {
- Class> testClass = mergedConfig.getTestClass();
+ protected ApplicationContext loadContextInAotMode(AotMergedContextConfiguration aotMergedConfig) throws Exception {
+ Class> testClass = aotMergedConfig.getTestClass();
ApplicationContextInitializer contextInitializer =
this.aotTestContextInitializers.getContextInitializer(testClass);
Assert.state(contextInitializer != null,
() -> "Failed to load AOT ApplicationContextInitializer for test class [%s]"
.formatted(testClass.getName()));
- ContextLoader contextLoader = getContextLoader(mergedConfig);
- logger.info(LogMessage.format("Loading ApplicationContext in AOT mode for %s", mergedConfig));
+ ContextLoader contextLoader = getContextLoader(aotMergedConfig);
+ logger.info(LogMessage.format("Loading ApplicationContext in AOT mode for %s", aotMergedConfig.getOriginal()));
if (!((contextLoader instanceof AotContextLoader aotContextLoader) &&
- (aotContextLoader.loadContextForAotRuntime(mergedConfig, contextInitializer)
+ (aotContextLoader.loadContextForAotRuntime(aotMergedConfig.getOriginal(), contextInitializer)
instanceof GenericApplicationContext gac))) {
throw new TestContextAotException("""
Cannot load ApplicationContext for AOT runtime for %s. The configured \
ContextLoader [%s] must be an AotContextLoader and must create a \
GenericApplicationContext."""
- .formatted(mergedConfig, contextLoader.getClass().getName()));
+ .formatted(aotMergedConfig.getOriginal(), contextLoader.getClass().getName()));
}
gac.registerShutdownHook();
return gac;
@@ -195,10 +196,24 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext
}
/**
- * Determine if we are running in AOT mode for the supplied test class.
+ * If the test class associated with the supplied {@link MergedContextConfiguration}
+ * has an AOT-optimized {@link ApplicationContext}, this method will create an
+ * {@link AotMergedContextConfiguration} to replace the provided {@code MergedContextConfiguration}.
+ * Otherwise, this method simply returns the supplied {@code MergedContextConfiguration}
+ * unmodified.
+ *
This allows for transparent {@link org.springframework.test.context.cache.ContextCache ContextCache}
+ * support for AOT-optimized application contexts.
*/
- private boolean runningInAotMode(Class> testClass) {
- return this.aotTestContextInitializers.isSupportedTestClass(testClass);
+ @SuppressWarnings("unchecked")
+ private MergedContextConfiguration replaceIfNecessary(MergedContextConfiguration mergedConfig) {
+ Class> testClass = mergedConfig.getTestClass();
+ if (this.aotTestContextInitializers.isSupportedTestClass(testClass)) {
+ Class extends ApplicationContextInitializer>> contextInitializerClass =
+ (Class extends ApplicationContextInitializer>>)
+ this.aotTestContextInitializers.getContextInitializer(testClass).getClass();
+ return new AotMergedContextConfiguration(testClass, contextInitializerClass, mergedConfig, this);
+ }
+ return mergedConfig;
}
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/AbstractAotTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/AbstractAotTests.java
index 5c83a336c46..20c67d1dfce 100644
--- a/spring-test/src/test/java/org/springframework/test/context/aot/AbstractAotTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/aot/AbstractAotTests.java
@@ -33,31 +33,38 @@ abstract class AbstractAotTests {
// Global
"org/springframework/test/context/aot/AotTestContextInitializers__Generated.java",
"org/springframework/test/context/aot/AotTestAttributes__Generated.java",
- // BasicSpringJupiterSharedConfigTests
+ // BasicSpringJupiterImportedConfigTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext001_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext001_BeanDefinitions.java",
- "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext001_ApplicationContextInitializer.java",
- "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext001_BeanFactoryRegistrations.java",
+ "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterImportedConfigTests__TestContext001_ApplicationContextInitializer.java",
+ "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterImportedConfigTests__TestContext001_BeanDefinitions.java",
+ "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterImportedConfigTests__TestContext001_BeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext001_BeanDefinitions.java",
- // BasicSpringJupiterTests -- not generated b/c already generated for BasicSpringJupiterSharedConfigTests.
- // BasicSpringJupiterTests.NestedTests
+ // BasicSpringJupiterSharedConfigTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext002_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext002_BeanDefinitions.java",
- "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext002_ApplicationContextInitializer.java",
- "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext002_BeanFactoryRegistrations.java",
+ "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext002_ApplicationContextInitializer.java",
+ "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext002_BeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext002_BeanDefinitions.java",
- // BasicSpringTestNGTests
+ // BasicSpringJupiterTests -- not generated b/c already generated for BasicSpringJupiterSharedConfigTests.
+ // BasicSpringJupiterTests.NestedTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext003_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext003_BeanDefinitions.java",
- "org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests__TestContext003_ApplicationContextInitializer.java",
- "org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests__TestContext003_BeanFactoryRegistrations.java",
+ "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext003_ApplicationContextInitializer.java",
+ "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext003_BeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext003_BeanDefinitions.java",
- // BasicSpringVintageTests
+ // BasicSpringTestNGTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext004_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext004_BeanDefinitions.java",
- "org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext004_ApplicationContextInitializer.java",
- "org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext004_BeanFactoryRegistrations.java",
- "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext004_BeanDefinitions.java"
+ "org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests__TestContext004_ApplicationContextInitializer.java",
+ "org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests__TestContext004_BeanFactoryRegistrations.java",
+ "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext004_BeanDefinitions.java",
+ // BasicSpringVintageTests
+ "org/springframework/context/event/DefaultEventListenerFactory__TestContext005_BeanDefinitions.java",
+ "org/springframework/context/event/EventListenerMethodProcessor__TestContext005_BeanDefinitions.java",
+ "org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext005_ApplicationContextInitializer.java",
+ "org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext005_BeanFactoryRegistrations.java",
+ "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext005_BeanDefinitions.java"
};
Stream> scan() {
diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java
index b5c45ce7ffc..89701d6969d 100644
--- a/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java
@@ -39,6 +39,7 @@ import org.springframework.aot.generate.InMemoryGeneratedFiles;
import org.springframework.aot.test.generate.CompilerFiles;
import org.springframework.core.test.tools.CompileWithForkedClassLoader;
import org.springframework.core.test.tools.TestCompiler;
+import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterImportedConfigTests;
import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterSharedConfigTests;
import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterTests;
import org.springframework.test.context.aot.samples.basic.BasicSpringTestNGTests;
@@ -96,9 +97,13 @@ class AotIntegrationTests extends AbstractAotTests {
// .printFiles(System.out)
.compile(compiled ->
// AOT RUN-TIME: EXECUTION
- runTestsInAotMode(5, List.of(
+ runTestsInAotMode(6, List.of(
BasicSpringJupiterSharedConfigTests.class,
BasicSpringJupiterTests.class, // NestedTests get executed automatically
+ // Run @Import tests AFTER the tests with otherwise identical config
+ // in order to ensure that the other test classes are not accidentally
+ // using the config for the @Import tests.
+ BasicSpringJupiterImportedConfigTests.class,
BasicSpringTestNGTests.class,
BasicSpringVintageTests.class)));
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/TestAotProcessorTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/TestAotProcessorTests.java
index 376bbc62062..fdce41c3f34 100644
--- a/spring-test/src/test/java/org/springframework/test/context/aot/TestAotProcessorTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/aot/TestAotProcessorTests.java
@@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.CleanupMode;
import org.junit.jupiter.api.io.TempDir;
+import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterImportedConfigTests;
import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterSharedConfigTests;
import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterTests;
import org.springframework.test.context.aot.samples.basic.BasicSpringTestNGTests;
@@ -49,6 +50,7 @@ class TestAotProcessorTests extends AbstractAotTests {
// Limit the scope of this test by creating a new classpath root on the fly.
Path classpathRoot = Files.createDirectories(tempDir.resolve("build/classes"));
Stream.of(
+ BasicSpringJupiterImportedConfigTests.class,
BasicSpringJupiterSharedConfigTests.class,
BasicSpringJupiterTests.class,
BasicSpringJupiterTests.NestedTests.class,
diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/TestClassScannerTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/TestClassScannerTests.java
index 5a940fe5ca0..59a188056f8 100644
--- a/spring-test/src/test/java/org/springframework/test/context/aot/TestClassScannerTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/aot/TestClassScannerTests.java
@@ -18,6 +18,7 @@ package org.springframework.test.context.aot;
import org.junit.jupiter.api.Test;
+import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterImportedConfigTests;
import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterSharedConfigTests;
import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterTests;
import org.springframework.test.context.aot.samples.basic.BasicSpringTestNGTests;
@@ -37,6 +38,7 @@ class TestClassScannerTests extends AbstractAotTests {
void scanBasicTestClasses() {
assertThat(scan("org.springframework.test.context.aot.samples.basic"))
.containsExactlyInAnyOrder(
+ BasicSpringJupiterImportedConfigTests.class,
BasicSpringJupiterSharedConfigTests.class,
BasicSpringJupiterTests.class,
BasicSpringJupiterTests.NestedTests.class,
@@ -48,8 +50,9 @@ class TestClassScannerTests extends AbstractAotTests {
@Test
void scanTestSuitesForJupiter() {
assertThat(scan("org.springframework.test.context.aot.samples.suites.jupiter"))
- .containsExactlyInAnyOrder(BasicSpringJupiterSharedConfigTests.class,
- BasicSpringJupiterTests.class, BasicSpringJupiterTests.NestedTests.class);
+ .containsExactlyInAnyOrder(BasicSpringJupiterImportedConfigTests.class,
+ BasicSpringJupiterSharedConfigTests.class, BasicSpringJupiterTests.class,
+ BasicSpringJupiterTests.NestedTests.class);
}
@Test
@@ -68,6 +71,7 @@ class TestClassScannerTests extends AbstractAotTests {
void scanTestSuitesForAllTestEngines() {
assertThat(scan("org.springframework.test.context.aot.samples.suites.all"))
.containsExactlyInAnyOrder(
+ BasicSpringJupiterImportedConfigTests.class,
BasicSpringJupiterSharedConfigTests.class,
BasicSpringJupiterTests.class,
BasicSpringJupiterTests.NestedTests.class,
@@ -80,6 +84,7 @@ class TestClassScannerTests extends AbstractAotTests {
void scanTestSuitesWithNestedSuites() {
assertThat(scan("org.springframework.test.context.aot.samples.suites.nested"))
.containsExactlyInAnyOrder(
+ BasicSpringJupiterImportedConfigTests.class,
BasicSpringJupiterSharedConfigTests.class,
BasicSpringJupiterTests.class,
BasicSpringJupiterTests.NestedTests.class,
diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java
index 24e00b9c835..2ce0b3de509 100644
--- a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java
@@ -193,7 +193,8 @@ class TestContextAotGeneratorTests extends AbstractAotTests {
// ContextCustomizerFactory
Stream.of(
"org.springframework.test.context.support.DynamicPropertiesContextCustomizerFactory",
- "org.springframework.test.context.web.socket.MockServerContainerContextCustomizerFactory"
+ "org.springframework.test.context.web.socket.MockServerContainerContextCustomizerFactory",
+ "org.springframework.test.context.aot.samples.basic.ImportsContextCustomizerFactory"
).forEach(type -> assertReflectionRegistered(runtimeHints, type, INVOKE_DECLARED_CONSTRUCTORS));
Stream.of(
diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringJupiterImportedConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringJupiterImportedConfigTests.java
new file mode 100644
index 00000000000..cc917a7610b
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringJupiterImportedConfigTests.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2002-2022 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.aot.samples.basic;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterImportedConfigTests.ImportedConfig;
+import org.springframework.test.context.aot.samples.common.MessageService;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Uses configuration identical to {@link BasicSpringJupiterTests} and
+ * {@link BasicSpringJupiterImportedConfigTests} EXCEPT that this class is
+ * annotated with {@link Import @Import} to register an additional bean.
+ *
+ * @author Sam Brannen
+ * @since 6.0
+ */
+@SpringJUnitConfig(BasicTestConfiguration.class)
+@Import(ImportedConfig.class)
+@TestPropertySource(properties = "test.engine = jupiter")
+public class BasicSpringJupiterImportedConfigTests {
+
+ @Autowired
+ ApplicationContext context;
+
+ @Autowired
+ MessageService messageService;
+
+ @Autowired
+ String enigma;
+
+ @Value("${test.engine}")
+ String testEngine;
+
+ @org.junit.jupiter.api.Test
+ void test() {
+ assertThat(messageService.generateMessage()).isEqualTo("Hello, AOT!");
+ assertThat(enigma).isEqualTo("imported!");
+ assertThat(testEngine).isEqualTo("jupiter");
+ assertThat(context.getEnvironment().getProperty("test.engine"))
+ .as("@TestPropertySource").isEqualTo("jupiter");
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ static class ImportedConfig {
+
+ @Bean
+ String enigma() {
+ return "imported!";
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/ImportsContextCustomizerFactory.java b/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/ImportsContextCustomizerFactory.java
new file mode 100644
index 00000000000..059a42cee02
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/ImportsContextCustomizerFactory.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2002-2022 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.aot.samples.basic;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.springframework.aot.AotDetector;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.AnnotatedBeanDefinitionReader;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.test.context.ContextConfigurationAttributes;
+import org.springframework.test.context.ContextCustomizer;
+import org.springframework.test.context.ContextCustomizerFactory;
+import org.springframework.test.context.MergedContextConfiguration;
+
+/**
+ * Emulates {@code ImportsContextCustomizerFactory} from Spring Boot's testing support.
+ *
+ * @author Sam Brannen
+ * @since 6.0
+ */
+class ImportsContextCustomizerFactory implements ContextCustomizerFactory {
+
+ @Override
+ public ContextCustomizer createContextCustomizer(Class> testClass,
+ List configAttributes) {
+
+ if (AotDetector.useGeneratedArtifacts()) {
+ return null;
+ }
+ if (testClass.getName().startsWith("org.springframework.test.context.aot.samples") &&
+ testClass.isAnnotationPresent(Import.class)) {
+ return new ImportsContextCustomizer(testClass);
+ }
+ return null;
+ }
+
+ /**
+ * Emulates {@code ImportsContextCustomizer} from Spring Boot's testing support.
+ */
+ private static class ImportsContextCustomizer implements ContextCustomizer {
+
+ private final Class> testClass;
+
+ ImportsContextCustomizer(Class> testClass) {
+ this.testClass = testClass;
+ }
+
+ @Override
+ public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
+ AnnotatedBeanDefinitionReader annotatedBeanDefinitionReader =
+ new AnnotatedBeanDefinitionReader((GenericApplicationContext) context);
+ Arrays.stream(this.testClass.getAnnotation(Import.class).value())
+ .forEach(annotatedBeanDefinitionReader::register);
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/AotMergedContextConfigurationTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/AotMergedContextConfigurationTests.java
new file mode 100644
index 00000000000..2f91668473e
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/cache/AotMergedContextConfigurationTests.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2002-2022 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.cache;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.test.context.CacheAwareContextLoaderDelegate;
+import org.springframework.test.context.ContextLoader;
+import org.springframework.test.context.MergedContextConfiguration;
+import org.springframework.test.context.support.DelegatingSmartContextLoader;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for {@link AotMergedContextConfiguration}.
+ *
+ * @author Sam Brannen
+ * @since 6.0
+ */
+class AotMergedContextConfigurationTests {
+
+ private final CacheAwareContextLoaderDelegate delegate =
+ new DefaultCacheAwareContextLoaderDelegate(mock(ContextCache.class));
+
+ private final ContextLoader contextLoader = new DelegatingSmartContextLoader();
+
+ private final MergedContextConfiguration mergedConfig = new MergedContextConfiguration(getClass(), null, null,
+ Set.of(DemoApplicationContextInitializer.class), null, contextLoader);
+
+ private final AotMergedContextConfiguration aotMergedConfig1 = new AotMergedContextConfiguration(getClass(),
+ DemoApplicationContextInitializer.class, mergedConfig, delegate);
+
+ private final AotMergedContextConfiguration aotMergedConfig2 = new AotMergedContextConfiguration(getClass(),
+ DemoApplicationContextInitializer.class, mergedConfig, delegate);
+
+
+ @Test
+ void testEquals() {
+ assertThat(aotMergedConfig1).isEqualTo(aotMergedConfig1);
+ assertThat(aotMergedConfig1).isEqualTo(aotMergedConfig2);
+
+ assertThat(mergedConfig).isNotEqualTo(aotMergedConfig1);
+ assertThat(aotMergedConfig1).isNotEqualTo(mergedConfig);
+ }
+
+ @Test
+ void testHashCode() {
+ assertThat(aotMergedConfig1).hasSameHashCodeAs(aotMergedConfig2);
+
+ assertThat(aotMergedConfig1).doesNotHaveSameHashCodeAs(mergedConfig);
+ }
+
+
+ static class DemoApplicationContextInitializer implements ApplicationContextInitializer {
+ @Override
+ public void initialize(GenericApplicationContext applicationContext) {
+ }
+ }
+
+}
diff --git a/spring-test/src/test/resources/META-INF/spring.factories b/spring-test/src/test/resources/META-INF/spring.factories
index 8b6ce9b4187..b26a9de824b 100644
--- a/spring-test/src/test/resources/META-INF/spring.factories
+++ b/spring-test/src/test/resources/META-INF/spring.factories
@@ -1,2 +1,6 @@
-# Test configuration file containing a non-existent default TestExecutionListener.
+# Test configuration file containing a non-existent default TestExecutionListener and a demo ContextCustomizerFactory.
+
org.springframework.test.context.TestExecutionListener = org.example.FooListener
+
+org.springframework.test.context.ContextCustomizerFactory =\
+ org.springframework.test.context.aot.samples.basic.ImportsContextCustomizerFactory