diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc
index f2f7655f1f..2952797003 100644
--- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc
+++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc
@@ -47,6 +47,9 @@ Java::
<2> The result of this static method will be used as the instance and injected into the field
======
+NOTE: The method to invoke is searched in the test class and any enclosing class it might
+have, as well as its hierarchy. This typically allows nested test class to provide the
+method to use in the root test class.
[[spring-testing-annotation-beanoverriding-mockitobean]]
== `@MockitoBean` and `@MockitoSpyBean`
@@ -100,32 +103,31 @@ and associated infrastructure, which allows to define custom bean overriding var
In order to provide an extension, three classes are needed:
- - A concrete `BeanOverrideProcessor
`.
- - A concrete `OverrideMetadata` created by said processor.
+ - A concrete `BeanOverrideProcessor` implementation, `P`.
+ - One or more concrete `OverrideMetadata` implementations created by said processor.
- An annotation meta-annotated with `@BeanOverride(P.class)`.
The Spring TestContext Framework includes infrastructure classes that support bean
-overriding: a `BeanPostProcessor`, a `TestExecutionListener` and a `ContextCustomizerFactory`.
-These are automatically registered via the Spring TestContext Framework `spring.factories`
-file.
+overriding: a `BeanFactoryPostProcessor`, a `TestExecutionListener` and a `ContextCustomizerFactory`.
+The later two are automatically registered via the Spring TestContext Framework
+`spring.factories` file, and are responsible for setting up the rest of the infrastructure.
The test classes are parsed looking for any field meta-annotated with `@BeanOverride`,
instantiating the relevant `BeanOverrideProcessor` in order to register an `OverrideMetadata`.
-Then the `BeanOverrideBeanPostProcessor` will use that information to alter the Context,
-registering and replacing bean definitions as influenced by each metadata
+Then the `BeanOverrideBeanFactoryPostProcessor` will use that information to alter the
+Context, registering and replacing bean definitions as influenced by each metadata
`BeanOverrideStrategy`:
- `REPLACE_DEFINITION`: the bean post-processor replaces the bean definition.
If it is not present in the context, an exception is thrown.
- `CREATE_OR_REPLACE_DEFINITION`: same as above but if the bean definition is not present
in the context, one is created
- - `WRAP_EARLY_BEAN`: an original instance is obtained via
-`SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference(Object, String)` and
-provided to the processor during `OverrideMetadata` creation.
+ - `WRAP_EARLY_BEAN`: an original instance is obtained and passed to the `OverrideMetadata`
+when the override instance is created.
-NOTE: The Bean Overriding infrastructure works best with singleton beans. It also doesn't
-include any bean resolution (unlike e.g. an `@Autowired`-annotated field). As such, the
-name of the bean to override MUST be somehow provided to or computed by the
-`BeanOverrideProcessor`. Typically, the end user provides the name as part of the custom
-annotation's attributes, or the annotated field's name.
\ No newline at end of file
+NOTE: The Bean Overriding infrastructure doesn't include any bean resolution step
+(unlike e.g. an `@Autowired`-annotated field). As such, the name of the bean to override
+MUST be somehow provided to or computed by the `BeanOverrideProcessor`. Typically, the end
+user provides the name as part of the custom annotation's attributes, or the annotated
+field's name.
\ No newline at end of file
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java
index d93499a44d..2bfd17a7fe 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java
@@ -34,7 +34,7 @@ import java.lang.annotation.Target;
*
* @author Simon Baslé
* @since 6.2
- * @see BeanOverrideBeanPostProcessor
+ * @see BeanOverrideBeanFactoryPostProcessor
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java
new file mode 100644
index 0000000000..60f914df6f
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright 2002-2024 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.bean.override;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+
+import org.springframework.aop.scope.ScopedProxyUtils;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactoryUtils;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.config.ConstructorArgumentValues;
+import org.springframework.beans.factory.config.RuntimeBeanReference;
+import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.beans.factory.support.RootBeanDefinition;
+import org.springframework.core.Ordered;
+import org.springframework.core.PriorityOrdered;
+import org.springframework.core.ResolvableType;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * A {@link BeanFactoryPostProcessor} implementation that processes test classes
+ * and adapt the {@link BeanDefinitionRegistry} for any {@link BeanOverride} it
+ * may define.
+ *
+ *
A set of classes from which to parse {@link OverrideMetadata} must be
+ * provided to this processor. Each test class is expected to use any
+ * annotation meta-annotated with {@link BeanOverride @BeanOverride} to mark
+ * beans to override. The {@link BeanOverrideParsingUtils#hasBeanOverride(Class)}
+ * method can be used to check if a class matches the above criteria.
+ *
+ *
The provided classes are fully parsed at creation to build a metadata set.
+ * This processor implements several {@link BeanOverrideStrategy overriding
+ * strategy} and chooses the correct one according to each override metadata
+ * {@link OverrideMetadata#getStrategy()} method. Additionally, it provides
+ * support for injecting the overridden bean instances into their corresponding
+ * annotated {@link Field fields}.
+ *
+ * @author Simon Baslé
+ * @since 6.2
+ */
+class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered {
+
+ private static final String INFRASTRUCTURE_BEAN_NAME = BeanOverrideBeanFactoryPostProcessor.class.getName();
+
+ private static final String EARLY_INFRASTRUCTURE_BEAN_NAME =
+ BeanOverrideBeanFactoryPostProcessor.WrapEarlyBeanPostProcessor.class.getName();
+
+ private final BeanOverrideRegistrar overrideRegistrar;
+
+
+ /**
+ * Create a new {@code BeanOverrideBeanFactoryPostProcessor} instance with
+ * the given {@link BeanOverrideRegistrar}, which contains a set of parsed
+ * {@link OverrideMetadata}.
+ * @param overrideRegistrar the {@link BeanOverrideRegistrar} used to track
+ * metadata
+ */
+ public BeanOverrideBeanFactoryPostProcessor(BeanOverrideRegistrar overrideRegistrar) {
+ this.overrideRegistrar = overrideRegistrar;
+ }
+
+
+ @Override
+ public int getOrder() {
+ return Ordered.LOWEST_PRECEDENCE - 10;
+ }
+
+
+ @Override
+ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
+ Assert.isInstanceOf(DefaultListableBeanFactory.class, beanFactory,
+ "Bean overriding annotations can only be used on a DefaultListableBeanFactory");
+ postProcessWithRegistry((DefaultListableBeanFactory) beanFactory);
+ }
+
+ private void postProcessWithRegistry(DefaultListableBeanFactory registry) {
+ for (OverrideMetadata metadata : this.overrideRegistrar.getOverrideMetadata()) {
+ registerBeanOverride(registry, metadata);
+ }
+ }
+
+ /**
+ * Copy certain details of a {@link BeanDefinition} to the definition created by
+ * this processor for a given {@link OverrideMetadata}.
+ *
The default implementation copies the {@linkplain BeanDefinition#isPrimary()
+ * primary flag} and the {@linkplain BeanDefinition#getScope() scope}.
+ */
+ protected void copyBeanDefinitionDetails(BeanDefinition from, RootBeanDefinition to) {
+ to.setPrimary(from.isPrimary());
+ to.setScope(from.getScope());
+ }
+
+ private void registerBeanOverride(DefaultListableBeanFactory beanFactory, OverrideMetadata overrideMetadata) {
+ switch (overrideMetadata.getStrategy()) {
+ case REPLACE_DEFINITION -> registerReplaceDefinition(beanFactory, overrideMetadata, true);
+ case REPLACE_OR_CREATE_DEFINITION -> registerReplaceDefinition(beanFactory, overrideMetadata, false);
+ case WRAP_BEAN -> registerWrapBean(beanFactory, overrideMetadata);
+ }
+ }
+
+ private void registerReplaceDefinition(DefaultListableBeanFactory beanFactory, OverrideMetadata overrideMetadata, boolean enforceExistingDefinition) {
+
+ RootBeanDefinition beanDefinition = createBeanDefinition(overrideMetadata);
+ String beanName = overrideMetadata.getBeanName();
+
+ BeanDefinition existingBeanDefinition = null;
+ if (beanFactory.containsBeanDefinition(beanName)) {
+ existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
+ copyBeanDefinitionDetails(existingBeanDefinition, beanDefinition);
+ beanFactory.removeBeanDefinition(beanName);
+ }
+ else if (enforceExistingDefinition) {
+ throw new IllegalStateException("Unable to override bean '" + beanName + "'; there is no" +
+ " bean definition to replace with that name");
+ }
+ beanFactory.registerBeanDefinition(beanName, beanDefinition);
+
+ Object override = overrideMetadata.createOverride(beanName, existingBeanDefinition, null);
+ if (beanFactory.isSingleton(beanName)) {
+ // Now we have an instance (the override) that we can register.
+ // At this stage we don't expect a singleton instance to be present,
+ // and this call will throw if there is such an instance already.
+ beanFactory.registerSingleton(beanName, override);
+ }
+
+ overrideMetadata.track(override, beanFactory);
+ this.overrideRegistrar.registerNameForMetadata(overrideMetadata, beanName);
+ }
+
+ /**
+ * Check that the expected bean name is registered and matches the type to override.
+ *
If so, put the override metadata in the early tracking map.
+ *
The map will later be checked to see if a given bean should be wrapped
+ * upon creation, during the {@link WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String)}
+ * phase.
+ */
+ private void registerWrapBean(DefaultListableBeanFactory beanFactory, OverrideMetadata metadata) {
+ Set existingBeanNames = getExistingBeanNames(beanFactory, metadata.getBeanType());
+ String beanName = metadata.getBeanName();
+ if (!existingBeanNames.contains(beanName)) {
+ throw new IllegalStateException("Unable to override bean '" + beanName + "' by wrapping," +
+ " no existing bean instance by this name of type " + metadata.getBeanType());
+ }
+ this.overrideRegistrar.markWrapEarly(metadata, beanName);
+ this.overrideRegistrar.registerNameForMetadata(metadata, beanName);
+ }
+
+ private RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) {
+ RootBeanDefinition definition = new RootBeanDefinition();
+ definition.setTargetType(metadata.getBeanType());
+ return definition;
+ }
+
+ private Set getExistingBeanNames(DefaultListableBeanFactory beanFactory, ResolvableType resolvableType) {
+ Set beans = new LinkedHashSet<>(
+ Arrays.asList(beanFactory.getBeanNamesForType(resolvableType, true, false)));
+ Class> type = resolvableType.resolve(Object.class);
+ for (String beanName : beanFactory.getBeanNamesForType(FactoryBean.class, true, false)) {
+ beanName = BeanFactoryUtils.transformedBeanName(beanName);
+ BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
+ Object attribute = beanDefinition.getAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE);
+ if (resolvableType.equals(attribute) || type.equals(attribute)) {
+ beans.add(beanName);
+ }
+ }
+ beans.removeIf(ScopedProxyUtils::isScopedTarget);
+ return beans;
+ }
+
+ /**
+ * Register a {@link BeanOverrideBeanFactoryPostProcessor} with a {@link BeanDefinitionRegistry}.
+ * Not required when using the Spring TestContext Framework, as registration
+ * is automatic via the
+ * {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader}
+ * mechanism.
+ * @param registry the bean definition registry
+ */
+ public static void register(BeanDefinitionRegistry registry) {
+ RuntimeBeanReference registrarReference = new RuntimeBeanReference(BeanOverrideRegistrar.INFRASTRUCTURE_BEAN_NAME);
+ // Early processor
+ addInfrastructureBeanDefinition(
+ registry, WrapEarlyBeanPostProcessor.class, EARLY_INFRASTRUCTURE_BEAN_NAME, constructorArgs ->
+ constructorArgs.addIndexedArgumentValue(0, registrarReference));
+
+ // Main processor
+ addInfrastructureBeanDefinition(
+ registry, BeanOverrideBeanFactoryPostProcessor.class, INFRASTRUCTURE_BEAN_NAME, constructorArgs ->
+ constructorArgs.addIndexedArgumentValue(0, registrarReference));
+ }
+
+ private static void addInfrastructureBeanDefinition(BeanDefinitionRegistry registry,
+ Class> clazz, String beanName, Consumer constructorArgumentsConsumer) {
+
+ if (!registry.containsBeanDefinition(beanName)) {
+ RootBeanDefinition definition = new RootBeanDefinition(clazz);
+ definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
+ ConstructorArgumentValues constructorArguments = definition.getConstructorArgumentValues();
+ constructorArgumentsConsumer.accept(constructorArguments);
+ registry.registerBeanDefinition(beanName, definition);
+ }
+ }
+
+
+ static final class WrapEarlyBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor,
+ PriorityOrdered {
+
+ private final BeanOverrideRegistrar overrideRegistrar;
+
+ private final Map earlyReferences;
+
+
+ private WrapEarlyBeanPostProcessor(BeanOverrideRegistrar registrar) {
+ this.overrideRegistrar = registrar;
+ this.earlyReferences = new ConcurrentHashMap<>(16);
+ }
+
+
+ @Override
+ public int getOrder() {
+ return Ordered.HIGHEST_PRECEDENCE;
+ }
+
+ @Override
+ public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
+ if (bean instanceof FactoryBean) {
+ return bean;
+ }
+ this.earlyReferences.put(getCacheKey(bean, beanName), bean);
+ return this.overrideRegistrar.wrapIfNecessary(bean, beanName);
+ }
+
+ @Override
+ public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+ if (bean instanceof FactoryBean) {
+ return bean;
+ }
+ if (this.earlyReferences.remove(getCacheKey(bean, beanName)) != bean) {
+ return this.overrideRegistrar.wrapIfNecessary(bean, beanName);
+ }
+ return bean;
+ }
+
+ private String getCacheKey(Object bean, String beanName) {
+ return (StringUtils.hasLength(beanName) ? beanName : bean.getClass().getName());
+ }
+
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessor.java
deleted file mode 100644
index fdb004afc3..0000000000
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessor.java
+++ /dev/null
@@ -1,382 +0,0 @@
-/*
- * Copyright 2002-2024 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.bean.override;
-
-import java.lang.reflect.Field;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Consumer;
-
-import org.springframework.aop.scope.ScopedProxyUtils;
-import org.springframework.beans.BeansException;
-import org.springframework.beans.PropertyValues;
-import org.springframework.beans.factory.BeanCreationException;
-import org.springframework.beans.factory.BeanFactory;
-import org.springframework.beans.factory.BeanFactoryAware;
-import org.springframework.beans.factory.BeanFactoryUtils;
-import org.springframework.beans.factory.FactoryBean;
-import org.springframework.beans.factory.config.BeanDefinition;
-import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
-import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
-import org.springframework.beans.factory.config.ConstructorArgumentValues;
-import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;
-import org.springframework.beans.factory.config.RuntimeBeanReference;
-import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
-import org.springframework.beans.factory.support.BeanDefinitionRegistry;
-import org.springframework.beans.factory.support.RootBeanDefinition;
-import org.springframework.context.ApplicationContext;
-import org.springframework.core.Ordered;
-import org.springframework.core.PriorityOrdered;
-import org.springframework.core.ResolvableType;
-import org.springframework.lang.Nullable;
-import org.springframework.util.Assert;
-import org.springframework.util.ReflectionUtils;
-import org.springframework.util.StringUtils;
-
-/**
- * A {@link BeanFactoryPostProcessor} used to register and inject overriding
- * bean metadata with the {@link ApplicationContext}.
- *
- * A set of {@link OverrideMetadata} must be provided to this processor. A
- * {@link BeanOverrideParser} can typically be used to parse this metadata from
- * test classes that use any annotation meta-annotated with
- * {@link BeanOverride @BeanOverride} to mark override sites.
- *
- *
This processor supports two types of {@link BeanOverrideStrategy}:
- *
- * - Replacing a given bean's definition, immediately preparing a singleton
- * instance
- * - Intercepting the actual bean instance upon creation and wrapping it,
- * using the early bean definition mechanism of
- * {@link SmartInstantiationAwareBeanPostProcessor}
- *
- *
- * This processor also provides support for injecting the overridden bean
- * instances into their corresponding annotated {@link Field fields}.
- *
- * @author Simon Baslé
- * @since 6.2
- */
-public class BeanOverrideBeanPostProcessor implements InstantiationAwareBeanPostProcessor,
- BeanFactoryAware, BeanFactoryPostProcessor, Ordered {
-
- private static final String INFRASTRUCTURE_BEAN_NAME = BeanOverrideBeanPostProcessor.class.getName();
-
- private static final String EARLY_INFRASTRUCTURE_BEAN_NAME =
- BeanOverrideBeanPostProcessor.WrapEarlyBeanPostProcessor.class.getName();
-
-
- private final Map earlyOverrideMetadata = new HashMap<>();
-
- private final Map beanNameRegistry = new HashMap<>();
-
- private final Map fieldRegistry = new HashMap<>();
-
- private final Set overrideMetadata;
-
- @Nullable
- private ConfigurableListableBeanFactory beanFactory;
-
-
- /**
- * Create a new {@code BeanOverrideBeanPostProcessor} instance with the
- * given {@link OverrideMetadata} set.
- * @param overrideMetadata the initial override metadata
- */
- public BeanOverrideBeanPostProcessor(Set overrideMetadata) {
- this.overrideMetadata = overrideMetadata;
- }
-
-
- @Override
- public int getOrder() {
- return Ordered.LOWEST_PRECEDENCE - 10;
- }
-
- @Override
- public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
- Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory,
- "Bean overriding can only be used with a ConfigurableListableBeanFactory");
- this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
- }
-
- /**
- * Return this processor's {@link OverrideMetadata} set.
- */
- protected Set getOverrideMetadata() {
- return this.overrideMetadata;
- }
-
- @Override
- public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
- Assert.state(this.beanFactory == beanFactory, "Unexpected BeanFactory to post-process");
- Assert.isInstanceOf(BeanDefinitionRegistry.class, beanFactory,
- "Bean overriding annotations can only be used on bean factories that implement "
- + "BeanDefinitionRegistry");
- postProcessWithRegistry((BeanDefinitionRegistry) beanFactory);
- }
-
- private void postProcessWithRegistry(BeanDefinitionRegistry registry) {
- // Note that a tracker bean is registered down the line only if there is some overrideMetadata parsed.
- for (OverrideMetadata metadata : getOverrideMetadata()) {
- registerBeanOverride(registry, metadata);
- }
- }
-
- /**
- * Copy certain details of a {@link BeanDefinition} to the definition created by
- * this processor for a given {@link OverrideMetadata}.
- * The default implementation copies the {@linkplain BeanDefinition#isPrimary()
- * primary flag} and the {@linkplain BeanDefinition#getScope() scope}.
- */
- protected void copyBeanDefinitionDetails(BeanDefinition from, RootBeanDefinition to) {
- to.setPrimary(from.isPrimary());
- to.setScope(from.getScope());
- }
-
- private void registerBeanOverride(BeanDefinitionRegistry registry, OverrideMetadata overrideMetadata) {
- switch (overrideMetadata.getBeanOverrideStrategy()) {
- case REPLACE_DEFINITION -> registerReplaceDefinition(registry, overrideMetadata, true);
- case REPLACE_OR_CREATE_DEFINITION -> registerReplaceDefinition(registry, overrideMetadata, false);
- case WRAP_EARLY_BEAN -> registerWrapEarly(overrideMetadata);
- }
- }
-
- private void registerReplaceDefinition(BeanDefinitionRegistry registry, OverrideMetadata overrideMetadata,
- boolean enforceExistingDefinition) {
-
- RootBeanDefinition beanDefinition = createBeanDefinition(overrideMetadata);
- String beanName = overrideMetadata.getExpectedBeanName();
-
- BeanDefinition existingBeanDefinition = null;
- if (registry.containsBeanDefinition(beanName)) {
- existingBeanDefinition = registry.getBeanDefinition(beanName);
- copyBeanDefinitionDetails(existingBeanDefinition, beanDefinition);
- registry.removeBeanDefinition(beanName);
- }
- else if (enforceExistingDefinition) {
- throw new IllegalStateException("Unable to override " + overrideMetadata.getBeanOverrideDescription() +
- " bean; expected a bean definition to replace with name '" + beanName + "'");
- }
- registry.registerBeanDefinition(beanName, beanDefinition);
-
- Object override = overrideMetadata.createOverride(beanName, existingBeanDefinition, null);
- Assert.state(this.beanFactory != null, "ConfigurableListableBeanFactory must not be null");
- if (this.beanFactory.isSingleton(beanName)) {
- // Now we have an instance (the override) that we can register.
- // At this stage we don't expect a singleton instance to be present,
- // and this call will throw if there is such an instance already.
- this.beanFactory.registerSingleton(beanName, override);
- }
-
- overrideMetadata.track(override, this.beanFactory);
- this.beanNameRegistry.put(overrideMetadata, beanName);
- this.fieldRegistry.put(overrideMetadata.field(), beanName);
- }
-
- /**
- * Check that the expected bean name is registered and matches the type to override.
- *
If so, put the override metadata in the early tracking map.
- *
The map will later be checked to see if a given bean should be wrapped
- * upon creation, during the {@link WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String)}
- * phase.
- */
- private void registerWrapEarly(OverrideMetadata metadata) {
- Set existingBeanNames = getExistingBeanNames(metadata.typeToOverride());
- String beanName = metadata.getExpectedBeanName();
- if (!existingBeanNames.contains(beanName)) {
- throw new IllegalStateException("Unable to override wrap-early bean named '" + beanName + "', not found among " +
- existingBeanNames);
- }
- this.earlyOverrideMetadata.put(beanName, metadata);
- this.beanNameRegistry.put(metadata, beanName);
- this.fieldRegistry.put(metadata.field(), beanName);
- }
-
- /**
- * Check early override records and use the {@link OverrideMetadata} to
- * create an override instance from the provided bean, if relevant.
- * Called during the {@link SmartInstantiationAwareBeanPostProcessor}
- * phases.
- * @see WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String)
- * @see WrapEarlyBeanPostProcessor#postProcessAfterInitialization(Object, String)
- */
- protected final Object wrapIfNecessary(Object bean, String beanName) throws BeansException {
- final OverrideMetadata metadata = this.earlyOverrideMetadata.get(beanName);
- if (metadata != null && metadata.getBeanOverrideStrategy() == BeanOverrideStrategy.WRAP_EARLY_BEAN) {
- bean = metadata.createOverride(beanName, null, bean);
- Assert.state(this.beanFactory != null, "ConfigurableListableBeanFactory must not be null");
- metadata.track(bean, this.beanFactory);
- }
- return bean;
- }
-
- private RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) {
- RootBeanDefinition definition = new RootBeanDefinition(metadata.typeToOverride().resolve());
- definition.setTargetType(metadata.typeToOverride());
- return definition;
- }
-
- private Set getExistingBeanNames(ResolvableType resolvableType) {
- Assert.state(this.beanFactory != null, "ConfigurableListableBeanFactory must not be null");
- Set beans = new LinkedHashSet<>(
- Arrays.asList(this.beanFactory.getBeanNamesForType(resolvableType, true, false)));
- Class> type = resolvableType.resolve(Object.class);
- for (String beanName : this.beanFactory.getBeanNamesForType(FactoryBean.class, true, false)) {
- beanName = BeanFactoryUtils.transformedBeanName(beanName);
- BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition(beanName);
- Object attribute = beanDefinition.getAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE);
- if (resolvableType.equals(attribute) || type.equals(attribute)) {
- beans.add(beanName);
- }
- }
- beans.removeIf(ScopedProxyUtils::isScopedTarget);
- return beans;
- }
-
- @Override
- public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName)
- throws BeansException {
- ReflectionUtils.doWithFields(bean.getClass(), field -> postProcessField(bean, field));
- return pvs;
- }
-
- private void postProcessField(Object bean, Field field) {
- String beanName = this.fieldRegistry.get(field);
- if (StringUtils.hasText(beanName)) {
- inject(field, bean, beanName);
- }
- }
-
- void inject(Field field, Object target, OverrideMetadata overrideMetadata) {
- String beanName = this.beanNameRegistry.get(overrideMetadata);
- Assert.state(StringUtils.hasLength(beanName),
- () -> "No bean found for OverrideMetadata: " + overrideMetadata);
- inject(field, target, beanName);
- }
-
- private void inject(Field field, Object target, String beanName) {
- try {
- ReflectionUtils.makeAccessible(field);
- Object existingValue = ReflectionUtils.getField(field, target);
- Assert.state(this.beanFactory != null, "ConfigurableListableBeanFactory must not be null");
- Object bean = this.beanFactory.getBean(beanName, field.getType());
- if (existingValue == bean) {
- return;
- }
- Assert.state(existingValue == null, () -> "The existing value '" + existingValue +
- "' of field '" + field + "' is not the same as the new value '" + bean + "'");
- ReflectionUtils.setField(field, target, bean);
- }
- catch (Throwable ex) {
- throw new BeanCreationException("Could not inject field '" + field + "'", ex);
- }
- }
-
- /**
- * Register a {@link BeanOverrideBeanPostProcessor} with a {@link BeanDefinitionRegistry}.
- * Not required when using the Spring TestContext Framework, as registration
- * is automatic via the
- * {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader}
- * mechanism.
- * @param registry the bean definition registry
- * @param overrideMetadata the initial override metadata set
- */
- public static void register(BeanDefinitionRegistry registry, @Nullable Set overrideMetadata) {
- // Early processor
- getOrAddInfrastructureBeanDefinition(
- registry, WrapEarlyBeanPostProcessor.class, EARLY_INFRASTRUCTURE_BEAN_NAME, constructorArgs ->
- constructorArgs.addIndexedArgumentValue(0, new RuntimeBeanReference(INFRASTRUCTURE_BEAN_NAME)));
-
- // Main processor
- BeanDefinition definition = getOrAddInfrastructureBeanDefinition(
- registry, BeanOverrideBeanPostProcessor.class, INFRASTRUCTURE_BEAN_NAME, constructorArgs ->
- constructorArgs.addIndexedArgumentValue(0, new LinkedHashSet()));
- ConstructorArgumentValues.ValueHolder constructorArg =
- definition.getConstructorArgumentValues().getIndexedArgumentValue(0, Set.class);
- @SuppressWarnings({"unchecked", "NullAway"})
- Set existing = (Set) constructorArg.getValue();
- if (overrideMetadata != null && existing != null) {
- existing.addAll(overrideMetadata);
- }
- }
-
- private static BeanDefinition getOrAddInfrastructureBeanDefinition(BeanDefinitionRegistry registry,
- Class> clazz, String beanName, Consumer constructorArgumentsConsumer) {
-
- if (!registry.containsBeanDefinition(beanName)) {
- RootBeanDefinition definition = new RootBeanDefinition(clazz);
- definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
- ConstructorArgumentValues constructorArguments = definition.getConstructorArgumentValues();
- constructorArgumentsConsumer.accept(constructorArguments);
- registry.registerBeanDefinition(beanName, definition);
- return definition;
- }
- return registry.getBeanDefinition(beanName);
- }
-
-
- private static final class WrapEarlyBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor,
- PriorityOrdered {
-
- private final BeanOverrideBeanPostProcessor mainProcessor;
-
- private final Map earlyReferences;
-
-
- private WrapEarlyBeanPostProcessor(BeanOverrideBeanPostProcessor mainProcessor) {
- this.mainProcessor = mainProcessor;
- this.earlyReferences = new ConcurrentHashMap<>(16);
- }
-
-
- @Override
- public int getOrder() {
- return Ordered.HIGHEST_PRECEDENCE;
- }
-
- @Override
- public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
- if (bean instanceof FactoryBean) {
- return bean;
- }
- this.earlyReferences.put(getCacheKey(bean, beanName), bean);
- return this.mainProcessor.wrapIfNecessary(bean, beanName);
- }
-
- @Override
- public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
- if (bean instanceof FactoryBean) {
- return bean;
- }
- if (this.earlyReferences.remove(getCacheKey(bean, beanName)) != bean) {
- return this.mainProcessor.wrapIfNecessary(bean, beanName);
- }
- return bean;
- }
-
- private String getCacheKey(Object bean, String beanName) {
- return (StringUtils.hasLength(beanName) ? beanName : bean.getClass().getName());
- }
-
- }
-
-}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java
index d2371de48d..6cddebf650 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java
@@ -16,6 +16,7 @@
package org.springframework.test.context.bean.override;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
@@ -29,56 +30,51 @@ import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.TestContextAnnotationUtils;
/**
- * {@link ContextCustomizerFactory} which provides support for Bean Overriding
- * in tests.
+ * {@link ContextCustomizerFactory} implementation that provides support for
+ * Bean Overriding.
*
* @author Simon Baslé
* @since 6.2
+ * @see BeanOverride
*/
-public class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory {
+class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory {
@Override
@Nullable
public ContextCustomizer createContextCustomizer(Class> testClass,
List configAttributes) {
- BeanOverrideParser parser = new BeanOverrideParser();
- parseMetadata(testClass, parser);
- if (parser.getOverrideMetadata().isEmpty()) {
+ Set> detectedClasses = new LinkedHashSet<>();
+ findClassesWithBeanOverride(testClass, detectedClasses);
+ if (detectedClasses.isEmpty()) {
return null;
}
- return new BeanOverrideContextCustomizer(parser.getOverrideMetadata());
+ return new BeanOverrideContextCustomizer(detectedClasses);
}
- private void parseMetadata(Class> testClass, BeanOverrideParser parser) {
- parser.parse(testClass);
+ private void findClassesWithBeanOverride(Class> testClass, Set> detectedClasses) {
+ if (BeanOverrideParsingUtils.hasBeanOverride(testClass)) {
+ detectedClasses.add(testClass);
+ }
if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) {
- parseMetadata(testClass.getEnclosingClass(), parser);
+ findClassesWithBeanOverride(testClass.getEnclosingClass(), detectedClasses);
}
}
- /**
- * {@link ContextCustomizer} for Bean Overriding in tests.
- */
private static final class BeanOverrideContextCustomizer implements ContextCustomizer {
- private final Set metadata;
+ private final Set> detectedClasses;
- /**
- * Construct a context customizer given some pre-existing override
- * metadata.
- * @param metadata a set of concrete {@link OverrideMetadata} provided
- * by the underlying {@link BeanOverrideParser}
- */
- BeanOverrideContextCustomizer(Set metadata) {
- this.metadata = metadata;
+ BeanOverrideContextCustomizer(Set> detectedClasses) {
+ this.detectedClasses = detectedClasses;
}
@Override
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
if (context instanceof BeanDefinitionRegistry registry) {
- BeanOverrideBeanPostProcessor.register(registry, this.metadata);
+ BeanOverrideRegistrar.register(registry, this.detectedClasses);
+ BeanOverrideBeanFactoryPostProcessor.register(registry);
}
}
@@ -91,12 +87,12 @@ public class BeanOverrideContextCustomizerFactory implements ContextCustomizerFa
return false;
}
BeanOverrideContextCustomizer other = (BeanOverrideContextCustomizer) obj;
- return this.metadata.equals(other.metadata);
+ return this.detectedClasses.equals(other.detectedClasses);
}
@Override
public int hashCode() {
- return this.metadata.hashCode();
+ return this.detectedClasses.hashCode();
}
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtils.java
similarity index 51%
rename from spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java
rename to spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtils.java
index e8c4f15177..c6ee7e7166 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtils.java
@@ -18,13 +18,12 @@ package org.springframework.test.context.bean.override;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
-import java.util.Collections;
import java.util.LinkedHashSet;
+import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.beans.BeanUtils;
-import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.util.Assert;
@@ -33,63 +32,58 @@ import org.springframework.util.ReflectionUtils;
import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.DIRECT;
/**
- * A parser that discovers annotations meta-annotated with {@link BeanOverride @BeanOverride}
- * on fields of a given class and creates {@link OverrideMetadata} accordingly.
+ * Internal parsing utilities to discover the presence of
+ * {@link BeanOverride @BeanOverride} on fields, and create the relevant
+ * {@link OverrideMetadata} accordingly.
*
* @author Simon Baslé
* @author Sam Brannen
* @since 6.2
*/
-class BeanOverrideParser {
-
- private final Set parsedMetadata = new LinkedHashSet<>();
-
+abstract class BeanOverrideParsingUtils {
/**
- * Get the set of {@link OverrideMetadata} once {@link #parse(Class)} has been called.
- */
- Set getOverrideMetadata() {
- return Collections.unmodifiableSet(this.parsedMetadata);
- }
-
- /**
- * Discover fields of the provided class that are meta-annotated with
- * {@link BeanOverride @BeanOverride}, then instantiate the corresponding
- * {@link BeanOverrideProcessor} and use it to create {@link OverrideMetadata}
- * for each field.
- * Each call to {@code parse} adds the parsed metadata to the parser's
- * override metadata {@link #getOverrideMetadata() set}.
- * @param testClass the test class in which to inspect fields
- */
- void parse(Class> testClass) {
- ReflectionUtils.doWithFields(testClass, field -> parseField(field, testClass));
- }
-
- /**
- * Check if any field of the provided {@code testClass} is meta-annotated
+ * Check if at least one field of the given {@code clazz} is meta-annotated
* with {@link BeanOverride @BeanOverride}.
- *
This is similar to the initial discovery of fields in {@link #parse(Class)}
- * without the heavier steps of instantiating processors and creating
- * {@link OverrideMetadata}. Consequently, this method leaves the current
- * state of the parser's override metadata {@link #getOverrideMetadata() set}
- * unchanged.
- * @param testClass the class which fields to inspect
- * @return true if there is a bean override annotation present, false otherwise
- * @see #parse(Class)
+ * @param clazz the class which fields to inspect
+ * @return {@code true} if there is a bean override annotation present,
+ * {@code false} otherwise
*/
- boolean hasBeanOverride(Class> testClass) {
+ static boolean hasBeanOverride(Class> clazz) {
AtomicBoolean hasBeanOverride = new AtomicBoolean();
- ReflectionUtils.doWithFields(testClass, field -> {
+ ReflectionUtils.doWithFields(clazz, field -> {
if (hasBeanOverride.get()) {
return;
}
boolean present = MergedAnnotations.from(field, DIRECT).isPresent(BeanOverride.class);
hasBeanOverride.compareAndSet(false, present);
});
- return hasBeanOverride.get();
+ if (hasBeanOverride.get()) {
+ return true;
+ }
+ return false;
}
- private void parseField(Field field, Class> source) {
+ /**
+ * Parse the specified classes for the presence of fields annotated with
+ * {@link BeanOverride @BeanOverride}, and create an {@link OverrideMetadata}
+ * for each.
+ * @param classes the classes to parse
+ */
+ static Set parse(Iterable> classes) {
+ Set result = new LinkedHashSet<>();
+ classes.forEach(c -> ReflectionUtils.doWithFields(c, field -> parseField(field, c, result)));
+ return result;
+ }
+
+ /**
+ * Convenience method to {@link #parse(Iterable) parse} a single test class.
+ */
+ static Set parse(Class> clazz) {
+ return parse(List.of(clazz));
+ }
+
+ private static void parseField(Field field, Class> testClass, Set metadataSet) {
AtomicBoolean overrideAnnotationFound = new AtomicBoolean();
MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> {
@@ -100,14 +94,11 @@ class BeanOverrideParser {
MergedAnnotation> metaSource = mergedAnnotation.getMetaSource();
Assert.state(metaSource != null, "Meta-annotation source must not be null");
Annotation composedAnnotation = metaSource.synthesize();
- ResolvableType typeToOverride = processor.getOrDeduceType(field, composedAnnotation, source);
Assert.state(overrideAnnotationFound.compareAndSet(false, true),
() -> "Multiple @BeanOverride annotations found on field: " + field);
- OverrideMetadata metadata = processor.createMetadata(field, composedAnnotation, typeToOverride);
- boolean isNewDefinition = this.parsedMetadata.add(metadata);
- Assert.state(isNewDefinition, () -> "Duplicate " + metadata.getBeanOverrideDescription() +
- " OverrideMetadata: " + metadata);
+ OverrideMetadata metadata = processor.createMetadata(composedAnnotation, testClass, field);
+ metadataSet.add(metadata);
});
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java
index 8546d405b2..afc3836826 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java
@@ -18,18 +18,14 @@ package org.springframework.test.context.bean.override;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
-import java.lang.reflect.TypeVariable;
-
-import org.springframework.beans.factory.config.BeanDefinition;
-import org.springframework.core.ResolvableType;
/**
- * Strategy interface for Bean Override processing.
+ * Strategy interface for Bean Override processing and creation of
+ * {@link OverrideMetadata}.
*
- * Processors are generally linked to one or more specific concrete annotations
- * (meta-annotated with {@link BeanOverride @BeanOverride}) and specify different
- * steps in the process of parsing these annotations, ultimately creating
- * {@link OverrideMetadata} which will be used to instantiate the overrides.
+ *
Processors are generally linked to one or more specific concrete
+ * annotations (meta-annotated with {@link BeanOverride @BeanOverride}) and
+ * concrete {@link OverrideMetadata} implementations.
*
*
Implementations are required to have a no-argument constructor and be
* stateless.
@@ -41,31 +37,13 @@ import org.springframework.core.ResolvableType;
public interface BeanOverrideProcessor {
/**
- * Determine the {@link ResolvableType} for which an {@link OverrideMetadata}
- * instance will be created — for example, by using the supplied annotation
- * to determine the type.
- *
The default implementation deduces the field's corresponding
- * {@link ResolvableType}, additionally tracking the source class if the
- * field's type is a {@link TypeVariable}.
- */
- default ResolvableType getOrDeduceType(Field field, Annotation annotation, Class> source) {
- return (field.getGenericType() instanceof TypeVariable ?
- ResolvableType.forField(field, source) : ResolvableType.forField(field));
- }
-
- /**
- * Create an {@link OverrideMetadata} instance for the given annotated field
- * and target {@link #getOrDeduceType(Field, Annotation, Class) type}.
- *
Specific implementations of metadata can have state to be used during
- * override {@linkplain OverrideMetadata#createOverride(String, BeanDefinition,
- * Object) instance creation} — for example, from further parsing of the
- * annotation or the annotated field.
- * @param field the annotated field
+ * Create an {@link OverrideMetadata} instance for the given annotated field.
* @param overrideAnnotation the field annotation
- * @param typeToOverride the target type
+ * @param testClass the test class being processed, which can be different
+ * from the {@code field.getDeclaringClass()} in case the field is inherited
+ * from a superclass
+ * @param field the annotated field
* @return a new {@link OverrideMetadata} instance
- * @see #getOrDeduceType(Field, Annotation, Class)
*/
- OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride);
-
+ OverrideMetadata createMetadata(Annotation overrideAnnotation, Class> testClass, Field field);
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistrar.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistrar.java
new file mode 100644
index 0000000000..eb98ffe27a
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistrar.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2002-2024 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.bean.override;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanCreationException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.ConfigurableBeanFactory;
+import org.springframework.beans.factory.config.ConstructorArgumentValues;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.RootBeanDefinition;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.ReflectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * An internal class used to track {@link OverrideMetadata}-related state after
+ * the bean factory has been processed and to provide field injection utilities
+ * for test execution listeners.
+ *
+ * @author Simon Baslé
+ * @since 6.2
+ */
+class BeanOverrideRegistrar implements BeanFactoryAware {
+
+ static final String INFRASTRUCTURE_BEAN_NAME = BeanOverrideRegistrar.class.getName();
+
+ private final Map beanNameRegistry;
+ private final Map earlyOverrideMetadata;
+ private final Set overrideMetadata;
+
+ @Nullable
+ private ConfigurableBeanFactory beanFactory;
+
+ /**
+ * Construct a new registrar and immediately parse the provided classes.
+ * @param classesToParse the initial set of classes that have been
+ * detected to contain bean overriding annotations, to be parsed immediately.
+ */
+ BeanOverrideRegistrar(Set> classesToParse) {
+ Set metadata = BeanOverrideParsingUtils.parse(classesToParse);
+ Assert.state(!metadata.isEmpty(), "Expected metadata to be produced by parser");
+ this.overrideMetadata = metadata;
+ this.beanNameRegistry = new HashMap<>();
+ this.earlyOverrideMetadata = new HashMap<>();
+ }
+
+ /**
+ * Return this processor's {@link OverrideMetadata} set.
+ */
+ Set getOverrideMetadata() {
+ return this.overrideMetadata;
+ }
+
+ @Override
+ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
+ Assert.isInstanceOf(ConfigurableBeanFactory.class, beanFactory,
+ "Bean OverrideRegistrar can only be used with a ConfigurableBeanFactory");
+ this.beanFactory = (ConfigurableBeanFactory) beanFactory;
+ }
+
+ /**
+ * Check {@link #markWrapEarly(OverrideMetadata, String) early override}
+ * records and use the {@link OverrideMetadata} to create an override
+ * instance from the provided bean, if relevant.
+ */
+ final Object wrapIfNecessary(Object bean, String beanName) throws BeansException {
+ final OverrideMetadata metadata = this.earlyOverrideMetadata.get(beanName);
+ if (metadata != null && metadata.getStrategy() == BeanOverrideStrategy.WRAP_BEAN) {
+ bean = metadata.createOverride(beanName, null, bean);
+ Assert.state(this.beanFactory != null, "ConfigurableBeanFactory must not be null");
+ metadata.track(bean, this.beanFactory);
+ }
+ return bean;
+ }
+
+ /**
+ * Register the provided {@link OverrideMetadata} and associated it with a
+ * {@code beanName}.
+ */
+ void registerNameForMetadata(OverrideMetadata metadata, String beanName) {
+ this.beanNameRegistry.put(metadata, beanName);
+ }
+
+ /**
+ * Mark the provided {@link OverrideMetadata} and {@code beanName} as "wrap
+ * early", allowing for later bean override using {@link #wrapIfNecessary(Object, String)}.
+ */
+ public void markWrapEarly(OverrideMetadata metadata, String beanName) {
+ this.earlyOverrideMetadata.put(beanName, metadata);
+ }
+
+ /**
+ * Register a bean definition for a {@link BeanOverrideRegistrar} if it does
+ * not yet exist. Additionally, each call adds the provided
+ * {@code detectedTestClasses} to the set that will be used as constructor
+ * argument.
+ * The resulting complete set of test classes will be parsed as soon as
+ * the {@link BeanOverrideRegistrar} is constructed.
+ * @param registry the bean definition registry
+ * @param detectedTestClasses a partial {@link Set} of {@link Class classes}
+ * that are expected to contain bean overriding annotations
+ */
+ public static void register(BeanDefinitionRegistry registry, @Nullable Set> detectedTestClasses) {
+ BeanDefinition definition;
+ if (!registry.containsBeanDefinition(BeanOverrideRegistrar.INFRASTRUCTURE_BEAN_NAME)) {
+ definition = new RootBeanDefinition(BeanOverrideRegistrar.class);
+ definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
+ ConstructorArgumentValues constructorArguments = definition.getConstructorArgumentValues();
+ constructorArguments.addIndexedArgumentValue(0, new LinkedHashSet>());
+ registry.registerBeanDefinition(INFRASTRUCTURE_BEAN_NAME, definition);
+ }
+ else {
+ definition = registry.getBeanDefinition(BeanOverrideRegistrar.INFRASTRUCTURE_BEAN_NAME);
+ }
+
+ ConstructorArgumentValues.ValueHolder constructorArg =
+ definition.getConstructorArgumentValues().getIndexedArgumentValue(0, Set.class);
+ @SuppressWarnings({"unchecked", "NullAway"})
+ Set> existing = (Set>) constructorArg.getValue();
+ if (detectedTestClasses != null && existing != null) {
+ existing.addAll(detectedTestClasses);
+ }
+ }
+
+ void inject(Object target, OverrideMetadata overrideMetadata) {
+ String beanName = this.beanNameRegistry.get(overrideMetadata);
+ Assert.state(StringUtils.hasLength(beanName),
+ () -> "No bean found for OverrideMetadata: " + overrideMetadata);
+ inject(overrideMetadata.getField(), target, beanName);
+ }
+
+ private void inject(Field field, Object target, String beanName) {
+ try {
+ ReflectionUtils.makeAccessible(field);
+ Object existingValue = ReflectionUtils.getField(field, target);
+ Assert.state(this.beanFactory != null, "beanFactory must not be null");
+ Object bean = this.beanFactory.getBean(beanName, field.getType());
+ if (existingValue == bean) {
+ return;
+ }
+ Assert.state(existingValue == null, () -> "The existing value '" + existingValue +
+ "' of field '" + field + "' is not the same as the new value '" + bean + "'");
+ ReflectionUtils.setField(field, target, bean);
+ }
+ catch (Throwable ex) {
+ throw new BeanCreationException("Could not inject field '" + field + "'", ex);
+ }
+ }
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideStrategy.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideStrategy.java
index 62e398a576..e1df52a993 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideStrategy.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideStrategy.java
@@ -17,8 +17,7 @@
package org.springframework.test.context.bean.override;
/**
- * Strategies for bean override instantiation, implemented in
- * {@link BeanOverrideBeanPostProcessor}.
+ * Strategies for bean override instantiation.
*
* @author Simon Baslé
* @since 6.2
@@ -39,10 +38,10 @@ public enum BeanOverrideStrategy {
REPLACE_OR_CREATE_DEFINITION,
/**
- * Intercept and wrap the actual bean instance upon creation, during the {@linkplain
- * org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference(Object, String)
- * early bean reference} phase.
+ * Intercept and process an early bean reference rather than a bean
+ * definition, allowing variants of bean overriding to wrap the instance
+ * (e.g. to delegate to actual methods in the context of a mocking "spy").
*/
- WRAP_EARLY_BEAN
+ WRAP_BEAN
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java
index 41f5068fef..44b28a08e3 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java
@@ -56,17 +56,17 @@ public class BeanOverrideTestExecutionListener extends AbstractTestExecutionList
}
/**
- * Using a registered {@link BeanOverrideBeanPostProcessor}, find metadata
+ * Using a registered {@link BeanOverrideBeanFactoryPostProcessor}, find metadata
* associated with the current test class and ensure fields are injected
* with the overridden bean instance.
*/
protected void injectFields(TestContext testContext) {
- postProcessFields(testContext, (testMetadata, postProcessor) -> postProcessor.inject(
- testMetadata.overrideMetadata.field(), testMetadata.testInstance, testMetadata.overrideMetadata));
+ postProcessFields(testContext, (testMetadata, overrideRegistrar) -> overrideRegistrar.inject(
+ testMetadata.testInstance, testMetadata.overrideMetadata));
}
/**
- * Using a registered {@link BeanOverrideBeanPostProcessor}, find metadata
+ * Using a registered {@link BeanOverrideBeanFactoryPostProcessor}, find metadata
* associated with the current test class and ensure fields are nulled out
* and then re-injected with the overridden bean instance.
* This method does nothing if the
@@ -79,31 +79,28 @@ public class BeanOverrideTestExecutionListener extends AbstractTestExecutionList
postProcessFields(testContext, (testMetadata, postProcessor) -> {
Object testInstance = testMetadata.testInstance;
- Field field = testMetadata.overrideMetadata.field();
+ Field field = testMetadata.overrideMetadata.getField();
ReflectionUtils.makeAccessible(field);
ReflectionUtils.setField(field, testInstance, null);
- postProcessor.inject(field, testInstance, testMetadata.overrideMetadata);
+ postProcessor.inject(testInstance, testMetadata.overrideMetadata);
});
}
}
private void postProcessFields(TestContext testContext, BiConsumer consumer) {
+ BeanOverrideRegistrar> consumer) {
Class> testClass = testContext.getTestClass();
Object testInstance = testContext.getTestInstance();
- BeanOverrideParser parser = new BeanOverrideParser();
- // Avoid full parsing, but validate that this particular class has some bean override field(s).
- if (parser.hasBeanOverride(testClass)) {
- BeanOverrideBeanPostProcessor postProcessor =
- testContext.getApplicationContext().getBean(BeanOverrideBeanPostProcessor.class);
- // The class should have already been parsed by the context customizer.
- for (OverrideMetadata metadata : postProcessor.getOverrideMetadata()) {
- if (!metadata.field().getDeclaringClass().equals(testClass)) {
+ if (BeanOverrideParsingUtils.hasBeanOverride(testClass)) {
+ BeanOverrideRegistrar registrar =
+ testContext.getApplicationContext().getBean(BeanOverrideRegistrar.class);
+ for (OverrideMetadata metadata : registrar.getOverrideMetadata()) {
+ if (!metadata.getField().getDeclaringClass().isAssignableFrom(testClass)) {
continue;
}
- consumer.accept(new TestContextOverrideMetadata(testInstance, metadata), postProcessor);
+ consumer.accept(new TestContextOverrideMetadata(testInstance, metadata), registrar);
}
}
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/OverrideMetadata.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/OverrideMetadata.java
index 94a5b664ec..cc9bdbcf85 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/OverrideMetadata.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/OverrideMetadata.java
@@ -16,7 +16,6 @@
package org.springframework.test.context.bean.override;
-import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Objects;
@@ -27,7 +26,16 @@ import org.springframework.core.style.ToStringCreator;
import org.springframework.lang.Nullable;
/**
- * Metadata for Bean Overrides.
+ * Metadata for Bean Override injection points, also responsible for the
+ * creation of the overriding instance.
+ *
+ * WARNING: implementations are used as a cache key and
+ * must implement proper {@code equals} and {@code hashCode}t methods.
+ *
+ *
Specific implementations of metadata can have state to be used during
+ * override {@linkplain #createOverride(String, BeanDefinition, Object)
+ * instance creation} — for example, from further parsing of the
+ * annotation or the annotated field.
*
* @author Simon Baslé
* @since 6.2
@@ -36,66 +44,45 @@ public abstract class OverrideMetadata {
private final Field field;
- private final Annotation overrideAnnotation;
-
- private final ResolvableType typeToOverride;
+ private final ResolvableType beanType;
private final BeanOverrideStrategy strategy;
- protected OverrideMetadata(Field field, Annotation overrideAnnotation,
- ResolvableType typeToOverride, BeanOverrideStrategy strategy) {
+ protected OverrideMetadata(Field field, ResolvableType beanType,
+ BeanOverrideStrategy strategy) {
this.field = field;
- this.overrideAnnotation = overrideAnnotation;
- this.typeToOverride = typeToOverride;
+ this.beanType = beanType;
this.strategy = strategy;
}
-
/**
- * Return a short, human-readable description of the kind of override this
- * instance handles.
+ * Return the bean name to override.
*/
- public abstract String getBeanOverrideDescription();
-
- /**
- * Return the expected bean name to override.
- *
Typically, this is either explicitly set in a concrete annotation or
- * inferred from the annotated field's name.
- * @return the expected bean name
- */
- protected String getExpectedBeanName() {
+ protected String getBeanName() {
return this.field.getName();
}
- /**
- * Return the annotated {@link Field}.
- */
- public Field field() {
- return this.field;
- }
-
- /**
- * Return the concrete override annotation, that is the one meta-annotated
- * with {@link BeanOverride @BeanOverride}.
- */
- public Annotation overrideAnnotation() {
- return this.overrideAnnotation;
- }
-
/**
* Return the bean {@link ResolvableType type} to override.
*/
- public ResolvableType typeToOverride() {
- return this.typeToOverride;
+ public ResolvableType getBeanType() {
+ return this.beanType;
+ }
+
+ /**
+ * Return the annotated {@link Field}.
+ */
+ public Field getField() {
+ return this.field;
}
/**
* Return the {@link BeanOverrideStrategy} for this instance, as a hint on
* how and when the override instance should be created.
*/
- public final BeanOverrideStrategy getBeanOverrideStrategy() {
+ public BeanOverrideStrategy getStrategy() {
return this.strategy;
}
@@ -133,25 +120,22 @@ public abstract class OverrideMetadata {
return false;
}
OverrideMetadata that = (OverrideMetadata) obj;
- return Objects.equals(this.field, that.field) &&
- Objects.equals(this.overrideAnnotation, that.overrideAnnotation) &&
- Objects.equals(this.strategy, that.strategy) &&
- Objects.equals(typeToOverride(), that.typeToOverride());
+ return Objects.equals(this.strategy, that.strategy) &&
+ Objects.equals(this.field, that.field) &&
+ Objects.equals(this.beanType, that.beanType);
}
@Override
public int hashCode() {
- return Objects.hash(this.field, this.overrideAnnotation, this.strategy, typeToOverride());
+ return Objects.hash(this.strategy, this.field, this.beanType);
}
@Override
public String toString() {
return new ToStringCreator(this)
- .append("category", getBeanOverrideDescription())
- .append("field", this.field)
- .append("overrideAnnotation", this.overrideAnnotation)
.append("strategy", this.strategy)
- .append("typeToOverride", typeToOverride())
+ .append("field", this.field)
+ .append("beanType", this.beanType)
.toString();
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java
index 4a712c60e9..e3fba8d0ad 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java
@@ -28,9 +28,11 @@ import org.springframework.test.context.bean.override.BeanOverride;
/**
* Mark a field to override a bean instance in the {@code BeanFactory}.
*
- *
The instance is created from a zero-argument static factory method in the test
- * class whose return type is compatible with the annotated field. The method
- * is deduced as follows.
+ *
The instance is created from a zero-argument static factory method in the
+ * test class whose return type is compatible with the annotated field. In the
+ * case of a nested test, any enclosing class it might have is also considered.
+ * Similarly, in case the test class inherits from a base class the whole class
+ * hierarchy is considered. The method is deduced as follows.
*
* - If the {@link #methodName()} is specified, look for a static method with
* that name.
@@ -121,6 +123,9 @@ public @interface TestBean {
/**
* Name of a static factory method to look for in the test class, which will
* be used to instantiate the bean to override.
+ * In the case of a nested test, any enclosing class it might have is
+ * also considered. Similarly, in case the test class inherits from a base
+ * class the whole class hierarchy is considered.
*
If left unspecified, the name of the factory method will be detected
* based on convention.
* @see #CONVENTION_SUFFIX
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java
index 011f8873e1..a02f879ec6 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java
@@ -24,11 +24,13 @@ import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
+import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.test.context.bean.override.BeanOverrideProcessor;
import org.springframework.test.context.bean.override.BeanOverrideStrategy;
import org.springframework.test.context.bean.override.OverrideMetadata;
@@ -37,126 +39,155 @@ import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
- * {@link BeanOverrideProcessor} implementation primarily made to work with
- * fields annotated with {@link TestBean @TestBean}, but can also work with
- * arbitrary test bean override annotations provided the annotated field's
- * declaring class declares an appropriate test bean factory method according
- * to the conventions documented in {@link TestBean}.
+ * {@link BeanOverrideProcessor} implementation for {@link TestBean @TestBean}
+ * support. It creates metadata for annotated fields in a given class and
+ * ensures that a corresponding static factory method exists, according to the
+ * {@linkplain TestBean documented conventions}.
*
* @author Simon Baslé
* @author Sam Brannen
* @since 6.2
*/
-public class TestBeanOverrideProcessor implements BeanOverrideProcessor {
+class TestBeanOverrideProcessor implements BeanOverrideProcessor {
/**
- * Find a test bean factory {@link Method} in the given {@link Class} which
- * meets the following criteria.
+ * Find a test bean factory {@link Method} in the given {@link Class} or one
+ * of its parent classes, which meets the following criteria.
*
* - The method is static.
* - The method does not accept any arguments.
* - The method's return type matches the supplied {@code methodReturnType}.
* - The method's name is one of the supplied {@code methodNames}.
*
+ * If the test class inherits from another class, the class hierarchy is
+ * searched for factory methods. Matching factory methods are prioritized
+ * from closest to furthest from the test class in the class hierarchy,
+ * provided they have the same name. However, if multiple methods are found
+ * that match distinct candidate names, an exception is thrown.
* @param clazz the class in which to search for the factory method
* @param methodReturnType the return type for the factory method
* @param methodNames a set of supported names for the factory method
* @return the corresponding factory method
- * @throws IllegalStateException if a single matching factory method cannot
- * be found
+ * @throws IllegalStateException if a matching factory method cannot
+ * be found or multiple methods have a match
*/
- public static Method findTestBeanFactoryMethod(Class> clazz, Class> methodReturnType, String... methodNames) {
+ static Method findTestBeanFactoryMethod(Class> clazz, Class> methodReturnType, String... methodNames) {
Assert.isTrue(methodNames.length > 0, "At least one candidate method name is required");
Set supportedNames = new LinkedHashSet<>(Arrays.asList(methodNames));
- List methods = Arrays.stream(clazz.getDeclaredMethods())
+ List methods = Arrays.stream(ReflectionUtils.getAllDeclaredMethods(clazz))
.filter(method -> Modifier.isStatic(method.getModifiers()) &&
supportedNames.contains(method.getName()) &&
methodReturnType.isAssignableFrom(method.getReturnType()))
.toList();
+ if (methods.isEmpty() && TestContextAnnotationUtils.searchEnclosingClass(clazz)) {
+ methods = Arrays.stream(ReflectionUtils.getAllDeclaredMethods(clazz.getEnclosingClass()))
+ .filter(method -> Modifier.isStatic(method.getModifiers()) &&
+ supportedNames.contains(method.getName()) &&
+ methodReturnType.isAssignableFrom(method.getReturnType()))
+ .toList();
+ }
+
Assert.state(!methods.isEmpty(), () -> """
Failed to find a static test bean factory method in %s with return type %s \
whose name matches one of the supported candidates %s""".formatted(
clazz.getName(), methodReturnType.getName(), supportedNames));
- Assert.state(methods.size() == 1, () -> """
+ long nameCount = methods.stream().map(Method::getName).distinct().count();
+ int methodCount = methods.size();
+ Assert.state(nameCount == 1, () -> """
Found %d competing static test bean factory methods in %s with return type %s \
whose name matches one of the supported candidates %s""".formatted(
- methods.size(), clazz.getName(), methodReturnType.getName(), supportedNames));
+ methodCount, clazz.getName(), methodReturnType.getName(), supportedNames));
return methods.get(0);
}
@Override
- public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride) {
- // If we can, get an explicit method name right away; fail fast if it doesn't match.
- if (overrideAnnotation instanceof TestBean testBeanAnnotation) {
- Method overrideMethod = null;
- String beanName = null;
- if (!testBeanAnnotation.methodName().isBlank()) {
- Class> declaringClass = field.getDeclaringClass();
- overrideMethod = findTestBeanFactoryMethod(declaringClass, field.getType(), testBeanAnnotation.methodName());
- }
- if (!testBeanAnnotation.name().isBlank()) {
- beanName = testBeanAnnotation.name();
- }
- return new MethodConventionOverrideMetadata(field, overrideMethod, beanName,
- overrideAnnotation, typeToOverride);
+ public TestBeanOverrideMetadata createMetadata(Annotation overrideAnnotation, Class> testClass, Field field) {
+ if (!(overrideAnnotation instanceof TestBean testBeanAnnotation)) {
+ throw new IllegalStateException(String.format("Invalid annotation passed to %s: expected @TestBean on field %s.%s",
+ TestBeanOverrideProcessor.class.getSimpleName(), field.getDeclaringClass().getName(),
+ field.getName()));
}
- // Otherwise defer the resolution of the static method until OverrideMetadata#createOverride.
- return new MethodConventionOverrideMetadata(field, null, null, overrideAnnotation, typeToOverride);
+ // If the user specified a method explicitly, search for that.
+ // Otherwise, search candidate factory methods using the convention suffix
+ // and the explicit bean name (if any) or field name.
+ Method explicitOverrideMethod;
+ if (!testBeanAnnotation.methodName().isBlank()) {
+ explicitOverrideMethod = findTestBeanFactoryMethod(testClass, field.getType(), testBeanAnnotation.methodName());
+ }
+ else {
+ String beanName = testBeanAnnotation.name();
+ if (!StringUtils.hasText(beanName)) {
+ explicitOverrideMethod = findTestBeanFactoryMethod(testClass, field.getType(),
+ field.getName() + TestBean.CONVENTION_SUFFIX);
+ }
+ else {
+ explicitOverrideMethod = findTestBeanFactoryMethod(testClass, field.getType(),
+ beanName + TestBean.CONVENTION_SUFFIX,
+ field.getName() + TestBean.CONVENTION_SUFFIX);
+ }
+ }
+
+ return new TestBeanOverrideMetadata(field, explicitOverrideMethod, testBeanAnnotation, ResolvableType.forField(field, testClass));
}
- static final class MethodConventionOverrideMetadata extends OverrideMetadata {
+ static final class TestBeanOverrideMetadata extends OverrideMetadata {
- @Nullable
private final Method overrideMethod;
- @Nullable
private final String beanName;
- public MethodConventionOverrideMetadata(Field field, @Nullable Method overrideMethod, @Nullable String beanName,
- Annotation overrideAnnotation, ResolvableType typeToOverride) {
+ public TestBeanOverrideMetadata(Field field, Method overrideMethod, TestBean overrideAnnotation,
+ ResolvableType typeToOverride) {
- super(field, overrideAnnotation, typeToOverride, BeanOverrideStrategy.REPLACE_DEFINITION);
+ super(field, typeToOverride, BeanOverrideStrategy.REPLACE_DEFINITION);
+ this.beanName = StringUtils.hasText(overrideAnnotation.name()) ?
+ overrideAnnotation.name() : field.getName();
this.overrideMethod = overrideMethod;
- this.beanName = beanName;
}
@Override
- protected String getExpectedBeanName() {
- if (StringUtils.hasText(this.beanName)) {
- return this.beanName;
- }
- return super.getExpectedBeanName();
- }
-
- @Override
- public String getBeanOverrideDescription() {
- return "@TestBean";
+ protected String getBeanName() {
+ return this.beanName;
}
@Override
protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition,
@Nullable Object existingBeanInstance) {
- Method methodToInvoke = this.overrideMethod;
- if (methodToInvoke == null) {
- methodToInvoke = findTestBeanFactoryMethod(field().getDeclaringClass(), field().getType(),
- beanName + TestBean.CONVENTION_SUFFIX,
- field().getName() + TestBean.CONVENTION_SUFFIX);
- }
-
try {
- ReflectionUtils.makeAccessible(methodToInvoke);
- return methodToInvoke.invoke(null);
+ ReflectionUtils.makeAccessible(this.overrideMethod);
+ return this.overrideMethod.invoke(null);
}
catch (IllegalAccessException | InvocationTargetException ex) {
- throw new IllegalArgumentException("Could not invoke bean overriding method " + methodToInvoke.getName() +
+ throw new IllegalArgumentException("Could not invoke bean overriding method " + this.overrideMethod.getName() +
"; a static method with no formal parameters is expected", ex);
}
}
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+ TestBeanOverrideMetadata that = (TestBeanOverrideMetadata) o;
+ return Objects.equals(this.overrideMethod, that.overrideMethod)
+ && Objects.equals(this.beanName, that.beanName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), this.overrideMethod, this.beanName);
+ }
}
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockDefinition.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanMetadata.java
similarity index 71%
rename from spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockDefinition.java
rename to spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanMetadata.java
index d77e50b269..8465889ba1 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockDefinition.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanMetadata.java
@@ -16,11 +16,11 @@
package org.springframework.test.context.bean.override.mockito;
-import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
+import java.util.Objects;
import java.util.Set;
import org.mockito.Answers;
@@ -44,7 +44,7 @@ import static org.mockito.Mockito.mock;
* @author Phillip Webb
* @since 6.2
*/
-class MockDefinition extends Definition {
+class MockitoBeanMetadata extends MockitoMetadata {
private final Set> extraInterfaces;
@@ -53,33 +53,27 @@ class MockDefinition extends Definition {
private final boolean serializable;
- MockDefinition(MockitoBean annotation, Field field, ResolvableType typeToMock) {
- this(annotation.name(), annotation.reset(), field, annotation, typeToMock,
+ MockitoBeanMetadata(MockitoBean annotation, Field field, ResolvableType typeToMock) {
+ this(annotation.name(), annotation.reset(), field, typeToMock,
annotation.extraInterfaces(), annotation.answers(), annotation.serializable());
}
- MockDefinition(String name, MockReset reset, Field field, Annotation annotation, ResolvableType typeToMock,
+ MockitoBeanMetadata(String name, MockReset reset, Field field, ResolvableType typeToMock,
Class>[] extraInterfaces, @Nullable Answers answer, boolean serializable) {
- super(name, reset, false, field, annotation, typeToMock, BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION);
+ super(name, reset, false, field, typeToMock, BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION);
Assert.notNull(typeToMock, "TypeToMock must not be null");
this.extraInterfaces = asClassSet(extraInterfaces);
this.answer = (answer != null) ? answer : Answers.RETURNS_DEFAULTS;
this.serializable = serializable;
}
-
- @Override
- public String getBeanOverrideDescription() {
- return "@MockitoBean";
- }
-
@Override
protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, @Nullable Object existingBeanInstance) {
return createMock(beanName);
}
- private Set> asClassSet(Class>[] classes) {
+ private Set> asClassSet(@Nullable Class>[] classes) {
Set> classSet = new LinkedHashSet<>();
if (classes != null) {
classSet.addAll(Arrays.asList(classes));
@@ -96,8 +90,8 @@ class MockDefinition extends Definition {
}
/**
- * Return the answers mode.
- * @return the answers mode; never {@code null}
+ * Return the {@link Answers}.
+ * @return the answers mode
*/
Answers getAnswer() {
return this.answer;
@@ -119,9 +113,8 @@ class MockDefinition extends Definition {
if (obj == null || obj.getClass() != getClass()) {
return false;
}
- MockDefinition other = (MockDefinition) obj;
+ MockitoBeanMetadata other = (MockitoBeanMetadata) obj;
boolean result = super.equals(obj);
- result = result && ObjectUtils.nullSafeEquals(typeToOverride(), other.typeToOverride());
result = result && ObjectUtils.nullSafeEquals(this.extraInterfaces, other.extraInterfaces);
result = result && ObjectUtils.nullSafeEquals(this.answer, other.answer);
result = result && this.serializable == other.serializable;
@@ -130,30 +123,21 @@ class MockDefinition extends Definition {
@Override
public int hashCode() {
- int result = super.hashCode();
- result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(typeToOverride());
- result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.extraInterfaces);
- result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.answer);
- result = MULTIPLIER * result + Boolean.hashCode(this.serializable);
- return result;
+ return Objects.hash(super.hashCode(), this.extraInterfaces, this.answer, this.serializable);
}
@Override
public String toString() {
return new ToStringCreator(this)
- .append("name", this.name)
- .append("typeToMock", typeToOverride())
- .append("extraInterfaces", this.extraInterfaces)
- .append("answer", this.answer)
- .append("serializable", this.serializable)
+ .append("beanName", getBeanName())
+ .append("fieldType", getBeanType())
+ .append("extraInterfaces", getExtraInterfaces())
+ .append("answer", getAnswer())
+ .append("serializable", isSerializable())
.append("reset", getReset())
.toString();
}
- T createMock() {
- return createMock(this.name);
- }
-
@SuppressWarnings("unchecked")
T createMock(String name) {
MockSettings settings = MockReset.withSettings(getReset());
@@ -167,7 +151,7 @@ class MockDefinition extends Definition {
if (this.serializable) {
settings.serializable();
}
- return (T) mock(typeToOverride().resolve(), settings);
+ return (T) mock(getBeanType().resolve(), settings);
}
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java
index b4781b2318..83a2a6c3cb 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java
@@ -21,7 +21,6 @@ import java.lang.reflect.Field;
import org.springframework.core.ResolvableType;
import org.springframework.test.context.bean.override.BeanOverrideProcessor;
-import org.springframework.test.context.bean.override.OverrideMetadata;
/**
* A {@link BeanOverrideProcessor} for mockito-related annotations
@@ -30,18 +29,19 @@ import org.springframework.test.context.bean.override.OverrideMetadata;
* @author Simon Baslé
* @since 6.2
*/
-public class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
+class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
@Override
- public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToMock) {
+ public MockitoMetadata createMetadata(Annotation overrideAnnotation, Class> testClass, Field field) {
if (overrideAnnotation instanceof MockitoBean mockBean) {
- return new MockDefinition(mockBean, field, typeToMock);
+ return new MockitoBeanMetadata(mockBean, field, ResolvableType.forField(field, testClass));
}
else if (overrideAnnotation instanceof MockitoSpyBean spyBean) {
- return new SpyDefinition(spyBean, field, typeToMock);
+ return new MockitoSpyBeanMetadata(spyBean, field, ResolvableType.forField(field, testClass));
}
- throw new IllegalArgumentException("Invalid annotation for MockitoBeanOverrideProcessor: " +
- overrideAnnotation.getClass().getName());
+ throw new IllegalStateException(String.format("Invalid annotation passed to MockitoBeanOverrideProcessor: "
+ + "expected @MockitoBean/@MockitoSpyBean on field %s.%s",
+ field.getDeclaringClass().getName(), field.getName()));
}
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/Definition.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoMetadata.java
similarity index 75%
rename from spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/Definition.java
rename to spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoMetadata.java
index b68d96c381..26311b6433 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/Definition.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoMetadata.java
@@ -16,8 +16,8 @@
package org.springframework.test.context.bean.override.mockito;
-import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
+import java.util.Objects;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.SingletonBeanRegistry;
@@ -29,15 +29,12 @@ import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
- * Base class for {@link MockDefinition} and {@link SpyDefinition}.
+ * Base class for {@link MockitoBeanMetadata} and {@link MockitoSpyBeanMetadata}.
*
* @author Phillip Webb
* @since 6.2
*/
-abstract class Definition extends OverrideMetadata {
-
- protected static final int MULTIPLIER = 31;
-
+abstract class MockitoMetadata extends OverrideMetadata {
protected final String name;
@@ -46,10 +43,10 @@ abstract class Definition extends OverrideMetadata {
private final boolean proxyTargetAware;
- Definition(String name, @Nullable MockReset reset, boolean proxyTargetAware, Field field,
- Annotation annotation, ResolvableType typeToOverride, BeanOverrideStrategy strategy) {
+ MockitoMetadata(String name, @Nullable MockReset reset, boolean proxyTargetAware, Field field,
+ ResolvableType typeToOverride, BeanOverrideStrategy strategy) {
- super(field, annotation, typeToOverride, strategy);
+ super(field, typeToOverride, strategy);
this.name = name;
this.reset = (reset != null) ? reset : MockReset.AFTER;
this.proxyTargetAware = proxyTargetAware;
@@ -57,11 +54,11 @@ abstract class Definition extends OverrideMetadata {
@Override
- protected String getExpectedBeanName() {
+ protected String getBeanName() {
if (StringUtils.hasText(this.name)) {
return this.name;
}
- return super.getExpectedBeanName();
+ return super.getBeanName();
}
@Override
@@ -104,8 +101,9 @@ abstract class Definition extends OverrideMetadata {
if (obj == null || !getClass().isAssignableFrom(obj.getClass())) {
return false;
}
- Definition other = (Definition) obj;
- boolean result = ObjectUtils.nullSafeEquals(this.name, other.name);
+ MockitoMetadata other = (MockitoMetadata) obj;
+ boolean result = super.equals(obj);
+ result = result && ObjectUtils.nullSafeEquals(this.name, other.name);
result = result && ObjectUtils.nullSafeEquals(this.reset, other.reset);
result = result && ObjectUtils.nullSafeEquals(this.proxyTargetAware, other.proxyTargetAware);
return result;
@@ -113,11 +111,7 @@ abstract class Definition extends OverrideMetadata {
@Override
public int hashCode() {
- int result = 1;
- result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.name);
- result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.reset);
- result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.proxyTargetAware);
- return result;
+ return Objects.hash(super.hashCode(), this.name, this.reset, this.proxyTargetAware);
}
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/SpyDefinition.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanMetadata.java
similarity index 77%
rename from spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/SpyDefinition.java
rename to spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanMetadata.java
index a832ff651c..78454cd809 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/SpyDefinition.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanMetadata.java
@@ -16,9 +16,9 @@
package org.springframework.test.context.bean.override.mockito;
-import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
+import java.util.Objects;
import org.mockito.AdditionalAnswers;
import org.mockito.MockSettings;
@@ -44,26 +44,20 @@ import static org.mockito.Mockito.mock;
* @author Phillip Webb
* @since 6.2
*/
-class SpyDefinition extends Definition {
+class MockitoSpyBeanMetadata extends MockitoMetadata {
- SpyDefinition(MockitoSpyBean spyAnnotation, Field field, ResolvableType typeToSpy) {
- this(spyAnnotation.name(), spyAnnotation.reset(), spyAnnotation.proxyTargetAware(), field,
- spyAnnotation, typeToSpy);
+ MockitoSpyBeanMetadata(MockitoSpyBean spyAnnotation, Field field, ResolvableType typeToSpy) {
+ this(spyAnnotation.name(), spyAnnotation.reset(), spyAnnotation.proxyTargetAware(),
+ field, typeToSpy);
}
- SpyDefinition(String name, MockReset reset, boolean proxyTargetAware, Field field, Annotation annotation,
- ResolvableType typeToSpy) {
+ MockitoSpyBeanMetadata(String name, MockReset reset, boolean proxyTargetAware, Field field, ResolvableType typeToSpy) {
- super(name, reset, proxyTargetAware, field, annotation, typeToSpy, BeanOverrideStrategy.WRAP_EARLY_BEAN);
+ super(name, reset, proxyTargetAware, field, typeToSpy, BeanOverrideStrategy.WRAP_BEAN);
Assert.notNull(typeToSpy, "typeToSpy must not be null");
}
- @Override
- public String getBeanOverrideDescription() {
- return "@MockitoSpyBean";
- }
-
@Override
protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition,
@Nullable Object existingBeanInstance) {
@@ -75,41 +69,35 @@ class SpyDefinition extends Definition {
@Override
public boolean equals(@Nullable Object obj) {
- // For SpyBean we want the class to be exactly the same.
if (obj == this) {
return true;
}
+ // For SpyBean we want the class to be exactly the same.
if (obj == null || obj.getClass() != getClass()) {
return false;
}
- SpyDefinition that = (SpyDefinition) obj;
- return (super.equals(obj) && ObjectUtils.nullSafeEquals(typeToOverride(), that.typeToOverride()));
+ MockitoSpyBeanMetadata that = (MockitoSpyBeanMetadata) obj;
+ return (super.equals(obj) && ObjectUtils.nullSafeEquals(getBeanType(), that.getBeanType()));
}
@Override
public int hashCode() {
- int result = super.hashCode();
- result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(typeToOverride());
- return result;
+ return Objects.hash(super.hashCode(), getBeanType());
}
@Override
public String toString() {
return new ToStringCreator(this)
- .append("name", this.name)
- .append("typeToSpy", typeToOverride())
+ .append("beanName", getBeanName())
+ .append("beanType", getBeanType())
.append("reset", getReset())
.toString();
}
- T createSpy(Object instance) {
- return createSpy(this.name, instance);
- }
-
@SuppressWarnings("unchecked")
T createSpy(String name, Object instance) {
Assert.notNull(instance, "Instance must not be null");
- Class> resolvedTypeToOverride = typeToOverride().resolve();
+ Class> resolvedTypeToOverride = getBeanType().resolve();
Assert.notNull(resolvedTypeToOverride, "Failed to resolve type to override");
Assert.isInstanceOf(resolvedTypeToOverride, instance);
if (Mockito.mockingDetails(instance).isSpy()) {
@@ -125,7 +113,7 @@ class SpyDefinition extends Definition {
Class> toSpy;
if (Proxy.isProxyClass(instance.getClass())) {
settings.defaultAnswer(AdditionalAnswers.delegatesTo(instance));
- toSpy = typeToOverride().toClass();
+ toSpy = getBeanType().toClass();
}
else {
settings.defaultAnswer(Mockito.CALLS_REAL_METHODS);
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessorTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java
similarity index 81%
rename from spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessorTests.java
rename to spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java
index 7e97cfd5ce..8754cc95d5 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessorTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java
@@ -17,6 +17,7 @@
package org.springframework.test.context.bean.override;
import java.util.Map;
+import java.util.Set;
import java.util.function.Predicate;
import org.junit.jupiter.api.Test;
@@ -46,20 +47,16 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatNoException;
/**
- * Tests for for {@link BeanOverrideBeanPostProcessor}.
+ * Tests for {@link BeanOverrideBeanFactoryPostProcessor} combined with a
+ * {@link BeanOverrideRegistrar}.
*
* @author Simon Baslé
*/
-class BeanOverrideBeanPostProcessorTests {
-
- private final BeanOverrideParser parser = new BeanOverrideParser();
-
+class BeanOverrideBeanFactoryPostProcessorTests {
@Test
void canReplaceExistingBeanDefinitions() {
- this.parser.parse(ReplaceBeans.class);
- AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
- BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
+ AnnotationConfigApplicationContext context = createContext(ReplaceBeans.class);
context.register(ReplaceBeans.class);
context.registerBean("explicit", ExampleService.class, () -> new RealExampleService("unexpected"));
context.registerBean("implicitName", ExampleService.class, () -> new RealExampleService("unexpected"));
@@ -72,22 +69,19 @@ class BeanOverrideBeanPostProcessorTests {
@Test
void cannotReplaceIfNoBeanMatching() {
- this.parser.parse(ReplaceBeans.class);
- AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
- BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
+ AnnotationConfigApplicationContext context = createContext(ReplaceBeans.class);
context.register(ReplaceBeans.class);
//note we don't register any original bean here
assertThatIllegalStateException()
.isThrownBy(context::refresh)
- .withMessage("Unable to override test bean; expected a bean definition to replace with name 'explicit'");
+ .withMessage("Unable to override bean 'explicit'; " +
+ "there is no bean definition to replace with that name");
}
@Test
void canReplaceExistingBeanDefinitionsWithCreateReplaceStrategy() {
- this.parser.parse(CreateIfOriginalIsMissingBean.class);
- AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
- BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
+ AnnotationConfigApplicationContext context = createContext(CreateIfOriginalIsMissingBean.class);
context.register(CreateIfOriginalIsMissingBean.class);
context.registerBean("explicit", ExampleService.class, () -> new RealExampleService("unexpected"));
context.registerBean("implicitName", ExampleService.class, () -> new RealExampleService("unexpected"));
@@ -100,9 +94,7 @@ class BeanOverrideBeanPostProcessorTests {
@Test
void canCreateIfOriginalMissingWithCreateReplaceStrategy() {
- this.parser.parse(CreateIfOriginalIsMissingBean.class);
- AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
- BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
+ AnnotationConfigApplicationContext context = createContext(CreateIfOriginalIsMissingBean.class);
context.register(CreateIfOriginalIsMissingBean.class);
//note we don't register original beans here
@@ -114,9 +106,7 @@ class BeanOverrideBeanPostProcessorTests {
@Test
void canOverrideBeanProducedByFactoryBeanWithClassObjectTypeAttribute() {
- this.parser.parse(OverriddenFactoryBean.class);
- AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
- BeanOverrideBeanPostProcessor.register(context, parser.getOverrideMetadata());
+ AnnotationConfigApplicationContext context = createContext(OverriddenFactoryBean.class);
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(TestFactoryBean.class);
factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, SomeInterface.class);
context.registerBeanDefinition("beanToBeOverridden", factoryBeanDefinition);
@@ -129,9 +119,7 @@ class BeanOverrideBeanPostProcessorTests {
@Test
void canOverrideBeanProducedByFactoryBeanWithResolvableTypeObjectTypeAttribute() {
- this.parser.parse(OverriddenFactoryBean.class);
- AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
- BeanOverrideBeanPostProcessor.register(context, parser.getOverrideMetadata());
+ AnnotationConfigApplicationContext context = createContext(OverriddenFactoryBean.class);
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(TestFactoryBean.class);
ResolvableType objectType = ResolvableType.forClass(SomeInterface.class);
factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, objectType);
@@ -145,10 +133,9 @@ class BeanOverrideBeanPostProcessorTests {
@Test
void postProcessorShouldNotTriggerEarlyInitialization() {
- this.parser.parse(EagerInitBean.class);
- AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
+ AnnotationConfigApplicationContext context = createContext(EagerInitBean.class);
+
context.register(FactoryBeanRegisteringPostProcessor.class);
- BeanOverrideBeanPostProcessor.register(context, parser.getOverrideMetadata());
context.register(EarlyBeanInitializationDetector.class);
context.register(EagerInitBean.class);
@@ -157,12 +144,10 @@ class BeanOverrideBeanPostProcessorTests {
@Test
void allowReplaceDefinitionWhenSingletonDefinitionPresent() {
- this.parser.parse(SingletonBean.class);
- AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
+ AnnotationConfigApplicationContext context = createContext(SingletonBean.class);
RootBeanDefinition definition = new RootBeanDefinition(String.class, () -> "ORIGINAL");
definition.setScope(BeanDefinition.SCOPE_SINGLETON);
context.registerBeanDefinition("singleton", definition);
- BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
context.register(SingletonBean.class);
assertThatNoException().isThrownBy(context::refresh);
@@ -172,14 +157,12 @@ class BeanOverrideBeanPostProcessorTests {
@Test
void copyDefinitionPrimaryAndScope() {
- this.parser.parse(SingletonBean.class);
- AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
+ AnnotationConfigApplicationContext context = createContext(SingletonBean.class);
context.getBeanFactory().registerScope("customScope", new SimpleThreadScope());
RootBeanDefinition definition = new RootBeanDefinition(String.class, () -> "ORIGINAL");
definition.setScope("customScope");
definition.setPrimary(true);
context.registerBeanDefinition("singleton", definition);
- BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata());
context.register(SingletonBean.class);
assertThatNoException().isThrownBy(context::refresh);
@@ -192,6 +175,14 @@ class BeanOverrideBeanPostProcessorTests {
}
+ private AnnotationConfigApplicationContext createContext(Class>... classes) {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
+ BeanOverrideRegistrar.register(context, Set.of(classes));
+ BeanOverrideBeanFactoryPostProcessor.register(context);
+ return context;
+ }
+
+
/*
Classes to parse and register with the bean post processor
-----
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideParserTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtilsTests.java
similarity index 69%
rename from spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideParserTests.java
rename to spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtilsTests.java
index 099d7d7cf3..ef134d292f 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideParserTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtilsTests.java
@@ -28,33 +28,27 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
/**
- * Unit tests for {@link BeanOverrideParser}.
+ * Unit tests for {@link BeanOverrideParsingUtils}.
*
* @since 6.2
*/
-class BeanOverrideParserTests {
-
- // Copy of ExampleBeanOverrideProcessor.DUPLICATE_TRIGGER which is package-private.
- private static final String DUPLICATE_TRIGGER = "DUPLICATE";
-
- private final BeanOverrideParser parser = new BeanOverrideParser();
+class BeanOverrideParsingUtilsTests {
+ // Metadata built from a String that starts with DUPLICATE_TRIGGER are considered equal
+ private static final String DUPLICATE_TRIGGER1 = ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER + "-v1";
+ private static final String DUPLICATE_TRIGGER2 = ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER + "-v2";
@Test
void findsOnField() {
- parser.parse(SingleAnnotationOnField.class);
-
- assertThat(parser.getOverrideMetadata())
- .map(om -> ((ExampleBeanOverrideAnnotation) om.overrideAnnotation()).value())
+ assertThat(BeanOverrideParsingUtils.parse(SingleAnnotationOnField.class))
+ .map(Object::toString)
.containsExactly("onField");
}
@Test
void allowsMultipleProcessorsOnDifferentElements() {
- parser.parse(AnnotationsOnMultipleFields.class);
-
- assertThat(parser.getOverrideMetadata())
- .map(om -> ((ExampleBeanOverrideAnnotation) om.overrideAnnotation()).value())
+ assertThat(BeanOverrideParsingUtils.parse(AnnotationsOnMultipleFields.class))
+ .map(Object::toString)
.containsExactlyInAnyOrder("onField1", "onField2");
}
@@ -62,15 +56,15 @@ class BeanOverrideParserTests {
void rejectsMultipleAnnotationsOnSameElement() {
Field field = ReflectionUtils.findField(MultipleAnnotationsOnField.class, "message");
assertThatRuntimeException()
- .isThrownBy(() -> parser.parse(MultipleAnnotationsOnField.class))
+ .isThrownBy(() -> BeanOverrideParsingUtils.parse(MultipleAnnotationsOnField.class))
.withMessage("Multiple @BeanOverride annotations found on field: " + field);
}
@Test
- void detectsDuplicateMetadata() {
- assertThatRuntimeException()
- .isThrownBy(() -> parser.parse(DuplicateConf.class))
- .withMessage("Duplicate test OverrideMetadata: {DUPLICATE_TRIGGER}");
+ void keepsFirstOccurrenceOfEqualMetadata() {
+ assertThat(BeanOverrideParsingUtils.parse(DuplicateConf.class))
+ .map(Object::toString)
+ .containsExactly("{DUPLICATE-v1}");
}
@@ -114,10 +108,10 @@ class BeanOverrideParserTests {
static class DuplicateConf {
- @ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER)
+ @ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER1)
String message1;
- @ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER)
+ @ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER2)
String message2;
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java
index a9538133ec..24d834df9c 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java
@@ -16,7 +16,6 @@
package org.springframework.test.context.bean.override;
-import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import org.junit.jupiter.api.Test;
@@ -39,7 +38,7 @@ class OverrideMetadataTests {
@Test
void implicitConfigurations() throws Exception {
OverrideMetadata metadata = exampleOverride();
- assertThat(metadata.getExpectedBeanName()).as("expectedBeanName").isEqualTo(metadata.field().getName());
+ assertThat(metadata.getBeanName()).as("expectedBeanName").isEqualTo(metadata.getField().getName());
}
@@ -48,21 +47,16 @@ class OverrideMetadataTests {
private static OverrideMetadata exampleOverride() throws Exception {
Field field = OverrideMetadataTests.class.getDeclaredField("annotated");
- return new ConcreteOverrideMetadata(field, field.getAnnotation(NonNull.class),
- ResolvableType.forClass(String.class), BeanOverrideStrategy.REPLACE_DEFINITION);
+ return new ConcreteOverrideMetadata(field, ResolvableType.forClass(String.class),
+ BeanOverrideStrategy.REPLACE_DEFINITION);
}
static class ConcreteOverrideMetadata extends OverrideMetadata {
- ConcreteOverrideMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride,
+ ConcreteOverrideMetadata(Field field, ResolvableType typeToOverride,
BeanOverrideStrategy strategy) {
- super(field, overrideAnnotation, typeToOverride, strategy);
- }
-
- @Override
- public String getBeanOverrideDescription() {
- return ConcreteOverrideMetadata.class.getSimpleName();
+ super(field, typeToOverride, strategy);
}
@Override
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/AbstractTestBeanIntegrationTestCase.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/AbstractTestBeanIntegrationTestCase.java
new file mode 100644
index 0000000000..11d5a423c6
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/AbstractTestBeanIntegrationTestCase.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2002-2024 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.bean.override.convention;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+@SpringJUnitConfig
+class AbstractTestBeanIntegrationTestCase {
+
+ @TestBean
+ Pojo someBean;
+
+ @TestBean
+ Pojo otherBean;
+
+ @TestBean(name = "thirdBean")
+ Pojo anotherBean;
+
+ static Pojo otherBeanTestOverride() {
+ return new FakePojo("otherBean in superclass");
+ }
+
+ static Pojo thirdBeanTestOverride() {
+ return new FakePojo("third in superclass");
+ }
+
+ static Pojo commonBeanOverride() {
+ return new FakePojo("in superclass");
+ }
+
+ interface Pojo {
+
+ default String getValue() {
+ return "Prod";
+ }
+ }
+
+ static class ProdPojo implements Pojo { }
+
+ static class FakePojo implements Pojo {
+ final String value;
+
+ protected FakePojo(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return this.value;
+ }
+
+ @Override
+ public String toString() {
+ return getValue();
+ }
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ Pojo someBean() {
+ return new ProdPojo();
+ }
+ @Bean
+ Pojo otherBean() {
+ return new ProdPojo();
+ }
+ @Bean
+ Pojo thirdBean() {
+ return new ProdPojo();
+ }
+ @Bean
+ Pojo pojo() {
+ return new ProdPojo();
+ }
+ @Bean
+ Pojo pojo2() {
+ return new ProdPojo();
+ }
+ }
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanInheritanceIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanInheritanceIntegrationTests.java
new file mode 100644
index 0000000000..4ca465da74
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanInheritanceIntegrationTests.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2002-2024 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.bean.override.convention;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.engine.TestExecutionResult;
+import org.junit.platform.testkit.engine.EngineExecutionResults;
+import org.junit.platform.testkit.engine.EngineTestKit;
+
+import org.springframework.context.ApplicationContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.InstanceOfAssertFactories.THROWABLE;
+import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
+
+public class TestBeanInheritanceIntegrationTests {
+
+ static AbstractTestBeanIntegrationTestCase.Pojo nestedBeanOverride() {
+ return new AbstractTestBeanIntegrationTestCase.FakePojo("in enclosing test class");
+ }
+
+ @Nested
+ @DisplayName("Concrete inherited test with correct @TestBean setup")
+ class ConcreteTestBeanIntegrationTests extends AbstractTestBeanIntegrationTestCase {
+
+ @TestBean(methodName = "commonBeanOverride")
+ Pojo pojo;
+
+ @TestBean(methodName = "nestedBeanOverride")
+ Pojo pojo2;
+
+ static Pojo someBeanTestOverride() {
+ return new FakePojo("someBeanOverride");
+ }
+
+ @Test
+ void fieldInSupertypeMethodInType(ApplicationContext ctx) {
+ assertThat(ctx.getBean("someBean")).as("applicationContext").hasToString("someBeanOverride");
+ assertThat(this.someBean.getValue()).as("injection point").isEqualTo("someBeanOverride");
+ }
+
+ @Test
+ void fieldInTypeMethodInSuperType(ApplicationContext ctx) {
+ assertThat(ctx.getBean("pojo")).as("applicationContext").hasToString("in superclass");
+ assertThat(this.pojo.getValue()).as("injection point").isEqualTo("in superclass");
+ }
+
+ @Test
+ void fieldInTypeMethodInEnclosingClass(ApplicationContext ctx) {
+ assertThat(ctx.getBean("pojo2")).as("applicationContext").hasToString("in enclosing test class");
+ assertThat(this.pojo2.getValue()).as("injection point").isEqualTo("in enclosing test class");
+ }
+
+ @Test
+ void fieldInSupertypePrioritizeMethodInType(ApplicationContext ctx) {
+ assertThat(ctx.getBean("someBean")).as("applicationContext").hasToString("someBeanOverride");
+ assertThat(this.someBean.getValue()).as("injection point").isEqualTo("someBeanOverride");
+ }
+ }
+
+
+ @Test
+ void failsIfFieldInSupertypeButNoMethod() {
+ Class> clazz = Failing1.class;
+ EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
+ .selectors(selectClass(clazz))//
+ .execute();
+
+ assertThat(results.allEvents().failed().stream()).hasSize(1).first()
+ .satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
+ .getThrowable()).get(THROWABLE)
+ .rootCause().isInstanceOf(IllegalStateException.class)
+ .hasMessage("""
+ Failed to find a static test bean factory method in %s with return type %s \
+ whose name matches one of the supported candidates [someBeanTestOverride]""",
+ clazz.getName(), AbstractTestBeanIntegrationTestCase.Pojo.class.getName()));
+ }
+
+ @Test
+ void failsIfMethod1InSupertypeAndMethod2InType() {
+ Class> clazz = Failing2.class;
+ EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
+ .selectors(selectClass(clazz))
+ .execute();
+
+ assertThat(results.allEvents().failed().stream()).hasSize(1).first()
+ .satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
+ .getThrowable()).get(THROWABLE)
+ .rootCause().isInstanceOf(IllegalStateException.class)
+ .hasMessage("""
+ Found 2 competing static test bean factory methods in %s with return type %s \
+ whose name matches one of the supported candidates \
+ [thirdBeanTestOverride, anotherBeanTestOverride]""",
+ clazz.getName(), AbstractTestBeanIntegrationTestCase.Pojo.class.getName()));
+ }
+
+ static class Failing1 extends AbstractTestBeanIntegrationTestCase {
+
+ @Test
+ void ignored() {
+ }
+ }
+
+ static class Failing2 extends AbstractTestBeanIntegrationTestCase {
+
+ static Pojo someBeanTestOverride() {
+ return new FakePojo("ignored");
+ }
+
+ static Pojo anotherBeanTestOverride() {
+ return new FakePojo("sub2");
+ }
+
+ @Test
+ void ignored2() { }
+ }
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanIntegrationTests.java
new file mode 100644
index 0000000000..babb08d332
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanIntegrationTests.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2002-2024 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.bean.override.convention;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.engine.TestExecutionResult;
+import org.junit.platform.testkit.engine.EngineExecutionResults;
+import org.junit.platform.testkit.engine.EngineTestKit;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.assertj.core.api.InstanceOfAssertFactories.THROWABLE;
+import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
+
+@SpringJUnitConfig
+public class TestBeanIntegrationTests {
+
+ @TestBean
+ String field;
+
+ @TestBean
+ String nestedField;
+
+ @TestBean(name = "field")
+ String renamed1;
+
+ @TestBean(name = "nestedField")
+ String renamed2;
+
+ @TestBean(methodName = "fieldTestOverride")
+ String methodRenamed1;
+
+ @TestBean(methodName = "nestedFieldTestOverride")
+ String methodRenamed2;
+
+ static String fieldTestOverride() {
+ return "fieldOverride";
+ }
+
+ static String nestedFieldTestOverride() {
+ return "nestedFieldOverride";
+ }
+
+ @Test
+ void fieldHasOverride(ApplicationContext ctx) {
+ assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride");
+ assertThat(this.field).as("injection point").isEqualTo("fieldOverride");
+ }
+
+ @Test
+ void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
+ assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride");
+ assertThat(this.renamed1).as("injection point").isEqualTo("fieldOverride");
+ }
+
+ @Test
+ void fieldWithMethodNameHasOverride(ApplicationContext ctx) {
+ assertThat(ctx.getBean("methodRenamed1")).as("applicationContext").isEqualTo("fieldOverride");
+ assertThat(this.methodRenamed1).as("injection point").isEqualTo("fieldOverride");
+ }
+
+ @Test
+ void testBeanFailingNoFieldNameBean() {
+ EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
+ .selectors(selectClass(Failing1.class))//
+ .execute();
+
+ assertThat(results.allEvents().failed().stream()).hasSize(1).first()
+ .satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
+ .getThrowable()).get(THROWABLE)
+ .cause()
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Unable to override bean 'noOriginalBean'; " +
+ "there is no bean definition to replace with that name"));
+ }
+
+ @Test
+ void testBeanFailingNoExplicitNameBean() {
+ EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
+ .selectors(selectClass(Failing2.class))//
+ .execute();
+
+ assertThat(results.allEvents().failed().stream()).hasSize(1).first()
+ .satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
+ .getThrowable()).get(THROWABLE)
+ .cause()
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Unable to override bean 'notPresent'; " +
+ "there is no bean definition to replace with that name"));
+ }
+
+ @Test
+ void testBeanFailingNoImplicitMethod() {
+ EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
+ .selectors(selectClass(Failing3.class))//
+ .execute();
+
+ assertThat(results.allEvents().failed().stream()).hasSize(1).first()
+ .satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
+ .getThrowable()).get(THROWABLE)
+ .rootCause().isInstanceOf(IllegalStateException.class)
+ .hasMessage("Failed to find a static test bean factory method in " +
+ "org.springframework.test.context.bean.override.convention.TestBeanIntegrationTests$Failing3 " +
+ "with return type java.lang.String whose name matches one of the " +
+ "supported candidates [notPresent]"));
+ }
+
+ @Test
+ void testBeanFailingNoExplicitMethod() {
+ EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
+ .selectors(selectClass(Failing4.class))//
+ .execute();
+
+ assertThat(results.allEvents().failed().stream()).hasSize(1).first()
+ .satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
+ .getThrowable()).get(THROWABLE)
+ .rootCause().isInstanceOf(IllegalStateException.class)
+ .hasMessage("Failed to find a static test bean factory method in " +
+ "org.springframework.test.context.bean.override.convention.TestBeanIntegrationTests$Failing4 " +
+ "with return type java.lang.String whose name matches one of the " +
+ "supported candidates [fieldTestOverride]"));
+ }
+
+ @Nested
+ @DisplayName("With @TestBean on enclosing class")
+ class TestBeanNested {
+
+ @Test
+ void fieldHasOverride(ApplicationContext ctx) {
+ assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride");
+ assertThat(TestBeanIntegrationTests.this.nestedField).isEqualTo("nestedFieldOverride");
+ }
+
+ @Test
+ void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
+ assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride");
+ assertThat(TestBeanIntegrationTests.this.renamed2).isEqualTo("nestedFieldOverride");
+ }
+
+ @Test
+ void fieldWithMethodNameHasOverride(ApplicationContext ctx) {
+ assertThat(ctx.getBean("methodRenamed2")).as("applicationContext").isEqualTo("nestedFieldOverride");
+ assertThat(TestBeanIntegrationTests.this.methodRenamed2).isEqualTo("nestedFieldOverride");
+ }
+
+ }
+
+ @Nested
+ @DisplayName("With factory method on enclosing class")
+ class TestBeanNested2 {
+
+ @TestBean(methodName = "nestedFieldTestOverride", name = "nestedField")
+ String nestedField2;
+
+ @Test
+ void fieldHasOverride(ApplicationContext ctx) {
+ assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride");
+ assertThat(this.nestedField2).isEqualTo("nestedFieldOverride");
+ }
+
+ }
+
+
+ @Configuration
+ static class Config {
+
+ @Bean("field")
+ String bean1() {
+ return "prod";
+ }
+
+ @Bean("nestedField")
+ String bean2() {
+ return "nestedProd";
+ }
+
+ @Bean("methodRenamed1")
+ String bean3() {
+ return "Prod";
+ }
+
+ @Bean("methodRenamed2")
+ String bean4() {
+ return "NestedProd";
+ }
+ }
+
+ @SpringJUnitConfig
+ static class Failing1 {
+
+ @TestBean
+ String noOriginalBean;
+
+ @Test
+ void ignored() {
+ fail("should fail earlier");
+ }
+
+ static String noOriginalBeanTestOverride() {
+ return "should be ignored";
+ }
+
+ }
+
+ @SpringJUnitConfig
+ static class Failing2 {
+
+ @TestBean(name = "notPresent")
+ String field;
+
+ @Test
+ void ignored() {
+ fail("should fail earlier");
+ }
+
+ static String notPresentTestOverride() {
+ return "should be ignored";
+ }
+ }
+
+ @SpringJUnitConfig
+ static class Failing3 {
+
+ @TestBean(methodName = "notPresent")
+ String field;
+
+ @Test
+ void ignored() {
+ fail("should fail earlier");
+ }
+ }
+
+ @SpringJUnitConfig
+ static class Failing4 {
+
+ @TestBean //expects fieldTestOverride method
+ String field;
+
+ @Test
+ void ignored() {
+ fail("should fail earlier");
+ }
+ }
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java
index b18ac5ea44..d9d2bb1c79 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java
@@ -23,8 +23,9 @@ import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
-import org.springframework.core.ResolvableType;
-import org.springframework.test.context.bean.override.convention.TestBeanOverrideProcessor.MethodConventionOverrideMetadata;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.lang.NonNull;
+import org.springframework.test.context.bean.override.convention.TestBeanOverrideProcessor.TestBeanOverrideMetadata;
import org.springframework.test.context.bean.override.example.ExampleService;
import org.springframework.test.context.bean.override.example.FailingExampleService;
@@ -95,7 +96,7 @@ class TestBeanOverrideProcessorTests {
TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor();
assertThatIllegalStateException()
- .isThrownBy(() -> processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(returnType)))
+ .isThrownBy(() -> processor.createMetadata(overrideAnnotation, clazz, field))
.withMessage("""
Failed to find a static test bean factory method in %s with return type %s \
whose name matches one of the supported candidates %s""",
@@ -104,35 +105,49 @@ class TestBeanOverrideProcessorTests {
@Test
void createMetaDataForKnownExplicitMethod() throws Exception {
- Class> returnType = ExampleService.class;
- Field field = ExplicitMethodNameConf.class.getField("b");
+ Class> clazz = ExplicitMethodNameConf.class;
+ Field field = clazz.getField("b");
TestBean overrideAnnotation = field.getAnnotation(TestBean.class);
assertThat(overrideAnnotation).isNotNull();
TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor();
- assertThat(processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(returnType)))
- .isInstanceOf(MethodConventionOverrideMetadata.class);
+ assertThat(processor.createMetadata(overrideAnnotation, clazz, field))
+ .isInstanceOf(TestBeanOverrideMetadata.class);
}
@Test
- void createMetaDataWithDeferredCheckForExistenceOfConventionBasedFactoryMethod() throws Exception {
+ void createMetaDataForConventionBasedFactoryMethod() throws Exception {
Class> returnType = ExampleService.class;
- Field field = MethodConventionConf.class.getField("field");
+ Class> clazz = MethodConventionConf.class;
+ Field field = clazz.getField("field");
TestBean overrideAnnotation = field.getAnnotation(TestBean.class);
assertThat(overrideAnnotation).isNotNull();
TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor();
- // When in convention-based mode, createMetadata() will not verify that
- // the factory method actually exists. So, we don't expect an exception
- // for this use case.
- assertThat(processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(returnType)))
- .isInstanceOf(MethodConventionOverrideMetadata.class);
+ assertThatIllegalStateException().isThrownBy(() -> processor.createMetadata(
+ overrideAnnotation, clazz, field))
+ .withMessage("""
+ Failed to find a static test bean factory method in %s with return type %s \
+ whose name matches one of the supported candidates %s""",
+ clazz.getName(), returnType.getName(), List.of("someFieldTestOverride", "fieldTestOverride"));
+ }
+
+ @Test
+ void failToCreateMetadataForOtherAnnotation() throws NoSuchFieldException {
+ Class> clazz = MethodConventionConf.class;
+ Field field = clazz.getField("field");
+ NonNull badAnnotation = AnnotationUtils.synthesizeAnnotation(NonNull.class);
+
+ TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor();
+ assertThatIllegalStateException().isThrownBy(() -> processor.createMetadata(badAnnotation, clazz, field))
+ .withMessage("Invalid annotation passed to TestBeanOverrideProcessor: expected @TestBean" +
+ " on field %s.%s", field.getDeclaringClass().getName(), field.getName());
}
static class MethodConventionConf {
- @TestBean
+ @TestBean(name = "someField")
public ExampleService field;
@Bean
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideAnnotation.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideAnnotation.java
index bf9fec1cf0..81e61b8870 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideAnnotation.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideAnnotation.java
@@ -29,6 +29,8 @@ import org.springframework.test.context.bean.override.BeanOverride;
public @interface ExampleBeanOverrideAnnotation {
String DEFAULT_VALUE = "TEST OVERRIDE";
+ // Any metadata using this as the prefix for the bean name will be considered equal
+ String DUPLICATE_TRIGGER = "DUPLICATE";
String value() default DEFAULT_VALUE;
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideProcessor.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideProcessor.java
index 294da24f98..6ecbe2f1bc 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideProcessor.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideProcessor.java
@@ -26,24 +26,15 @@ import org.springframework.test.context.bean.override.OverrideMetadata;
// Intentionally NOT public
class ExampleBeanOverrideProcessor implements BeanOverrideProcessor {
- static final String DUPLICATE_TRIGGER = "DUPLICATE";
-
- private static final TestOverrideMetadata CONSTANT = new TestOverrideMetadata() {
- @Override
- public String toString() {
- return "{DUPLICATE_TRIGGER}";
- }
- };
-
@Override
- public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride) {
+ public OverrideMetadata createMetadata(Annotation overrideAnnotation, Class> testClass, Field field) {
if (!(overrideAnnotation instanceof ExampleBeanOverrideAnnotation annotation)) {
throw new IllegalStateException("unexpected annotation");
}
- if (annotation.value().equals(DUPLICATE_TRIGGER)) {
- return CONSTANT;
+ if (annotation.value().startsWith(ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER)) {
+ return new TestOverrideMetadata(annotation.value());
}
- return new TestOverrideMetadata(field, annotation, typeToOverride);
+ return new TestOverrideMetadata(field, annotation, ResolvableType.forField(field, testClass));
}
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestOverrideMetadata.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestOverrideMetadata.java
index 542217877c..a89398f15f 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestOverrideMetadata.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestOverrideMetadata.java
@@ -40,6 +40,8 @@ class TestOverrideMetadata extends OverrideMetadata {
@Nullable
private final String beanName;
+ private final String methodName;
+
@Nullable
private static Method findMethod(AnnotatedElement element, String methodName) {
if (DEFAULT_VALUE.equals(methodName)) {
@@ -77,30 +79,31 @@ class TestOverrideMetadata extends OverrideMetadata {
}
public TestOverrideMetadata(Field field, ExampleBeanOverrideAnnotation overrideAnnotation, ResolvableType typeToOverride) {
- super(field, overrideAnnotation, typeToOverride, overrideAnnotation.createIfMissing() ?
+ super(field, typeToOverride, overrideAnnotation.createIfMissing() ?
BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION: BeanOverrideStrategy.REPLACE_DEFINITION);
this.method = findMethod(field, overrideAnnotation.value());
+ this.methodName = overrideAnnotation.value();
this.beanName = overrideAnnotation.beanName();
}
//Used to trigger duplicate detection in parser test
- TestOverrideMetadata() {
- super(null, null, null, null);
+ TestOverrideMetadata(String duplicateTrigger) {
+ super(null, null, null);
this.method = null;
- this.beanName = null;
+ this.methodName = duplicateTrigger;
+ this.beanName = duplicateTrigger;
}
@Override
- protected String getExpectedBeanName() {
+ protected String getBeanName() {
if (StringUtils.hasText(this.beanName)) {
return this.beanName;
}
- return super.getExpectedBeanName();
+ return super.getBeanName();
}
- @Override
- public String getBeanOverrideDescription() {
- return "test";
+ String getAnnotationMethodName() {
+ return this.methodName;
}
@Override
@@ -117,4 +120,29 @@ class TestOverrideMetadata extends OverrideMetadata {
}
}
+ @Override
+ public boolean equals(Object obj) {
+ if (this.method == null) {
+ return obj instanceof TestOverrideMetadata tem &&
+ tem.beanName != null &&
+ tem.beanName.startsWith(ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER);
+ }
+ return super.equals(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ if (this.method == null) {
+ return ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER.hashCode();
+ }
+ return super.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ if (this.method == null) {
+ return "{" + this.beanName + "}";
+ }
+ return this.methodName;
+ }
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanIntegrationTests.java
new file mode 100644
index 0000000000..be351838dc
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanIntegrationTests.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2002-2024 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.bean.override.mockito;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.RealExampleService;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringJUnitConfig
+public class MockitoBeanIntegrationTests {
+
+ @MockitoBean
+ ExampleService field;
+
+ @MockitoBean
+ ExampleService nestedField;
+
+ @MockitoBean(name = "field")
+ ExampleService renamed1;
+
+ @MockitoBean(name = "nestedField")
+ ExampleService renamed2;
+
+ @MockitoBean(name = "nonExistingBean")
+ ExampleService nonExisting1;
+
+ @MockitoBean(name = "nestedNonExistingBean")
+ ExampleService nonExisting2;
+
+ @Test
+ void fieldHasOverride(ApplicationContext ctx) {
+ assertThat(ctx.getBean("field"))
+ .isInstanceOf(ExampleService.class)
+ .satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
+ .as("isMock").isTrue())
+ .isSameAs(this.field);
+
+ assertThat(this.field.greeting()).as("mocked greeting").isNull();
+ }
+
+ @Test
+ void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
+ assertThat(ctx.getBean("field"))
+ .isInstanceOf(ExampleService.class)
+ .satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
+ .as("isMock").isTrue())
+ .isSameAs(this.renamed1);
+
+ assertThat(this.renamed1.greeting()).as("mocked greeting").isNull();
+ }
+
+ @Test
+ void fieldIsMockedWhenNoOriginalBean(ApplicationContext ctx) {
+ assertThat(ctx.getBean("nonExistingBean"))
+ .isInstanceOf(ExampleService.class)
+ .satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
+ .as("isMock").isTrue())
+ .isSameAs(this.nonExisting1);
+
+ assertThat(this.nonExisting1.greeting()).as("mocked greeting").isNull();
+ }
+
+ @Nested
+ @DisplayName("With @MockitoBean on enclosing class")
+ class MockitoBeanNested {
+
+ @Test
+ void fieldHasOverride(ApplicationContext ctx) {
+ assertThat(ctx.getBean("nestedField"))
+ .isInstanceOf(ExampleService.class)
+ .satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
+ .as("isMock").isTrue())
+ .isSameAs(MockitoBeanIntegrationTests.this.nestedField);
+ }
+
+ @Test
+ void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
+ assertThat(ctx.getBean("nestedField"))
+ .isInstanceOf(ExampleService.class)
+ .satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
+ .as("isMock").isTrue())
+ .isSameAs(MockitoBeanIntegrationTests.this.renamed2);
+ }
+
+ @Test
+ void fieldIsMockedWhenNoOriginalBean(ApplicationContext ctx) {
+ assertThat(ctx.getBean("nestedNonExistingBean"))
+ .isInstanceOf(ExampleService.class)
+ .satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock())
+ .as("isMock").isTrue())
+ .isSameAs(MockitoBeanIntegrationTests.this.nonExisting2);
+ }
+ }
+
+
+ @Configuration
+ static class Config {
+ @Bean("field")
+ ExampleService bean1() {
+ return new RealExampleService("Hello Field");
+ }
+ @Bean("nestedField")
+ ExampleService bean2() {
+ return new RealExampleService("Hello Nested Field");
+ }
+ }
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessorTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessorTests.java
new file mode 100644
index 0000000000..6fe90daaf4
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessorTests.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2002-2024 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.bean.override.mockito;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.lang.Nullable;
+import org.springframework.test.context.bean.override.OverrideMetadata;
+import org.springframework.test.context.bean.override.example.ExampleService;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+
+public class MockitoBeanOverrideProcessorTests {
+
+ @Test
+ void mockAnnotationCreatesMockMetadata() throws NoSuchFieldException {
+ MockitoBeanOverrideProcessor processor = new MockitoBeanOverrideProcessor();
+ MockitoBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoBean.class);
+ Class> clazz = MockitoConf.class;
+ Field field = clazz.getField("a");
+
+ OverrideMetadata object = processor.createMetadata(annotation, clazz, field);
+
+ assertThat(object).isExactlyInstanceOf(MockitoBeanMetadata.class);
+ }
+
+ @Test
+ void spyAnnotationCreatesSpyMetadata() throws NoSuchFieldException {
+ MockitoBeanOverrideProcessor processor = new MockitoBeanOverrideProcessor();
+ MockitoSpyBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoSpyBean.class);
+ Class> clazz = MockitoConf.class;
+ Field field = clazz.getField("a");
+
+ OverrideMetadata object = processor.createMetadata(annotation, clazz, field);
+
+ assertThat(object).isExactlyInstanceOf(MockitoSpyBeanMetadata.class);
+ }
+
+ @Test
+ void otherAnnotationThrows() throws NoSuchFieldException {
+ MockitoBeanOverrideProcessor processor = new MockitoBeanOverrideProcessor();
+ Class> clazz = MockitoConf.class;
+ Field field = clazz.getField("a");
+ Annotation annotation = field.getAnnotation(Nullable.class);
+
+ assertThatIllegalStateException().isThrownBy(() -> processor.createMetadata(annotation, clazz, field))
+ .withMessage("Invalid annotation passed to MockitoBeanOverrideProcessor: expected " +
+ "@MockitoBean/@MockitoSpyBean on field %s.%s", field.getDeclaringClass().getName(),
+ field.getName());
+ }
+
+ static class MockitoConf {
+ @Nullable
+ @MockitoBean
+ @MockitoSpyBean
+ public ExampleService a;
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanIntegrationTests.java
new file mode 100644
index 0000000000..9cb01c3ffa
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanIntegrationTests.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2002-2024 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.bean.override.mockito;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.engine.TestExecutionResult;
+import org.junit.platform.testkit.engine.EngineExecutionResults;
+import org.junit.platform.testkit.engine.EngineTestKit;
+import org.mockito.Mockito;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.InstanceOfAssertFactories.THROWABLE;
+import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
+
+@SpringJUnitConfig(MockitoBeanIntegrationTests.Config.class)
+public class MockitoSpyBeanIntegrationTests {
+
+ @MockitoSpyBean
+ ExampleService field;
+
+ @MockitoSpyBean
+ ExampleService nestedField;
+
+ @MockitoSpyBean(name = "field")
+ ExampleService renamed1;
+
+ @MockitoSpyBean(name = "nestedField")
+ ExampleService renamed2;
+
+ @Test
+ void fieldHasOverride(ApplicationContext ctx) {
+ assertThat(ctx.getBean("field"))
+ .isInstanceOf(ExampleService.class)
+ .satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy())
+ .as("isSpy").isTrue())
+ .isSameAs(this.field);
+
+ assertThat(this.field.greeting()).as("spied greeting")
+ .isEqualTo("Hello Field");
+
+ }
+
+ @Test
+ void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
+ assertThat(ctx.getBean("field"))
+ .isInstanceOf(ExampleService.class)
+ .satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy())
+ .as("isSpy").isTrue())
+ .isSameAs(this.renamed1);
+
+ assertThat(this.field.greeting()).as("spied greeting")
+ .isEqualTo("Hello Field");
+ }
+
+ @Test
+ void failWhenBeanNotPresentFieldName() {
+ EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
+ .selectors(selectClass(Failure1.class))//
+ .execute();
+
+ assertThat(results.allEvents().failed().stream()).hasSize(1).first()
+ .satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
+ .getThrowable()).get(THROWABLE)
+ .cause()
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Unable to override bean 'notPresent' by wrapping," +
+ " no existing bean instance by this name of type %s",
+ ExampleService.class.getName()));
+ }
+
+ @Test
+ void failWhenBeanNotPresentExplicitName() {
+ EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
+ .selectors(selectClass(Failure2.class))//
+ .execute();
+
+ assertThat(results.allEvents().failed().stream()).hasSize(1).first()
+ .satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class)
+ .getThrowable()).get(THROWABLE)
+ .cause()
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Unable to override bean 'notPresentAtAll' by wrapping," +
+ " no existing bean instance by this name of type %s",
+ ExampleService.class.getName()));
+ }
+
+ @Nested
+ @DisplayName("With @MockitoSpyBean on enclosing class")
+ class MockitoBeanNested {
+
+ @Test
+ void fieldHasOverride(ApplicationContext ctx) {
+ assertThat(ctx.getBean("nestedField"))
+ .isInstanceOf(ExampleService.class)
+ .satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy())
+ .as("isSpy").isTrue())
+ .isSameAs(MockitoSpyBeanIntegrationTests.this.nestedField);
+
+ assertThat(MockitoSpyBeanIntegrationTests.this.nestedField.greeting())
+ .as("spied greeting")
+ .isEqualTo("Hello Nested Field");
+ }
+
+ @Test
+ void fieldWithBeanNameHasOverride(ApplicationContext ctx) {
+ assertThat(ctx.getBean("nestedField"))
+ .isInstanceOf(ExampleService.class)
+ .satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy())
+ .as("isSpy").isTrue())
+ .isSameAs(MockitoSpyBeanIntegrationTests.this.renamed2);
+
+
+ assertThat(MockitoSpyBeanIntegrationTests.this.renamed2.greeting())
+ .as("spied greeting")
+ .isEqualTo("Hello Nested Field");
+ }
+ }
+
+
+ @SpringJUnitConfig
+ static class Failure1 {
+
+ @MockitoSpyBean
+ ExampleService notPresent;
+
+ @Test
+ void ignored() { }
+
+ }
+
+ @SpringJUnitConfig
+ static class Failure2 {
+
+ @MockitoSpyBean(name = "notPresentAtAll")
+ ExampleService field;
+
+ @Test
+ void ignored() { }
+ }
+}