diff --git a/framework-docs/modules/ROOT/nav.adoc b/framework-docs/modules/ROOT/nav.adoc
index 24123d403f6..37cb971836c 100644
--- a/framework-docs/modules/ROOT/nav.adoc
+++ b/framework-docs/modules/ROOT/nav.adoc
@@ -122,6 +122,7 @@
**** xref:testing/testcontext-framework/ctx-management/groovy.adoc[]
**** xref:testing/testcontext-framework/ctx-management/javaconfig.adoc[]
**** xref:testing/testcontext-framework/ctx-management/mixed-config.adoc[]
+**** xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[]
**** xref:testing/testcontext-framework/ctx-management/initializers.adoc[]
**** xref:testing/testcontext-framework/ctx-management/inheritance.adoc[]
**** xref:testing/testcontext-framework/ctx-management/env-profiles.adoc[]
@@ -166,6 +167,7 @@
***** xref:testing/annotations/integration-spring/annotation-contextconfiguration.adoc[]
***** xref:testing/annotations/integration-spring/annotation-webappconfiguration.adoc[]
***** xref:testing/annotations/integration-spring/annotation-contexthierarchy.adoc[]
+***** xref:testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc[]
***** xref:testing/annotations/integration-spring/annotation-activeprofiles.adoc[]
***** xref:testing/annotations/integration-spring/annotation-testpropertysource.adoc[]
***** xref:testing/annotations/integration-spring/annotation-dynamicpropertysource.adoc[]
diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc
index b035e01a1d0..e9cd1e17679 100644
--- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc
+++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc
@@ -223,6 +223,7 @@ following annotations.
* xref:testing/annotations/integration-spring/annotation-contextconfiguration.adoc[`@ContextConfiguration`]
* xref:testing/annotations/integration-spring/annotation-webappconfiguration.adoc[`@WebAppConfiguration`]
* xref:testing/annotations/integration-spring/annotation-contexthierarchy.adoc[`@ContextHierarchy`]
+* xref:testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc[`@ContextCustomizerFactories`]
* xref:testing/annotations/integration-spring/annotation-activeprofiles.adoc[`@ActiveProfiles`]
* xref:testing/annotations/integration-spring/annotation-testpropertysource.adoc[`@TestPropertySource`]
* xref:testing/annotations/integration-spring/annotation-dynamicpropertysource.adoc[`@DynamicPropertySource`]
diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-meta.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-meta.adoc
index 1e992c75e86..20bb7976d6a 100644
--- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-meta.adoc
+++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-meta.adoc
@@ -11,6 +11,7 @@ xref:testing/testcontext-framework.adoc[TestContext framework].
* `@BootstrapWith`
* `@ContextConfiguration`
* `@ContextHierarchy`
+* `@ContextCustomizerFactories`
* `@ActiveProfiles`
* `@TestPropertySource`
* `@DirtiesContext`
diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring.adoc
index 3db79d44bc4..45faf7965d4 100644
--- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring.adoc
+++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring.adoc
@@ -12,6 +12,7 @@ Spring's testing annotations include the following:
* xref:testing/annotations/integration-spring/annotation-contextconfiguration.adoc[`@ContextConfiguration`]
* xref:testing/annotations/integration-spring/annotation-webappconfiguration.adoc[`@WebAppConfiguration`]
* xref:testing/annotations/integration-spring/annotation-contexthierarchy.adoc[`@ContextHierarchy`]
+* xref:testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc[`@ContextCustomizerFactories`]
* xref:testing/annotations/integration-spring/annotation-activeprofiles.adoc[`@ActiveProfiles`]
* xref:testing/annotations/integration-spring/annotation-testpropertysource.adoc[`@TestPropertySource`]
* xref:testing/annotations/integration-spring/annotation-dynamicpropertysource.adoc[`@DynamicPropertySource`]
diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc
new file mode 100644
index 00000000000..497ef55afe1
--- /dev/null
+++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc
@@ -0,0 +1,45 @@
+[[spring-testing-annotation-contextcustomizerfactories]]
+= `@ContextCustomizerFactories`
+
+`@ContextCustomizerFactories` is used to register `ContextCustomizerFactory`
+implementations for a particular test class, its subclasses, and its nested classes. If
+you wish to register a factory globally, you should register it via the automatic
+discovery mechanism described in
+xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[`ContextCustomizerFactory` Configuration].
+
+The following example shows how to register two `ContextCustomizerFactory` implementations:
+
+[tabs]
+======
+Java::
++
+[source,java,indent=0,subs="verbatim,quotes",role="primary"]
+----
+ @ContextConfiguration
+ @ContextCustomizerFactories({CustomContextCustomizerFactory.class, AnotherContextCustomizerFactory.class}) // <1>
+ class CustomContextCustomizerFactoryTests {
+ // class body...
+ }
+----
+<1> Register two `ContextCustomizerFactory` implementations.
+
+Kotlin::
++
+[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
+----
+ @ContextConfiguration
+ @ContextCustomizerFactories([CustomContextCustomizerFactory::class, AnotherContextCustomizerFactory::class]) // <1>
+ class CustomContextCustomizerFactoryTests {
+ // class body...
+ }
+----
+<1> Register two `ContextCustomizerFactory` implementations.
+======
+
+
+By default, `@ContextCustomizerFactories` provides support for inheriting factories from
+superclasses or enclosing classes. See
+xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-nested-test-configuration[`@Nested` test class configuration] and the
+{api-spring-framework}/test/context/ContextCustomizerFactories.html[`@ContextCustomizerFactories`
+javadoc] for an example and further details.
+
diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management.adoc
index 61fe27babeb..9aa446ed035 100644
--- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management.adoc
+++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management.adoc
@@ -112,6 +112,7 @@ advanced use cases.
* xref:testing/testcontext-framework/ctx-management/groovy.adoc[Context Configuration with Groovy Scripts]
* xref:testing/testcontext-framework/ctx-management/javaconfig.adoc[Context Configuration with Component Classes]
* xref:testing/testcontext-framework/ctx-management/mixed-config.adoc[Mixing XML, Groovy Scripts, and Component Classes]
+* xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[Context Configuration with Context Customizers]
* xref:testing/testcontext-framework/ctx-management/initializers.adoc[Context Configuration with Context Initializers]
* xref:testing/testcontext-framework/ctx-management/inheritance.adoc[Context Configuration Inheritance]
* xref:testing/testcontext-framework/ctx-management/env-profiles.adoc[Context Configuration with Environment Profiles]
diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-customizers.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-customizers.adoc
new file mode 100644
index 00000000000..1979482042c
--- /dev/null
+++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-customizers.adoc
@@ -0,0 +1,69 @@
+[[testcontext-context-customizers]]
+= Configuration Configuration with Context Customizers
+
+A `ContextCustomizer` is responsible for customizing the supplied
+`ConfigurableApplicationContext` after bean definitions have been loaded into the context
+but before the context has been refreshed.
+
+A `ContextCustomizerFactory` is responsible for creating a `ContextCustomizer`, based on
+some custom logic which determines if the `ContextCustomizer` is necessary for a given
+test class -- for example, based on the presence of a certain annotation. Factories are
+invoked after `ContextLoaders` have processed context configuration attributes for a test
+class but before the `MergedContextConfiguration` is created.
+
+For example, Spring Framework provides the following `ContextCustomizerFactory`
+implementation which is registered by default:
+
+`MockServerContainerContextCustomizerFactory`:: Creates a
+ `MockServerContainerContextCustomizer` if WebSocket support is present in the classpath
+ and the test class or one of its enclosing classes is annotated or meta-annotated with
+ `@WebAppConfiguration`. `MockServerContainerContextCustomizer` instantiates a new
+ `MockServerContainer` and stores it in the `ServletContext` under the attribute named
+ `jakarta.websocket.server.ServerContainer`.
+
+
+[[testcontext-context-customizers-registration]]
+== Registering `ContextCustomizerFactory` Implementations
+
+You can register `ContextCustomizerFactory` implementations explicitly for a test class, its
+subclasses, and its nested classes by using the `@ContextCustomizerFactories` annotation. See
+xref:testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc[annotation support]
+and the javadoc for
+{api-spring-framework}/test/context/ContextCustomizerFactories.html[`@ContextCustomizerFactories`]
+for details and examples.
+
+
+[[testcontext-context-customizers-automatic-discovery]]
+== Automatic Discovery of Default `ContextCustomizerFactory` Implementations
+
+Registering `ContextCustomizerFactory` implementations by using `@ContextCustomizerFactories` is
+suitable for custom factories that are used in limited testing scenarios. However, it can
+become cumbersome if a custom factory needs to be used across an entire test suite. This
+issue is addressed through support for automatic discovery of default
+`ContextCustomizerFactory` implementations through the `SpringFactoriesLoader` mechanism.
+
+Specifically, the modules that make up the testing support in Spring Framework and Spring
+Boot declare all core default `ContextCustomizerFactory` implementations under the
+`org.springframework.test.context.ContextCustomizerFactory` key in their
+`META-INF/spring.factories` properties files. Third-party frameworks and developers can
+contribute their own `ContextCustomizerFactory` implementations to the list of default
+factories in the same manner through their own `META-INF/spring.factories` properties
+files.
+
+
+[[testcontext-context-customizers-merging]]
+== Merging `ContextCustomizerFactory` Implementations
+
+If a custom `ContextCustomizerFactory` is registered via `@ContextCustomizerFactories`, it
+will be _merged_ with the default factories that have been registered using the aforementioned
+xref:testing/testcontext-framework/ctx-management/context-customizers.adoc#testcontext-context-customizers-automatic-discovery[automatic discovery mechanism].
+
+The merging algorithm ensures that duplicates are removed from the list and that locally
+declared factories are appended to the list of default factories when merged.
+
+[TIP]
+====
+To replace the default factories for a test class, its subclasses, and its nested
+classes, you can set the `mergeMode` attribute of `@ContextCustomizerFactories` to
+`MergeMode.REPLACE_DEFAULTS`.
+====
diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextCustomizer.java b/spring-test/src/main/java/org/springframework/test/context/ContextCustomizer.java
index 0f9ac0a5661..aeff0708ff6 100644
--- a/spring-test/src/main/java/org/springframework/test/context/ContextCustomizer.java
+++ b/spring-test/src/main/java/org/springframework/test/context/ContextCustomizer.java
@@ -34,6 +34,7 @@ import org.springframework.context.ConfigurableApplicationContext;
* @author Sam Brannen
* @since 4.3
* @see ContextCustomizerFactory
+ * @see ContextCustomizerFactories @ContextCustomizerFactories
* @see org.springframework.test.context.support.AbstractContextLoader#customizeContext
*/
@FunctionalInterface
diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextCustomizerFactories.java b/spring-test/src/main/java/org/springframework/test/context/ContextCustomizerFactories.java
new file mode 100644
index 00000000000..25ebff330f7
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/ContextCustomizerFactories.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2002-2023 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;
+
+import org.springframework.core.annotation.AliasFor;
+
+/**
+ * {@code @ContextCustomizerFactories} defines class-level metadata for configuring
+ * which {@link ContextCustomizerFactory} implementations should be registered with
+ * the Spring TestContext Framework.
+ *
+ *
{@code @ContextCustomizerFactories} is used to register factories for a
+ * particular test class, its subclasses, and its nested classes. If you wish to
+ * register a factory globally, you should register it via the automatic discovery
+ * mechanism described in {@link ContextCustomizerFactory}.
+ *
+ *
This annotation may be used as a meta-annotation to create custom
+ * composed annotations. In addition, this annotation will be inherited
+ * from an enclosing test class by default. See
+ * {@link NestedTestConfiguration @NestedTestConfiguration} for details.
+ *
+ * @author Sam Brannen
+ * @since 6.1
+ * @see ContextCustomizerFactory
+ * @see ContextCustomizer
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface ContextCustomizerFactories {
+
+ /**
+ * Alias for {@link #factories}.
+ *
This attribute may not be used in conjunction with
+ * {@link #factories}, but it may be used instead of {@link #factories}.
+ */
+ @AliasFor("factories")
+ Class extends ContextCustomizerFactory>[] value() default {};
+
+ /**
+ * The {@link ContextCustomizerFactory} implementations to register.
+ *
This attribute may not be used in conjunction with
+ * {@link #value}, but it may be used instead of {@link #value}.
+ */
+ @AliasFor("value")
+ Class extends ContextCustomizerFactory>[] factories() default {};
+
+ /**
+ * Whether the configured set of {@link #factories} from superclasses and
+ * enclosing classes should be inherited.
+ *
The default value is {@code true}, which means that an annotated class
+ * will inherit the factories defined by an annotated superclass or
+ * enclosing class. Specifically, the factories for an annotated class will be
+ * appended to the list of factories defined by an annotated superclass or
+ * enclosing class. Thus, subclasses and nested classes have the option of
+ * extending the list of factories.
+ *
If {@code inheritListeners} is set to {@code false}, the factories for
+ * the annotated class will shadow and effectively replace any
+ * factories defined by a superclass or enclosing class.
+ */
+ boolean inheritFactories() default true;
+
+ /**
+ * The merge mode to use when {@code @ContextCustomizerFactories} is
+ * declared on a class that does not inherit factories from
+ * a superclass or enclosing class.
+ *
Can be set to {@link MergeMode#REPLACE_DEFAULTS REPLACE_DEFAULTS} to
+ * have locally declared factories replace the default factories.
+ *
The mode is ignored if factories are inherited from a superclass or
+ * enclosing class.
+ *
Defaults to {@link MergeMode#MERGE_WITH_DEFAULTS MERGE_WITH_DEFAULTS}.
+ * @see MergeMode
+ */
+ MergeMode mergeMode() default MergeMode.MERGE_WITH_DEFAULTS;
+
+
+ /**
+ * Enumeration of modes that dictate whether explicitly declared
+ * factories are merged with the default factories when
+ * {@code @ContextCustomizerFactories} is declared on a class that does
+ * not inherit factories from a superclass or enclosing
+ * class.
+ */
+ enum MergeMode {
+
+ /**
+ * Indicates that locally declared factories should be merged with the
+ * default factories.
+ *
The merging algorithm ensures that duplicates are removed from the
+ * list and that locally declared factories are appended to the list of
+ * default factories when merged.
+ */
+ MERGE_WITH_DEFAULTS,
+
+ /**
+ * Indicates that locally declared factories should replace the default
+ * factories.
+ */
+ REPLACE_DEFAULTS
+
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/context/ContextCustomizerFactory.java
index e2ff0a45025..e8b35ff2c51 100644
--- a/spring-test/src/main/java/org/springframework/test/context/ContextCustomizerFactory.java
+++ b/spring-test/src/main/java/org/springframework/test/context/ContextCustomizerFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2023 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,12 +29,18 @@ import org.springframework.lang.Nullable;
*
*
By default, the Spring TestContext Framework will use the
* {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader}
- * mechanism for loading factories configured in all {@code META-INF/spring.factories}
+ * mechanism for loading default factories configured in all {@code META-INF/spring.factories}
* files on the classpath.
*
+ *
As of Spring Framework 6.1, it is also possible to register factories
+ * declaratively via the {@link ContextCustomizerFactories @ContextCustomizerFactories}
+ * annotation.
+ *
* @author Phillip Webb
* @author Sam Brannen
* @since 4.3
+ * @see ContextCustomizer
+ * @see ContextCustomizerFactories @ContextCustomizerFactories
*/
@FunctionalInterface
public interface ContextCustomizerFactory {
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 2343bb5d860..9765a5c7fd5 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
@@ -37,6 +37,7 @@ import org.springframework.test.context.CacheAwareContextLoaderDelegate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
+import org.springframework.test.context.ContextCustomizerFactories;
import org.springframework.test.context.ContextCustomizerFactory;
import org.springframework.test.context.ContextHierarchy;
import org.springframework.test.context.ContextLoader;
@@ -378,7 +379,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
private Set getContextCustomizers(Class> testClass,
List configAttributes) {
- List factories = getContextCustomizerFactories();
+ List factories = getContextCustomizerFactories(testClass);
Set customizers = new LinkedHashSet<>(factories.size());
for (ContextCustomizerFactory factory : factories) {
ContextCustomizer customizer = factory.createContextCustomizer(testClass, configAttributes);
@@ -397,6 +398,69 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
return customizers;
}
+ private List getContextCustomizerFactories(Class> testClass) {
+ AnnotationDescriptor descriptor =
+ TestContextAnnotationUtils.findAnnotationDescriptor(testClass, ContextCustomizerFactories.class);
+ List factories = new ArrayList<>();
+
+ if (descriptor == null) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("@ContextCustomizerFactories is not present for class [%s]"
+ .formatted(testClass.getName()));
+ }
+ factories.addAll(getContextCustomizerFactories());
+ }
+ else {
+ // Traverse the class hierarchy...
+ while (descriptor != null) {
+ Class> declaringClass = descriptor.getDeclaringClass();
+ ContextCustomizerFactories annotation = descriptor.getAnnotation();
+ if (logger.isTraceEnabled()) {
+ logger.trace("Retrieved %s for declaring class [%s]."
+ .formatted(annotation, declaringClass.getName()));
+ }
+
+ boolean inheritFactories = annotation.inheritFactories();
+ AnnotationDescriptor parentDescriptor = descriptor.next();
+ factories.addAll(0, instantiateCustomizerFactories(annotation.factories()));
+
+ // If there are no factories to inherit, we might need to merge the
+ // locally declared factories with the defaults.
+ if ((!inheritFactories || parentDescriptor == null) &&
+ annotation.mergeMode() == ContextCustomizerFactories.MergeMode.MERGE_WITH_DEFAULTS) {
+ if (logger.isTraceEnabled()) {
+ logger.trace(String.format("Merging default factories with factories configured via " +
+ "@ContextCustomizerFactories for class [%s].", descriptor.getRootDeclaringClass().getName()));
+ }
+ factories.addAll(0, getContextCustomizerFactories());
+ }
+
+ descriptor = (inheritFactories ? parentDescriptor : null);
+ }
+ }
+
+ // Remove possible duplicates.
+ List uniqueFactories = new ArrayList<>(factories.size());
+ factories.forEach(factory -> {
+ Class extends ContextCustomizerFactory> factoryClass = factory.getClass();
+ if (uniqueFactories.stream().map(Object::getClass).noneMatch(factoryClass::equals)) {
+ uniqueFactories.add(factory);
+ }
+ });
+ factories = uniqueFactories;
+
+ if (logger.isTraceEnabled()) {
+ logger.trace("Using ContextCustomizerFactory implementations for test class [%s]: %s"
+ .formatted(testClass.getName(), factories));
+ }
+ else if (logger.isDebugEnabled()) {
+ logger.debug("Using ContextCustomizerFactory implementations for test class [%s]: %s"
+ .formatted(testClass.getSimpleName(), classSimpleNames(factories)));
+ }
+
+ return factories;
+ }
+
/**
* Get the {@link ContextCustomizerFactory} instances for this bootstrapper.
* The default implementation delegates to
@@ -407,6 +471,33 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
return TestContextSpringFactoriesUtils.loadFactoryImplementations(ContextCustomizerFactory.class);
}
+ @SuppressWarnings("unchecked")
+ private List instantiateCustomizerFactories(Class extends ContextCustomizerFactory>... classes) {
+ List factories = new ArrayList<>(classes.length);
+ for (Class extends ContextCustomizerFactory> factoryClass : classes) {
+ try {
+ factories.add(BeanUtils.instantiateClass(factoryClass));
+ }
+ catch (BeanInstantiationException ex) {
+ Throwable cause = ex.getCause();
+ if (cause instanceof ClassNotFoundException || cause instanceof NoClassDefFoundError) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("""
+ Skipping candidate %1$s [%2$s] due to a missing dependency. \
+ Specify custom %1$s classes or make the default %1$s classes \
+ and their required dependencies available. Offending class: [%3$s]"""
+ .formatted(ContextCustomizerFactory.class.getSimpleName(), factoryClass.getName(),
+ cause.getMessage()));
+ }
+ }
+ else {
+ throw ex;
+ }
+ }
+ }
+ return factories;
+ }
+
/**
* Resolve the {@link ContextLoader} {@linkplain Class class} to use for the
* supplied list of {@link ContextConfigurationAttributes} and then instantiate
diff --git a/spring-test/src/test/java/org/springframework/test/context/customizers/BarContextCustomizerFactory.java b/spring-test/src/test/java/org/springframework/test/context/customizers/BarContextCustomizerFactory.java
new file mode 100644
index 00000000000..7f564397a0e
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/customizers/BarContextCustomizerFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2002-2023 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.customizers;
+
+import java.util.List;
+
+import org.springframework.test.context.ContextConfigurationAttributes;
+import org.springframework.test.context.ContextCustomizer;
+import org.springframework.test.context.ContextCustomizerFactory;
+
+/**
+ * @author Sam Brannen
+ * @since 6.1
+ */
+class BarContextCustomizerFactory implements ContextCustomizerFactory {
+
+ @Override
+ public ContextCustomizer createContextCustomizer(Class> testClass,
+ List configAttributes) {
+ return (context, mergedConfig) -> context.getBeanFactory().registerSingleton("bar", "baz");
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/customizers/ContextCustomizerDeclarativeRegistrationComposedAnnotationTests.java b/spring-test/src/test/java/org/springframework/test/context/customizers/ContextCustomizerDeclarativeRegistrationComposedAnnotationTests.java
new file mode 100644
index 00000000000..ba692dcef69
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/customizers/ContextCustomizerDeclarativeRegistrationComposedAnnotationTests.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2002-2023 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.customizers;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.test.context.ContextCustomizer;
+import org.springframework.test.context.ContextCustomizerFactories;
+import org.springframework.test.context.ContextCustomizerFactory;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Integration test which verifies support for {@link ContextCustomizerFactory}
+ * and {@link ContextCustomizer} when a custom factory is registered declaratively
+ * via {@link ContextCustomizerFactories @ContextCustomizerFactories}.
+ *
+ * @author Sam Brannen
+ * @since 6.1
+ */
+@SpringJUnitConfig({})
+@CustomizeWithFruit
+@CustomizeWithFoo
+@CustomizeWithBar
+class ContextCustomizerDeclarativeRegistrationComposedAnnotationTests {
+
+ // GlobalFruitContextCustomizerFactory is registered via spring.factories
+ @Autowired(required = false)
+ @Qualifier("global$fruit")
+ String fruit;
+
+ @Autowired(required = false)
+ @Qualifier("foo")
+ String foo;
+
+ @Autowired(required = false)
+ @Qualifier("bar")
+ String bar;
+
+
+ @Test
+ void injectedBean() {
+ // registered globally via spring.factories
+ assertThat(fruit).isEqualTo("apple, banana, cherry");
+
+ // From local @ContextCustomizerFactories
+ assertThat(foo).isEqualTo("bar");
+
+ // @ContextCustomizerFactories is not currently supported as a repeatable annotation.
+ assertThat(bar).isNull();
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/customizers/ContextCustomizerDeclarativeRegistrationTests.java b/spring-test/src/test/java/org/springframework/test/context/customizers/ContextCustomizerDeclarativeRegistrationTests.java
new file mode 100644
index 00000000000..eee46f4d9c6
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/customizers/ContextCustomizerDeclarativeRegistrationTests.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2002-2023 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.customizers;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.test.context.ContextCustomizer;
+import org.springframework.test.context.ContextCustomizerFactories;
+import org.springframework.test.context.ContextCustomizerFactory;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Integration test which verifies support for {@link ContextCustomizerFactory}
+ * and {@link ContextCustomizer} when a custom factory is registered declaratively
+ * via {@link ContextCustomizerFactories @ContextCustomizerFactories}.
+ *
+ * @author Sam Brannen
+ * @since 6.1
+ */
+@SpringJUnitConfig({})
+@CustomizeWithFruit
+@CustomizeWithFoo
+@ContextCustomizerFactories(EnigmaContextCustomizerFactory.class)
+@CustomizeWithBar
+class ContextCustomizerDeclarativeRegistrationTests {
+
+ // GlobalFruitContextCustomizerFactory is registered via spring.factories
+ @Autowired(required = false)
+ @Qualifier("global$fruit")
+ String fruit;
+
+ @Autowired
+ Integer enigma;
+
+ @Autowired(required = false)
+ @Qualifier("foo")
+ String foo;
+
+ @Autowired(required = false)
+ @Qualifier("bar")
+ String bar;
+
+
+ @Test
+ void injectedBean() {
+ // registered globally via spring.factories
+ assertThat(fruit).isEqualTo("apple, banana, cherry");
+
+ // From local @ContextCustomizerFactories
+ assertThat(enigma).isEqualTo(42);
+
+ // @ContextCustomizerFactories is not currently supported as a repeatable annotation,
+ // and a directly present @ContextCustomizerFactories annotation overrides
+ // @ContextCustomizerFactories meta-annotations.
+ assertThat(foo).isNull();
+ assertThat(bar).isNull();
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/customizers/ContextCustomizerTests.java b/spring-test/src/test/java/org/springframework/test/context/customizers/ContextCustomizerTests.java
new file mode 100644
index 00000000000..757e632231a
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/customizers/ContextCustomizerTests.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2002-2023 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.customizers;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.test.context.BootstrapWith;
+import org.springframework.test.context.ContextCustomizer;
+import org.springframework.test.context.ContextCustomizerFactories;
+import org.springframework.test.context.ContextCustomizerFactory;
+import org.springframework.test.context.customizers.ContextCustomizerTests.EnigmaTestContextBootstrapper;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+import org.springframework.test.context.support.AbstractTestContextBootstrapper;
+import org.springframework.test.context.support.DefaultTestContextBootstrapper;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Integration test which verifies support for {@link ContextCustomizerFactory}
+ * and {@link ContextCustomizer} when a custom factory is registered by overriding
+ * {@link AbstractTestContextBootstrapper#getContextCustomizerFactories} and
+ * additional factories are registered declaratively via
+ * {@link ContextCustomizerFactories @ContextCustomizerFactories}.
+ *
+ * @author Sam Brannen
+ * @author Phillip Webb
+ * @since 4.3
+ */
+@SpringJUnitConfig({})
+@CustomizeWithFoo
+@BootstrapWith(EnigmaTestContextBootstrapper.class)
+@CustomizeWithBar
+class ContextCustomizerTests {
+
+ // GlobalFruitContextCustomizerFactory is registered via spring.factories
+ @Autowired(required = false)
+ @Qualifier("global$fruit")
+ String fruit;
+
+ @Autowired
+ Integer enigma;
+
+ @Autowired(required = false)
+ @Qualifier("foo")
+ String foo;
+
+ @Autowired(required = false)
+ @Qualifier("bar")
+ String bar;
+
+
+ @Test
+ void injectedBean() {
+ // Local Bootstrapper overrides spring.factories lookup
+ assertThat(fruit).isNull();
+
+ // From local Bootstrapper
+ assertThat(enigma).isEqualTo(42);
+
+ // From local @ContextCustomizerFactories
+ assertThat(foo).isEqualTo("bar");
+
+ // @ContextCustomizerFactories is not currently supported as a repeatable annotation.
+ assertThat(bar).isNull();
+ }
+
+
+ static class EnigmaTestContextBootstrapper extends DefaultTestContextBootstrapper {
+
+ @Override
+ protected List getContextCustomizerFactories() {
+ return List.of(new EnigmaContextCustomizerFactory());
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/customizers/CustomizeWithBar.java b/spring-test/src/test/java/org/springframework/test/context/customizers/CustomizeWithBar.java
new file mode 100644
index 00000000000..96ed430a538
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/customizers/CustomizeWithBar.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2002-2023 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.customizers;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.test.context.ContextCustomizerFactories;
+
+/**
+ * @author Sam Brannen
+ * @since 6.1
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@ContextCustomizerFactories(BarContextCustomizerFactory.class)
+@interface CustomizeWithBar {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/customizers/CustomizeWithFoo.java b/spring-test/src/test/java/org/springframework/test/context/customizers/CustomizeWithFoo.java
new file mode 100644
index 00000000000..7a7c96f4de8
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/customizers/CustomizeWithFoo.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2002-2023 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.customizers;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.test.context.ContextCustomizerFactories;
+
+/**
+ * @author Sam Brannen
+ * @since 6.1
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@ContextCustomizerFactories(FooContextCustomizerFactory.class)
+@interface CustomizeWithFoo {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/customizers/CustomizeWithFruit.java b/spring-test/src/test/java/org/springframework/test/context/customizers/CustomizeWithFruit.java
new file mode 100644
index 00000000000..2fa0d5383a9
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/customizers/CustomizeWithFruit.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2002-2023 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.customizers;
+
+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;
+
+/**
+ * Marker annotation to trigger {@link GlobalFruitContextCustomizerFactory}.
+ *
+ * @author Sam Brannen
+ * @since 6.1
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Inherited
+@interface CustomizeWithFruit {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/customizers/EnigmaContextCustomizerFactory.java b/spring-test/src/test/java/org/springframework/test/context/customizers/EnigmaContextCustomizerFactory.java
new file mode 100644
index 00000000000..1a690f2f90b
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/customizers/EnigmaContextCustomizerFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2002-2023 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.customizers;
+
+import java.util.List;
+
+import org.springframework.test.context.ContextConfigurationAttributes;
+import org.springframework.test.context.ContextCustomizer;
+import org.springframework.test.context.ContextCustomizerFactory;
+
+/**
+ * @author Sam Brannen
+ * @since 6.1
+ */
+class EnigmaContextCustomizerFactory implements ContextCustomizerFactory {
+
+ @Override
+ public ContextCustomizer createContextCustomizer(Class> testClass,
+ List configAttributes) {
+ return (context, mergedConfig) -> context.getBeanFactory().registerSingleton("enigma", 42);
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/customizers/FooContextCustomizerFactory.java b/spring-test/src/test/java/org/springframework/test/context/customizers/FooContextCustomizerFactory.java
new file mode 100644
index 00000000000..b645d551cd0
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/customizers/FooContextCustomizerFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2002-2023 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.customizers;
+
+import java.util.List;
+
+import org.springframework.test.context.ContextConfigurationAttributes;
+import org.springframework.test.context.ContextCustomizer;
+import org.springframework.test.context.ContextCustomizerFactory;
+
+/**
+ * @author Sam Brannen
+ * @since 6.1
+ */
+class FooContextCustomizerFactory implements ContextCustomizerFactory {
+
+ @Override
+ public ContextCustomizer createContextCustomizer(Class> testClass,
+ List configAttributes) {
+ return (context, mergedConfig) -> context.getBeanFactory().registerSingleton("foo", "bar");
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/customizers/GlobalFruitContextCustomizerFactory.java b/spring-test/src/test/java/org/springframework/test/context/customizers/GlobalFruitContextCustomizerFactory.java
new file mode 100644
index 00000000000..9a4e52c53b9
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/customizers/GlobalFruitContextCustomizerFactory.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2002-2023 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.customizers;
+
+import java.util.List;
+
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.lang.Nullable;
+import org.springframework.test.context.ContextConfigurationAttributes;
+import org.springframework.test.context.ContextCustomizer;
+import org.springframework.test.context.ContextCustomizerFactory;
+import org.springframework.test.context.MergedContextConfiguration;
+
+/**
+ * @author Sam Brannen
+ * @since 6.1
+ */
+class GlobalFruitContextCustomizerFactory implements ContextCustomizerFactory {
+
+ @Override
+ public ContextCustomizer createContextCustomizer(Class> testClass,
+ List configAttributes) {
+
+ if (testClass.isAnnotationPresent(CustomizeWithFruit.class)) {
+ return new GlobalFruitContextCustomizer();
+ }
+ return null;
+ }
+
+}
+
+class GlobalFruitContextCustomizer implements ContextCustomizer {
+
+ @Override
+ public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
+ // Use "global$fruit" as the bean name instead of something simple like "fruit"
+ // to avoid bean name clashes with any test that registers a bean named "fruit".
+ context.getBeanFactory().registerSingleton("global$fruit", "apple, banana, cherry");
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ return (this == other || (other != null && getClass() == other.getClass()));
+ }
+
+ @Override
+ public int hashCode() {
+ return getClass().hashCode();
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/customizers/InheritedContextCustomizerRegistrationTests.java b/spring-test/src/test/java/org/springframework/test/context/customizers/InheritedContextCustomizerRegistrationTests.java
new file mode 100644
index 00000000000..87153e9b344
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/customizers/InheritedContextCustomizerRegistrationTests.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2002-2023 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.customizers;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Sam Brannen
+ * @since 6.1
+ */
+@CustomizeWithBar
+class InheritedContextCustomizerRegistrationTests extends LocalContextCustomizerRegistrationTests {
+
+ @Autowired
+ @Qualifier("bar")
+ String bar;
+
+
+ @Override
+ @Test
+ void injectedBean() {
+ assertThat(fruit).isEqualTo("apple, banana, cherry");
+ assertThat(foo).isEqualTo("bar");
+ assertThat(bar).isEqualTo("baz");
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/customizers/LocalContextCustomizerRegistrationTests.java b/spring-test/src/test/java/org/springframework/test/context/customizers/LocalContextCustomizerRegistrationTests.java
new file mode 100644
index 00000000000..d02d463a22f
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/customizers/LocalContextCustomizerRegistrationTests.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2002-2023 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.customizers;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Sam Brannen
+ * @since 6.1
+ */
+@SpringJUnitConfig({})
+@CustomizeWithFruit
+@CustomizeWithFoo
+class LocalContextCustomizerRegistrationTests {
+
+ // GlobalFruitContextCustomizerFactory is registered via spring.factories
+ @Autowired(required = false)
+ @Qualifier("global$fruit")
+ String fruit;
+
+ @Autowired(required = false)
+ @Qualifier("foo")
+ String foo;
+
+
+ @Test
+ void injectedBean() {
+ assertThat(fruit).isEqualTo("apple, banana, cherry");
+ assertThat(foo).isEqualTo("bar");
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/customizers/OverriddenContextCustomizerRegistrationTests.java b/spring-test/src/test/java/org/springframework/test/context/customizers/OverriddenContextCustomizerRegistrationTests.java
new file mode 100644
index 00000000000..9f37e759518
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/customizers/OverriddenContextCustomizerRegistrationTests.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2002-2023 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.customizers;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.test.context.ContextCustomizerFactories;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Sam Brannen
+ * @since 6.1
+ */
+@ContextCustomizerFactories(factories = {BarContextCustomizerFactory.class, EnigmaContextCustomizerFactory.class},
+ inheritFactories = false)
+class OverriddenContextCustomizerRegistrationTests extends LocalContextCustomizerRegistrationTests {
+
+ @Autowired
+ @Qualifier("bar")
+ String bar;
+
+ @Autowired
+ Integer enigma;
+
+
+ @Override
+ @Test
+ void injectedBean() {
+ // globally registered via spring.factories
+ assertThat(fruit).isEqualTo("apple, banana, cherry");
+
+ // Overridden by this subclass (inheritFactories = false)
+ assertThat(foo).isNull();
+
+ // Local to this subclass
+ assertThat(bar).isEqualTo("baz");
+ assertThat(enigma).isEqualTo(42);
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/customizers/ReplaceDefaultsContextCustomizerTests.java b/spring-test/src/test/java/org/springframework/test/context/customizers/ReplaceDefaultsContextCustomizerTests.java
new file mode 100644
index 00000000000..7fee221e103
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/customizers/ReplaceDefaultsContextCustomizerTests.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2002-2023 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.customizers;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.test.context.ContextCustomizerFactories;
+import org.springframework.test.context.ContextCustomizerFactories.MergeMode;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Sam Brannen
+ * @since 6.1
+ */
+@SpringJUnitConfig({})
+@ContextCustomizerFactories(factories = {FooContextCustomizerFactory.class, BarContextCustomizerFactory.class},
+ mergeMode = MergeMode.REPLACE_DEFAULTS)
+class ReplaceDefaultsContextCustomizerTests {
+
+ // GlobalFruitContextCustomizerFactory is registered via spring.factories
+ @Autowired(required = false)
+ @Qualifier("global$fruit")
+ String fruit;
+
+ @Autowired(required = false)
+ @Qualifier("foo")
+ String foo;
+
+ @Autowired(required = false)
+ @Qualifier("bar")
+ String bar;
+
+
+ @Test
+ void injectedBean() {
+ // MergeMode.REPLACE_DEFAULTS overrides spring.factories lookup
+ assertThat(fruit).isNull();
+
+ // From local @ContextCustomizerFactories
+ assertThat(foo).isEqualTo("bar");
+ assertThat(bar).isEqualTo("baz");
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/ContextCustomizerSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/ContextCustomizerSpringRunnerTests.java
deleted file mode 100644
index f4cba2cb6bd..00000000000
--- a/spring-test/src/test/java/org/springframework/test/context/junit4/ContextCustomizerSpringRunnerTests.java
+++ /dev/null
@@ -1,66 +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.junit4;
-
-import java.util.List;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.test.context.BootstrapWith;
-import org.springframework.test.context.ContextCustomizer;
-import org.springframework.test.context.ContextCustomizerFactory;
-import org.springframework.test.context.junit4.ContextCustomizerSpringRunnerTests.CustomTestContextBootstrapper;
-import org.springframework.test.context.support.DefaultTestContextBootstrapper;
-
-import static java.util.Collections.singletonList;
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * JUnit 4 based integration test which verifies support of
- * {@link ContextCustomizerFactory} and {@link ContextCustomizer}.
- *
- * @author Sam Brannen
- * @author Phillip Webb
- * @since 4.3
- */
-@RunWith(SpringRunner.class)
-@BootstrapWith(CustomTestContextBootstrapper.class)
-public class ContextCustomizerSpringRunnerTests {
-
- @Autowired String foo;
-
-
- @Test
- public void injectedBean() {
- assertThat(foo).isEqualTo("foo");
- }
-
-
- static class CustomTestContextBootstrapper extends DefaultTestContextBootstrapper {
-
- @Override
- protected List getContextCustomizerFactories() {
- return singletonList(
- (ContextCustomizerFactory) (testClass, configAttributes) ->
- (ContextCustomizer) (context, mergedConfig) -> context.getBeanFactory().registerSingleton("foo", "foo")
- );
- }
- }
-
-}
diff --git a/spring-test/src/test/resources/META-INF/spring.factories b/spring-test/src/test/resources/META-INF/spring.factories
index cd50c180c2f..8dc245e5c8c 100644
--- a/spring-test/src/test/resources/META-INF/spring.factories
+++ b/spring-test/src/test/resources/META-INF/spring.factories
@@ -4,7 +4,8 @@
org.springframework.test.context.TestExecutionListener = org.example.FooListener
org.springframework.test.context.ContextCustomizerFactory =\
- org.springframework.test.context.aot.samples.basic.ImportsContextCustomizerFactory
+ org.springframework.test.context.aot.samples.basic.ImportsContextCustomizerFactory,\
+ org.springframework.test.context.customizers.GlobalFruitContextCustomizerFactory
org.springframework.test.context.ApplicationContextFailureProcessor =\
org.springframework.test.context.failures.TrackingApplicationContextFailureProcessor