From a220c545fc1d4f6d8d7f0cc01d7c8089c70389a4 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 30 Jun 2023 13:29:12 +0200 Subject: [PATCH] Support declarative ContextCustomizerFactory registration in the TCF Prior to this commit, it was only possible to register a ContextCustomizerFactory in the TestContext framework (TCF) via the SpringFactoriesLoader mechanism. This commit introduces support for declarative registration of a ContextCustomizerFactory local to a test class via a new @ContextCustomizerFactories annotation. Closes gh-26148 --- framework-docs/modules/ROOT/nav.adoc | 2 + .../integration-junit-jupiter.adoc | 1 + .../testing/annotations/integration-meta.adoc | 1 + .../annotations/integration-spring.adoc | 1 + ...annotation-contextcustomizerfactories.adoc | 45 +++++++ .../testcontext-framework/ctx-management.adoc | 1 + .../ctx-management/context-customizers.adoc | 69 ++++++++++ .../test/context/ContextCustomizer.java | 1 + .../context/ContextCustomizerFactories.java | 125 ++++++++++++++++++ .../context/ContextCustomizerFactory.java | 10 +- .../AbstractTestContextBootstrapper.java | 93 ++++++++++++- .../BarContextCustomizerFactory.java | 37 ++++++ ...veRegistrationComposedAnnotationTests.java | 70 ++++++++++ ...ustomizerDeclarativeRegistrationTests.java | 77 +++++++++++ .../customizers/ContextCustomizerTests.java | 94 +++++++++++++ .../context/customizers/CustomizeWithBar.java | 34 +++++ .../context/customizers/CustomizeWithFoo.java | 34 +++++ .../customizers/CustomizeWithFruit.java | 35 +++++ .../EnigmaContextCustomizerFactory.java | 37 ++++++ .../FooContextCustomizerFactory.java | 37 ++++++ .../GlobalFruitContextCustomizerFactory.java | 65 +++++++++ ...tedContextCustomizerRegistrationTests.java | 46 +++++++ ...calContextCustomizerRegistrationTests.java | 52 ++++++++ ...denContextCustomizerRegistrationTests.java | 57 ++++++++ ...ReplaceDefaultsContextCustomizerTests.java | 62 +++++++++ .../ContextCustomizerSpringRunnerTests.java | 66 --------- .../test/resources/META-INF/spring.factories | 3 +- 27 files changed, 1085 insertions(+), 70 deletions(-) create mode 100644 framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc create mode 100644 framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-customizers.adoc create mode 100644 spring-test/src/main/java/org/springframework/test/context/ContextCustomizerFactories.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/customizers/BarContextCustomizerFactory.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/customizers/ContextCustomizerDeclarativeRegistrationComposedAnnotationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/customizers/ContextCustomizerDeclarativeRegistrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/customizers/ContextCustomizerTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/customizers/CustomizeWithBar.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/customizers/CustomizeWithFoo.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/customizers/CustomizeWithFruit.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/customizers/EnigmaContextCustomizerFactory.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/customizers/FooContextCustomizerFactory.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/customizers/GlobalFruitContextCustomizerFactory.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/customizers/InheritedContextCustomizerRegistrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/customizers/LocalContextCustomizerRegistrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/customizers/OverriddenContextCustomizerRegistrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/customizers/ReplaceDefaultsContextCustomizerTests.java delete mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/ContextCustomizerSpringRunnerTests.java 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[] 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[] 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 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... classes) { + List factories = new ArrayList<>(classes.length); + for (Class 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