From f9523a785b456e0167133489233b3ff8b786a67e Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 22 Mar 2026 18:29:02 +0100 Subject: [PATCH] =?UTF-8?q?Support=20@=E2=81=A0MockitoBean=20and=20@?= =?UTF-8?q?=E2=81=A0MockitoSpyBean=20on=20test=20constructor=20parameters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prior to this commit, @MockitoBean and @MockitoSpyBean could be declared on fields or at the type level (on test classes and test interfaces), but not on constructor parameters. Consequently, a test class could not use constructor injection for bean overrides. To address that, this commit introduces support for @MockitoBean and @MockitoSpyBean on constructor parameters in JUnit Jupiter test classes. Specifically, the Bean Override infrastructure has been overhauled to support constructor parameters as declaration sites and injection points alongside fields, and the SpringExtension now recognizes composed @BeanOverride annotations on constructor parameters in supportsParameter() and resolves them properly in resolveParameter(). Note, however, that this support has not been introduced for @TestBean. For example, the following which uses field injection: @SpringJUnitConfig(TestConfig.class) class BeanOverrideTests { @MockitoBean CustomService customService; // tests... } Can now be rewritten to use constructor injection: @SpringJUnitConfig(TestConfig.class) class BeanOverrideTests { private final CustomService customService; BeanOverrideTests(@MockitoBean CustomService customService) { this.customService = customService; } // tests... } With Kotlin this can be achieved even more succinctly via a compact constructor declaration: @SpringJUnitConfig(TestConfig::class) class BeanOverrideTests(@MockitoBean val customService: CustomService) { // tests... } Of course, if one is a fan of so-called "test records", that can also be achieved succinctly with a Java record: @SpringJUnitConfig(TestConfig.class) record BeanOverrideTests(@MockitoBean CustomService customService) { // tests... } Closes gh-36096 --- .../annotation-mockitobean.adoc | 164 +++++++++++++- .../bean-overriding.adoc | 11 +- .../support-classes.adoc | 4 + .../context/bean/override/BeanOverride.java | 6 +- .../BeanOverrideBeanFactoryPostProcessor.java | 82 ++++--- .../bean/override/BeanOverrideHandler.java | 104 +++++++-- .../bean/override/BeanOverrideProcessor.java | 31 +++ .../bean/override/BeanOverrideUtils.java | 123 +++++++++- .../AbstractMockitoBeanOverrideHandler.java | 10 + .../bean/override/mockito/MockitoBean.java | 26 ++- .../mockito/MockitoBeanOverrideHandler.java | 20 +- .../mockito/MockitoBeanOverrideProcessor.java | 21 ++ .../bean/override/mockito/MockitoSpyBean.java | 30 +-- .../MockitoSpyBeanOverrideHandler.java | 9 +- .../junit/jupiter/SpringExtension.java | 26 ++- .../override/example/CustomQualifier.java | 2 +- .../MockitoBeanConfigurationErrorTests.java | 70 ++++++ .../MockitoBeanOverrideHandlerTests.java | 2 +- .../MockitoBeanOverrideProcessorTests.java | 76 +++++++ ...MockitoSpyBeanConfigurationErrorTests.java | 55 +++++ ...ConstructorParametersIntegrationTests.java | 175 ++++++++++++++ ...uctorParametersIntegrationRecordTests.java | 55 +++++ ...ConstructorParametersIntegrationTests.java | 207 +++++++++++++++++ ...ConstructorParametersIntegrationTests.java | 165 ++++++++++++++ ...ConstructorParametersIntegrationTests.java | 213 ++++++++++++++++++ .../typelevel/ConstructorService01.java | 20 ++ .../MockitoBeansByNameIntegrationTests.java | 29 +++ .../MockitoBeansByTypeIntegrationTests.java | 12 + .../mockito/typelevel/MockitoBeansTests.java | 6 +- ...rametersIntegrationKotlinDataClassTests.kt | 44 ++++ ...tructorParametersIntegrationKotlinTests.kt | 44 ++++ 31 files changed, 1730 insertions(+), 112 deletions(-) create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoBeanByNameLookupForConstructorParametersIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoBeanByTypeLookupForConstructorParametersIntegrationRecordTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoBeanByTypeLookupForConstructorParametersIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoSpyBeanByNameLookupForConstructorParametersIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoSpyBeanByTypeLookupForConstructorParametersIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/ConstructorService01.java create mode 100644 spring-test/src/test/kotlin/org/springframework/test/context/bean/override/mockito/MockitoBeanByTypeLookupForConstructorParametersIntegrationKotlinDataClassTests.kt create mode 100644 spring-test/src/test/kotlin/org/springframework/test/context/bean/override/mockito/MockitoBeanByTypeLookupForConstructorParametersIntegrationKotlinTests.kt diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc index b0ad7707ff3..591d3a50c35 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc @@ -12,18 +12,20 @@ The annotations can be applied in the following ways. * On a non-static field in a test class or any of its superclasses. * On a non-static field in an enclosing class for a `@Nested` test class or in any class in the type hierarchy or enclosing class hierarchy above the `@Nested` test class. +* On a parameter in the constructor for a test class. * At the type level on a test class or any superclass or implemented interface in the type hierarchy above the test class. * At the type level on an enclosing class for a `@Nested` test class or on any class or interface in the type hierarchy or enclosing class hierarchy above the `@Nested` test class. -When `@MockitoBean` or `@MockitoSpyBean` is declared on a field, the bean to mock or spy -is inferred from the type of the annotated field. If multiple candidates exist in the -`ApplicationContext`, a `@Qualifier` annotation can be declared on the field to help -disambiguate. In the absence of a `@Qualifier` annotation, the name of the annotated -field will be used as a _fallback qualifier_. Alternatively, you can explicitly specify a -bean name to mock or spy by setting the `value` or `name` attribute in the annotation. +When `@MockitoBean` or `@MockitoSpyBean` is declared on a field or constructor parameter, +the bean to mock or spy is inferred from the type of the annotated field or parameter. If +multiple candidates exist in the `ApplicationContext`, a `@Qualifier` annotation can be +declared on the field or parameter to help disambiguate. In the absence of a `@Qualifier` +annotation, the name of the annotated field or parameter will be used as a _fallback +qualifier_. Alternatively, you can explicitly specify a bean name to mock or spy by +setting the `value` or `name` attribute in the annotation. When `@MockitoBean` or `@MockitoSpyBean` is declared at the type level, the type of bean (or beans) to mock or spy must be supplied via the `types` attribute in the annotation – @@ -201,6 +203,82 @@ Kotlin:: <1> Replace the bean named `service` with a Mockito mock. ====== +The following example shows how to use `@MockitoBean` on a constructor parameter for a +by-type lookup. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig.class) + class BeanOverrideTests { + + private final CustomService customService; + + BeanOverrideTests(@MockitoBean CustomService customService) { // <1> + this.customService = customService; + } + + // tests... + } +---- +<1> Replace the bean with type `CustomService` with a Mockito mock and inject it into + the constructor. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig::class) + class BeanOverrideTests(@MockitoBean val customService: CustomService) { // <1> + + // tests... + } +---- +<1> Replace the bean with type `CustomService` with a Mockito mock and inject it into + the constructor. +====== + +The following example shows how to use `@MockitoBean` on a constructor parameter for a +by-name lookup. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig.class) + class BeanOverrideTests { + + private final CustomService customService; + + BeanOverrideTests(@MockitoBean("service") CustomService customService) { // <1> + this.customService = customService; + } + + // tests... + } +---- +<1> Replace the bean named `service` with a Mockito mock and inject it into the + constructor. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig::class) + class BeanOverrideTests(@MockitoBean("service") val customService: CustomService) { // <1> + + // tests... + } +---- +<1> Replace the bean named `service` with a Mockito mock and inject it into the + constructor. +====== + The following `@SharedMocks` annotation registers two mocks by-type and one mock by-name. [tabs] @@ -375,6 +453,80 @@ Kotlin:: <1> Wrap the bean named `service` with a Mockito spy. ====== +The following example shows how to use `@MockitoSpyBean` on a constructor parameter for +a by-type lookup. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig.class) + class BeanOverrideTests { + + private final CustomService customService; + + BeanOverrideTests(@MockitoSpyBean CustomService customService) { // <1> + this.customService = customService; + } + + // tests... + } +---- +<1> Wrap the bean with type `CustomService` with a Mockito spy and inject it into the + constructor. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig::class) + class BeanOverrideTests(@MockitoSpyBean val customService: CustomService) { // <1> + + // tests... + } +---- +<1> Wrap the bean with type `CustomService` with a Mockito spy and inject it into the + constructor. +====== + +The following example shows how to use `@MockitoSpyBean` on a constructor parameter for +a by-name lookup. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig.class) + class BeanOverrideTests { + + private final CustomService customService; + + BeanOverrideTests(@MockitoSpyBean("service") CustomService customService) { // <1> + this.customService = customService; + } + + // tests... + } +---- +<1> Wrap the bean named `service` with a Mockito spy and inject it into the constructor. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig::class) + class BeanOverrideTests(@MockitoSpyBean("service") val customService: CustomService) { // <1> + + // tests... + } +---- +<1> Wrap the bean named `service` with a Mockito spy and inject it into the constructor. +====== + The following `@SharedSpies` annotation registers two spies by-type and one spy by-name. [tabs] diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc index 055b718feaa..7fbb0ceeb0c 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc @@ -2,8 +2,9 @@ = Bean Overriding in Tests Bean overriding in tests refers to the ability to override specific beans in the -`ApplicationContext` for a test class, by annotating the test class or one or more -non-static fields in the test class. +`ApplicationContext` for a test class, by annotating the test class, one or more +non-static fields in the test class, or one or more parameters in the constructor for the +test class. NOTE: This feature is intended as a less risky alternative to the practice of registering a bean via `@Bean` with the `DefaultListableBeanFactory` @@ -42,9 +43,9 @@ The `spring-test` module registers implementations of the latter two {spring-framework-code}/spring-test/src/main/resources/META-INF/spring.factories[`META-INF/spring.factories` properties file]. -The bean overriding infrastructure searches for annotations on test classes as well as -annotations on non-static fields in test classes that are meta-annotated with -`@BeanOverride` and instantiates the corresponding `BeanOverrideProcessor` which is +The bean overriding infrastructure searches for annotations on test classes, non-static +fields in test classes, and parameters in test class constructors that are meta-annotated +with `@BeanOverride`, and instantiates the corresponding `BeanOverrideProcessor` which is responsible for creating an appropriate `BeanOverrideHandler`. The internal `BeanOverrideBeanFactoryPostProcessor` then uses bean override handlers to diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc index 5f77d9e08b4..a98f0f72f9c 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc @@ -179,6 +179,10 @@ If a specific parameter in a constructor for a JUnit Jupiter test class is of ty `ApplicationContext` (or a sub-type thereof) or is annotated or meta-annotated with `@Autowired`, `@Qualifier`, or `@Value`, Spring injects the value for that specific parameter with the corresponding bean or value from the test's `ApplicationContext`. +Similarly, if a specific parameter is annotated with `@MockitoBean` or `@MockitoSpyBean`, +Spring will inject a Mockito mock or spy, respectively — see +xref:testing/annotations/integration-spring/annotation-mockitobean.adoc[`@MockitoBean` and `@MockitoSpyBean`] +for details. Spring can also be configured to autowire all arguments for a test class constructor if the constructor is considered to be _autowirable_. A constructor is considered to be 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 f34c46028e0..2bc94bdbecc 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,8 +34,10 @@ import org.springframework.aot.hint.annotation.Reflective; * fields, it is expected that the composed annotation is meta-annotated with * {@link Target @Target(ElementType.FIELD)}. However, certain bean override * annotations may be declared with an additional {@code ElementType.TYPE} target - * for use at the type level, as is the case for {@code @MockitoBean} which can - * be declared on a field, test class, or test interface. + * for use at the type level. Similarly, as of Spring Framework 7.1, certain bean + * override annotations may be declared with an additional {@code ElementType.PARAMETER} + * target for use on constructor parameters. For example, {@code @MockitoBean} can + * be declared on a field, constructor parameter, test class, or test interface. * *
For concrete examples of such composed annotations, see * {@link org.springframework.test.context.bean.override.convention.TestBean @TestBean}, 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 index 2a609b9ac6b..0a74fde6da3 100644 --- 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 @@ -16,7 +16,7 @@ package org.springframework.test.context.bean.override; -import java.lang.reflect.Field; +import java.lang.reflect.AnnotatedElement; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashSet; @@ -54,7 +54,9 @@ import org.springframework.util.Assert; * {@linkplain BeanOverrideStrategy override strategy}. The bean override instance * is created, if necessary, and the related infrastructure is updated to allow * the bean override instance to be injected into the corresponding - * {@linkplain BeanOverrideHandler#getField() field} of the test class. + * {@linkplain BeanOverrideHandler#getField() field} of the test class or + * {@linkplain BeanOverrideHandler#getParameter() parameter} of the test class + * constructor. * *
This processor does not work against a particular test class but rather
* only prepares the bean factory for the identified, unique set of bean overrides.
@@ -113,7 +115,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
Assert.state(!BeanFactoryUtils.isFactoryDereference(beanName), () -> """
Unable to override bean '%s'%s: a FactoryBean cannot be overridden. \
To override the bean created by the FactoryBean, remove the '&' prefix."""
- .formatted(beanName, forField(handler.getField())));
+ .formatted(beanName, forDescription(handler)));
switch (handler.getStrategy()) {
case REPLACE -> replaceOrCreateBean(beanFactory, handler, generatedBeanNames, true);
@@ -175,12 +177,11 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
setQualifiedElement(existingBeanDefinition, handler);
}
else if (requireExistingBean) {
- Field field = handler.getField();
throw new IllegalStateException("""
Unable to replace bean: there is no bean with name '%s' and type %s%s. \
If the bean is defined in a @Bean method, make sure the return type is the \
most specific type possible (for example, the concrete implementation type)."""
- .formatted(beanName, handler.getBeanType(), requiredByField(field)));
+ .formatted(beanName, handler.getBeanType(), requiredByDescription(handler)));
}
// 4) We are creating a bean by-name with the provided beanName.
}
@@ -253,13 +254,12 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
*/
private void wrapBean(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler) {
String beanName = handler.getBeanName();
- Field field = handler.getField();
ResolvableType beanType = handler.getBeanType();
if (beanName == null) {
// We are wrapping an existing bean by-type.
Set Honors both primary and fallback semantics, and
- * otherwise matches against the field name as a fallback qualifier.
+ * otherwise matches against the field name or parameter name as a fallback
+ * qualifier.
* @return the name of the unique candidate, or {@code null} if none found
* @since 6.2.3
* @see org.springframework.beans.factory.support.DefaultListableBeanFactory#determineAutowireCandidate
*/
private static @Nullable String determineUniqueCandidate(ConfigurableListableBeanFactory beanFactory,
- Set The returned bean definition should not be used to create
* a bean instance but rather only for the purpose of having suitable bean
* definition metadata available in the {@code BeanFactory} — for example,
@@ -462,15 +462,16 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
/**
* Set the {@linkplain RootBeanDefinition#setQualifiedElement(java.lang.reflect.AnnotatedElement)
* qualified element} in the supplied {@link BeanDefinition} to the
- * {@linkplain BeanOverrideHandler#getField() field} of the supplied
+ * {@linkplain BeanOverrideHandler#getField() field} or
+ * {@linkplain BeanOverrideHandler#getParameter() parameter} of the supplied
* {@code BeanOverrideHandler}.
* This is necessary for proper autowiring candidate resolution.
* @since 6.2.6
*/
private static void setQualifiedElement(BeanDefinition beanDefinition, BeanOverrideHandler handler) {
- Field field = handler.getField();
- if (field != null && beanDefinition instanceof RootBeanDefinition rbd) {
- rbd.setQualifiedElement(field);
+ AnnotatedElement fieldOrParameter = handler.fieldOrParameter();
+ if (fieldOrParameter != null && beanDefinition instanceof RootBeanDefinition rbd) {
+ rbd.setQualifiedElement(fieldOrParameter);
}
}
@@ -500,19 +501,14 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
dlbf.destroySingleton(beanName);
}
- private static String forField(@Nullable Field field) {
- if (field == null) {
- return "";
- }
- return " for field '%s.%s'".formatted(field.getDeclaringClass().getSimpleName(), field.getName());
+ private static String forDescription(BeanOverrideHandler handler) {
+ String description = handler.fieldOrParameterDescription();
+ return (description != null ? " for " + description : "");
}
- private static String requiredByField(@Nullable Field field) {
- if (field == null) {
- return "";
- }
- return " (as required by field '%s.%s')".formatted(
- field.getDeclaringClass().getSimpleName(), field.getName());
+ private static String requiredByDescription(BeanOverrideHandler handler) {
+ String description = handler.fieldOrParameterDescription();
+ return (description != null ? " (as required by %s)".formatted(description) : "");
}
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java
index 13b3fc5a9c2..87f37de67d9 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java
@@ -17,7 +17,9 @@
package org.springframework.test.context.bean.override;
import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
+import java.lang.reflect.Parameter;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -27,10 +29,13 @@ import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.config.SingletonBeanRegistry;
+import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.style.ToStringCreator;
+import org.springframework.util.Assert;
/**
* Handler for Bean Override injection points that is responsible for creating
@@ -49,7 +54,7 @@ import org.springframework.core.style.ToStringCreator;
* Concrete implementations of {@code BeanOverrideHandler} can store additional
* metadata to use during override {@linkplain #createOverrideInstance instance
* creation} — for example, based on further processing of the annotation,
- * the annotated field, or the annotated class.
+ * the annotated field, the annotated parameter, or the annotated class.
*
* When replacing a bean created by a
* {@link org.springframework.beans.factory.FactoryBean FactoryBean}, the
* {@code FactoryBean} itself will be replaced with a singleton bean corresponding
- * to bean override instance created by the handler.
+ * to the bean override instance created by the handler.
*
* When wrapping a bean created by a
* {@link org.springframework.beans.factory.FactoryBean FactoryBean}, the object
@@ -79,6 +84,8 @@ public abstract class BeanOverrideHandler {
private final @Nullable Field field;
+ private final @Nullable Parameter parameter;
+
private final Set This method will only be invoked when a {@link BeanOverride @BeanOverride}
+ * annotation is declared on a constructor parameter — for example, if
+ * the supplied constructor parameter is annotated with {@code @MockitoBean}.
+ * The default implementation returns {@code null}, signaling that this
+ * {@code BeanOverrideProcessor} does not support {@code @BeanOverride}
+ * declarations on constructor parameters. Can be overridden by concrete
+ * implementations to support constructor parameter use cases.
+ * @param overrideAnnotation the composed annotation that declares the
+ * {@code @BeanOverride} annotation which registers this processor
+ * @param testClass the test class to process
+ * @param parameter the annotated constructor parameter
+ * @return the {@code BeanOverrideHandler} that should handle the given constructor
+ * parameter
+ * @since 7.1
+ * @see #createHandler(Annotation, Class, Field)
+ * @see #createHandlers(Annotation, Class)
+ */
+ default @Nullable BeanOverrideHandler createHandler(Annotation overrideAnnotation,
+ Class> testClass, Parameter parameter) {
+
+ return null;
+ }
+
/**
* Create a list of {@link BeanOverrideHandler} instances for the given override
* annotation and test class.
@@ -75,6 +105,7 @@ public interface BeanOverrideProcessor {
* @return the list of {@code BeanOverrideHandlers} for the annotated class
* @since 6.2.2
* @see #createHandler(Annotation, Class, Field)
+ * @see #createHandler(Annotation, Class, Parameter)
*/
default List This method does not search the enclosing class hierarchy and does not
- * search for {@code @BeanOverride} declarations on classes or interfaces.
+ * This method does not search the enclosing class hierarchy, does not
+ * search for {@code @BeanOverride} declarations on classes or interfaces, and
+ * does not search for {@code @BeanOverride} declarations on constructor
+ * parameters.
* @param testClass the test class to process
* @return a list of bean override handlers
* @see #findAllHandlers(Class)
@@ -69,7 +84,8 @@ public abstract class BeanOverrideUtils {
* Process the given {@code testClass} and build the corresponding
* {@link BeanOverrideHandler} list derived from {@link BeanOverride @BeanOverride}
* fields in the test class and in its type hierarchy as well as from
- * {@code @BeanOverride} declarations on classes and interfaces.
+ * {@code @BeanOverride} declarations on classes and interfaces and
+ * {@code @BeanOverride} declarations on constructor parameters.
* This method additionally searches for {@code @BeanOverride} declarations
* in the enclosing class hierarchy based on
* {@link TestContextAnnotationUtils#searchEnclosingClass(Class)} semantics.
@@ -83,7 +99,7 @@ public abstract class BeanOverrideUtils {
private static List If the test class declares multiple constructors, this method returns
+ * {@code null}.
+ * @param testClass the test class to process
+ * @return the candidate constructor, or {@code null} if no suitable candidate
+ * was found
+ * @since 7.1
+ */
+ private static @Nullable Constructor> findConstructorWithParameters(Class> testClass) {
+ List When {@code @MockitoBean} is declared on a field, the bean to mock is inferred
- * from the type of the annotated field. If multiple candidates exist in the
- * {@code ApplicationContext}, a {@code @Qualifier} annotation can be declared
- * on the field to help disambiguate. In the absence of a {@code @Qualifier}
- * annotation, the name of the annotated field will be used as a fallback
- * qualifier. Alternatively, you can explicitly specify a bean name to mock
- * by setting the {@link #value() value} or {@link #name() name} attribute.
+ * When {@code @MockitoBean} is declared on a field or parameter, the bean to
+ * mock is inferred from the type of the annotated field or parameter. If multiple
+ * candidates exist in the {@code ApplicationContext}, a {@code @Qualifier} annotation
+ * can be declared on the field or parameter to help disambiguate. In the absence
+ * of a {@code @Qualifier} annotation, the name of the annotated field or parameter
+ * will be used as a fallback qualifier. Alternatively, you can explicitly
+ * specify a bean name to mock by setting the {@link #value() value} or
+ * {@link #name() name} attribute.
*
* When {@code @MockitoBean} is declared at the type level, the type of bean
* (or beans) to mock must be supplied via the {@link #types() types} attribute.
@@ -116,7 +118,7 @@ import org.springframework.test.context.bean.override.BeanOverride;
* @see org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean
* @see org.springframework.test.context.bean.override.convention.TestBean @TestBean
*/
-@Target({ElementType.FIELD, ElementType.TYPE})
+@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(MockitoBeans.class)
@@ -135,9 +137,9 @@ public @interface MockitoBean {
/**
* Name of the bean to mock.
* If left unspecified, the bean to mock is selected according to the
- * configured {@link #types() types} or the annotated field's type, taking
- * qualifiers into account if necessary. See the {@linkplain MockitoBean
- * class-level documentation} for details.
+ * configured {@link #types() types} or the type of the annotated field or
+ * parameter, taking qualifiers into account if necessary. See the
+ * {@linkplain MockitoBean class-level documentation} for details.
* @see #value()
*/
@AliasFor("value")
@@ -148,7 +150,7 @@ public @interface MockitoBean {
* Defaults to none.
* Each type specified will result in a mock being created and registered
* with the {@code ApplicationContext}.
- * Types must be omitted when the annotation is used on a field.
+ * Types must be omitted when the annotation is used on a field or parameter.
* When {@code @MockitoBean} also defines a {@link #name name}, this attribute
* can only contain a single value.
* @return the types to mock
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java
index c0532f6f52b..79339d2d514 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java
@@ -17,6 +17,7 @@
package org.springframework.test.context.bean.override.mockito;
import java.lang.reflect.Field;
+import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
@@ -58,7 +59,7 @@ class MockitoBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
MockitoBeanOverrideHandler(ResolvableType typeToMock, MockitoBean mockitoBean) {
- this(null, typeToMock, mockitoBean);
+ this((Field) null, typeToMock, mockitoBean);
}
MockitoBeanOverrideHandler(@Nullable Field field, ResolvableType typeToMock, MockitoBean mockitoBean) {
@@ -67,6 +68,12 @@ class MockitoBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
mockitoBean.reset(), mockitoBean.extraInterfaces(), mockitoBean.answers(), mockitoBean.serializable());
}
+ MockitoBeanOverrideHandler(Parameter parameter, ResolvableType typeToMock, MockitoBean mockitoBean) {
+ this(parameter, typeToMock, (!mockitoBean.name().isBlank() ? mockitoBean.name() : null),
+ mockitoBean.contextName(), (mockitoBean.enforceOverride() ? REPLACE : REPLACE_OR_CREATE),
+ mockitoBean.reset(), mockitoBean.extraInterfaces(), mockitoBean.answers(), mockitoBean.serializable());
+ }
+
private MockitoBeanOverrideHandler(@Nullable Field field, ResolvableType typeToMock, @Nullable String beanName,
String contextName, BeanOverrideStrategy strategy, MockReset reset, Class>[] extraInterfaces,
Answers answers, boolean serializable) {
@@ -78,6 +85,16 @@ class MockitoBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
this.serializable = serializable;
}
+ private MockitoBeanOverrideHandler(Parameter parameter, ResolvableType typeToMock, @Nullable String beanName,
+ String contextName, BeanOverrideStrategy strategy, MockReset reset, Class>[] extraInterfaces,
+ Answers answers, boolean serializable) {
+
+ super(parameter, typeToMock, beanName, contextName, strategy, reset);
+ Assert.notNull(typeToMock, "'typeToMock' must not be null");
+ this.extraInterfaces = asClassSet(extraInterfaces);
+ this.answers = answers;
+ this.serializable = serializable;
+ }
private static Set When {@code @MockitoSpyBean} is declared on a field, the bean to spy is
- * inferred from the type of the annotated field. If multiple candidates exist in
- * the {@code ApplicationContext}, a {@code @Qualifier} annotation can be declared
- * on the field to help disambiguate. In the absence of a {@code @Qualifier}
- * annotation, the name of the annotated field will be used as a fallback
- * qualifier. Alternatively, you can explicitly specify a bean name to spy
- * by setting the {@link #value() value} or {@link #name() name} attribute. If a
- * bean name is specified, it is required that a target bean with that name has
- * been previously registered in the application context.
+ * When {@code @MockitoSpyBean} is declared on a field or parameter, the bean
+ * to spy is inferred from the type of the annotated field or parameter. If multiple
+ * candidates exist in the {@code ApplicationContext}, a {@code @Qualifier} annotation
+ * can be declared on the field or parameter to help disambiguate. In the absence
+ * of a {@code @Qualifier} annotation, the name of the annotated field or parameter
+ * will be used as a fallback qualifier. Alternatively, you can explicitly
+ * specify a bean name to spy by setting the {@link #value() value} or
+ * {@link #name() name} attribute. If a bean name is specified, it is required that
+ * a target bean with that name has been previously registered in the application
+ * context.
*
* When {@code @MockitoSpyBean} is declared at the type level, the type of bean
* (or beans) to spy must be supplied via the {@link #types() types} attribute.
@@ -123,7 +125,7 @@ import org.springframework.test.context.bean.override.BeanOverride;
* @see org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean
* @see org.springframework.test.context.bean.override.convention.TestBean @TestBean
*/
-@Target({ElementType.FIELD, ElementType.TYPE})
+@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(MockitoSpyBeans.class)
@@ -142,9 +144,9 @@ public @interface MockitoSpyBean {
/**
* Name of the bean to spy.
* If left unspecified, the bean to spy is selected according to the
- * configured {@link #types() types} or the annotated field's type, taking
- * qualifiers into account if necessary. See the {@linkplain MockitoSpyBean
- * class-level documentation} for details.
+ * configured {@link #types() types} or the type of the annotated field or
+ * parameter, taking qualifiers into account if necessary. See the
+ * {@linkplain MockitoSpyBean class-level documentation} for details.
* @see #value()
*/
@AliasFor("value")
@@ -155,7 +157,7 @@ public @interface MockitoSpyBean {
* Defaults to none.
* Each type specified will result in a spy being created and registered
* with the {@code ApplicationContext}.
- * Types must be omitted when the annotation is used on a field.
+ * Types must be omitted when the annotation is used on a field or parameter.
* When {@code @MockitoSpyBean} also defines a {@link #name name}, this
* attribute can only contain a single value.
* @return the types to spy
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java
index e9913ab44e0..d8cddb67489 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java
@@ -17,6 +17,7 @@
package org.springframework.test.context.bean.override.mockito;
import java.lang.reflect.Field;
+import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import org.jspecify.annotations.Nullable;
@@ -49,7 +50,7 @@ class MockitoSpyBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
MockitoSpyBeanOverrideHandler(ResolvableType typeToSpy, MockitoSpyBean spyBean) {
- this(null, typeToSpy, spyBean);
+ this((Field) null, typeToSpy, spyBean);
}
MockitoSpyBeanOverrideHandler(@Nullable Field field, ResolvableType typeToSpy, MockitoSpyBean spyBean) {
@@ -58,6 +59,12 @@ class MockitoSpyBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
Assert.notNull(typeToSpy, "typeToSpy must not be null");
}
+ MockitoSpyBeanOverrideHandler(Parameter parameter, ResolvableType typeToSpy, MockitoSpyBean spyBean) {
+ super(parameter, typeToSpy, (StringUtils.hasText(spyBean.name()) ? spyBean.name() : null),
+ spyBean.contextName(), BeanOverrideStrategy.WRAP, spyBean.reset());
+ Assert.notNull(typeToSpy, "typeToSpy must not be null");
+ }
+
@Override
protected Object createOverrideInstance(String beanName, @Nullable BeanDefinition existingBeanDefinition,
diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java
index debb5708369..92f186c7804 100644
--- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java
+++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java
@@ -24,6 +24,8 @@ import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
+import java.util.Objects;
+import java.util.Optional;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.AfterAll;
@@ -58,6 +60,9 @@ import org.springframework.core.annotation.RepeatableContainers;
import org.springframework.test.context.MethodInvoker;
import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.test.context.TestContextManager;
+import org.springframework.test.context.bean.override.BeanOverride;
+import org.springframework.test.context.bean.override.BeanOverrideHandler;
+import org.springframework.test.context.bean.override.BeanOverrideUtils;
import org.springframework.test.context.event.ApplicationEvents;
import org.springframework.test.context.event.RecordApplicationEvents;
import org.springframework.test.context.support.PropertyProvider;
@@ -370,6 +375,9 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes
* invoked with a fallback {@link PropertyProvider} that delegates its lookup
* to {@link ExtensionContext#getConfigurationParameter(String)}.
* Delegates to {@link ParameterResolutionDelegate}.
* @see #supportsParameter
- * @see ParameterResolutionDelegate#resolveDependency
+ * @see ParameterResolutionDelegate#resolveDependency(Parameter, int, String, Class, org.springframework.beans.factory.config.AutowireCapableBeanFactory)
*/
@Override
public @Nullable Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
@@ -428,6 +437,16 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes
}
ApplicationContext applicationContext = getApplicationContext(extensionContext);
+ if (isBeanOverride(parameter)) {
+ Optional If the supplied {@code ExtensionContext} is already properly scoped, it
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/CustomQualifier.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/CustomQualifier.java
index 6f1e5a73c2e..47ed8c0c792 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/CustomQualifier.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/CustomQualifier.java
@@ -24,7 +24,7 @@ import java.lang.annotation.Target;
import org.springframework.beans.factory.annotation.Qualifier;
-@Target({ElementType.FIELD, ElementType.METHOD})
+@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Qualifier
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanConfigurationErrorTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanConfigurationErrorTests.java
index cec11d42147..d7aabf7d2da 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanConfigurationErrorTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanConfigurationErrorTests.java
@@ -88,6 +88,65 @@ class MockitoBeanConfigurationErrorTests {
List.of("bean1", "bean2"));
}
+ @Test // gh-36096
+ void cannotOverrideBeanByNameWithNoSuchBeanNameOnConstructorParameter() {
+ GenericApplicationContext context = new GenericApplicationContext();
+ context.registerBean("anotherBean", String.class, () -> "example");
+ BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(FailureByNameLookupOnConstructorParameter.class, context);
+ assertThatIllegalStateException()
+ .isThrownBy(context::refresh)
+ .withMessage("""
+ Unable to replace bean: there is no bean with name 'beanToOverride' and type \
+ java.lang.String (as required by parameter 'example' in constructor for %s). \
+ If the bean is defined in a @Bean method, make sure the return type is the most \
+ specific type possible (for example, the concrete implementation type).""",
+ FailureByNameLookupOnConstructorParameter.class.getName());
+ }
+
+ @Test // gh-36096
+ void cannotOverrideBeanByNameWithBeanOfWrongTypeOnConstructorParameter() {
+ GenericApplicationContext context = new GenericApplicationContext();
+ context.registerBean("beanToOverride", Integer.class, () -> 42);
+ BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(FailureByNameLookupOnConstructorParameter.class, context);
+ assertThatIllegalStateException()
+ .isThrownBy(context::refresh)
+ .withMessage("""
+ Unable to replace bean: there is no bean with name 'beanToOverride' and type \
+ java.lang.String (as required by parameter 'example' in constructor for %s). \
+ If the bean is defined in a @Bean method, make sure the return type is the most \
+ specific type possible (for example, the concrete implementation type).""",
+ FailureByNameLookupOnConstructorParameter.class.getName());
+ }
+
+ @Test // gh-36096
+ void cannotOverrideBeanByTypeWithNoSuchBeanTypeOnConstructorParameter() {
+ GenericApplicationContext context = new GenericApplicationContext();
+ BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(FailureByTypeLookupOnConstructorParameter.class, context);
+ assertThatIllegalStateException()
+ .isThrownBy(context::refresh)
+ .withMessage("""
+ Unable to override bean: there are no beans of type java.lang.String \
+ (as required by parameter 'example' in constructor for %s). \
+ If the bean is defined in a @Bean method, make sure the return type is the most \
+ specific type possible (for example, the concrete implementation type).""",
+ FailureByTypeLookupOnConstructorParameter.class.getName());
+ }
+
+ @Test // gh-36096
+ void cannotOverrideBeanByTypeWithTooManyBeansOfThatTypeOnConstructorParameter() {
+ GenericApplicationContext context = new GenericApplicationContext();
+ context.registerBean("bean1", String.class, () -> "example1");
+ context.registerBean("bean2", String.class, () -> "example2");
+ BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(FailureByTypeLookupOnConstructorParameter.class, context);
+ assertThatIllegalStateException()
+ .isThrownBy(context::refresh)
+ .withMessage("""
+ Unable to select a bean to override: found 2 beans of type java.lang.String \
+ (as required by parameter 'example' in constructor for %s): %s""",
+ FailureByTypeLookupOnConstructorParameter.class.getName(),
+ List.of("bean1", "bean2"));
+ }
+
static class FailureByTypeLookup {
@@ -99,7 +158,18 @@ class MockitoBeanConfigurationErrorTests {
@MockitoBean(name = "beanToOverride", enforceOverride = true)
String example;
+ }
+
+ static class FailureByTypeLookupOnConstructorParameter {
+
+ FailureByTypeLookupOnConstructorParameter(@MockitoBean(enforceOverride = true) String example) {
+ }
+ }
+
+ static class FailureByNameLookupOnConstructorParameter {
+ FailureByNameLookupOnConstructorParameter(@MockitoBean(name = "beanToOverride", enforceOverride = true) String example) {
+ }
}
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java
index 3fe2c80157a..3b9234fb5bf 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java
@@ -194,7 +194,7 @@ class MockitoBeanOverrideHandlerTests {
private MockitoBeanOverrideHandler createHandler(Class> clazz) {
MockitoBean annotation = AnnotatedElementUtils.getMergedAnnotation(clazz, MockitoBean.class);
- return new MockitoBeanOverrideHandler(null, ResolvableType.forClass(annotation.types()[0]), annotation);
+ return new MockitoBeanOverrideHandler((Field) null, ResolvableType.forClass(annotation.types()[0]), annotation);
}
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
index 78fe74349c4..ff025944095 100644
--- 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
@@ -18,6 +18,7 @@ package org.springframework.test.context.bean.override.mockito;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
+import java.lang.reflect.Parameter;
import java.util.List;
import org.jspecify.annotations.Nullable;
@@ -102,6 +103,81 @@ class MockitoBeanOverrideProcessorTests {
}
}
+ @Nested // gh-36096
+ class CreateHandlerForParameterTests {
+
+ private final Parameter parameter = TestCase.class.getDeclaredConstructors()[0].getParameters()[0];
+
+
+ @Test
+ void mockAnnotationCreatesMockitoBeanOverrideHandler() {
+ MockitoBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoBean.class);
+ BeanOverrideHandler handler = processor.createHandler(annotation, TestCase.class, parameter);
+
+ assertThat(handler).isExactlyInstanceOf(MockitoBeanOverrideHandler.class);
+ }
+
+ @Test
+ void spyAnnotationCreatesMockitoSpyBeanOverrideHandler() {
+ MockitoSpyBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoSpyBean.class);
+ BeanOverrideHandler handler = processor.createHandler(annotation, TestCase.class, parameter);
+
+ assertThat(handler).isExactlyInstanceOf(MockitoSpyBeanOverrideHandler.class);
+ }
+
+ @Test
+ void otherAnnotationThrows() {
+ Annotation annotation = parameter.getAnnotation(Nullable.class);
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> processor.createHandler(annotation, TestCase.class, parameter))
+ .withMessage("Invalid annotation passed to MockitoBeanOverrideProcessor: expected either " +
+ "@MockitoBean or @MockitoSpyBean on parameter '%s' in constructor %s",
+ parameter.getName(), parameter.getDeclaringExecutable().getName());
+ }
+
+ @Test
+ void typesAttributeNotSupportedForMockitoBean() {
+ Parameter parameter = TypesNotSupportedForMockitoBeanTestCase.class
+ .getDeclaredConstructors()[0].getParameters()[0];
+ MockitoBean annotation = parameter.getAnnotation(MockitoBean.class);
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> processor.createHandler(annotation, TypesNotSupportedForMockitoBeanTestCase.class, parameter))
+ .withMessage("The @MockitoBean 'types' attribute must be omitted when declared on a parameter");
+ }
+
+ @Test
+ void typesAttributeNotSupportedForMockitoSpyBean() {
+ Parameter parameter = TypesNotSupportedForMockitoSpyBeanTestCase.class
+ .getDeclaredConstructors()[0].getParameters()[0];
+ MockitoSpyBean annotation = parameter.getAnnotation(MockitoSpyBean.class);
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> processor.createHandler(annotation, TypesNotSupportedForMockitoSpyBeanTestCase.class, parameter))
+ .withMessage("The @MockitoSpyBean 'types' attribute must be omitted when declared on a parameter");
+ }
+
+
+ static class TestCase {
+
+ TestCase(@MockitoBean @MockitoSpyBean @Nullable Integer number) {
+ }
+ }
+
+ static class TypesNotSupportedForMockitoBeanTestCase {
+
+ TypesNotSupportedForMockitoBeanTestCase(@MockitoBean(types = Integer.class) String param) {
+ }
+ }
+
+ static class TypesNotSupportedForMockitoSpyBeanTestCase {
+
+ TypesNotSupportedForMockitoSpyBeanTestCase(@MockitoSpyBean(types = Integer.class) String param) {
+ }
+ }
+ }
+
@Nested
class CreateHandlersTests {
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanConfigurationErrorTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanConfigurationErrorTests.java
index 387b4af0864..71f63337af4 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanConfigurationErrorTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanConfigurationErrorTests.java
@@ -113,6 +113,50 @@ class MockitoSpyBeanConfigurationErrorTests {
to spy on a scoped proxy, which is not supported.""");
}
+ @Test // gh-36096
+ void contextCustomizerCannotBeCreatedWithNoSuchBeanNameOnConstructorParameter() {
+ GenericApplicationContext context = new GenericApplicationContext();
+ context.registerBean("present", String.class, () -> "example");
+ BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(ByNameSingleLookupOnConstructorParameter.class, context);
+ assertThatIllegalStateException()
+ .isThrownBy(context::refresh)
+ .withMessage("""
+ Unable to wrap bean: there is no bean with name 'beanToSpy' and type \
+ java.lang.String (as required by parameter 'example' in constructor for %s). \
+ If the bean is defined in a @Bean method, make sure the return type is the most \
+ specific type possible (for example, the concrete implementation type).""",
+ ByNameSingleLookupOnConstructorParameter.class.getName());
+ }
+
+ @Test // gh-36096
+ void contextCustomizerCannotBeCreatedWithNoSuchBeanTypeOnConstructorParameter() {
+ GenericApplicationContext context = new GenericApplicationContext();
+ BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(ByTypeSingleLookupOnConstructorParameter.class, context);
+ assertThatIllegalStateException()
+ .isThrownBy(context::refresh)
+ .withMessage("""
+ Unable to select a bean to wrap: there are no beans of type java.lang.String \
+ (as required by parameter 'example' in constructor for %s). \
+ If the bean is defined in a @Bean method, make sure the return type is the most \
+ specific type possible (for example, the concrete implementation type).""",
+ ByTypeSingleLookupOnConstructorParameter.class.getName());
+ }
+
+ @Test // gh-36096
+ void contextCustomizerCannotBeCreatedWithTooManyBeansOfThatTypeOnConstructorParameter() {
+ GenericApplicationContext context = new GenericApplicationContext();
+ context.registerBean("bean1", String.class, () -> "example1");
+ context.registerBean("bean2", String.class, () -> "example2");
+ BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(ByTypeSingleLookupOnConstructorParameter.class, context);
+ assertThatIllegalStateException()
+ .isThrownBy(context::refresh)
+ .withMessage("""
+ Unable to select a bean to wrap: found 2 beans of type java.lang.String \
+ (as required by parameter 'example' in constructor for %s): %s""",
+ ByTypeSingleLookupOnConstructorParameter.class.getName(),
+ List.of("bean1", "bean2"));
+ }
+
static class ByTypeSingleLookup {
@@ -124,7 +168,18 @@ class MockitoSpyBeanConfigurationErrorTests {
@MockitoSpyBean("beanToSpy")
String example;
+ }
+
+ static class ByTypeSingleLookupOnConstructorParameter {
+
+ ByTypeSingleLookupOnConstructorParameter(@MockitoSpyBean String example) {
+ }
+ }
+ static class ByNameSingleLookupOnConstructorParameter {
+
+ ByNameSingleLookupOnConstructorParameter(@MockitoSpyBean("beanToSpy") String example) {
+ }
}
static class ScopedProxyTestCase {
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoBeanByNameLookupForConstructorParametersIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoBeanByNameLookupForConstructorParametersIntegrationTests.java
new file mode 100644
index 00000000000..e312efb3c80
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoBeanByNameLookupForConstructorParametersIntegrationTests.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2002-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.constructor;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+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.bean.override.mockito.MockitoBean;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+import org.springframework.test.mockito.MockitoAssertions;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Integration tests for {@link MockitoBean @MockitoBean} that use by-name lookup
+ * on constructor parameters.
+ *
+ * @author Sam Brannen
+ * @since 7.1
+ * @see gh-36096
+ * @see org.springframework.test.context.bean.override.mockito.MockitoBeanByNameLookupTestMethodScopedExtensionContextIntegrationTests
+ */
+@SpringJUnitConfig
+class MockitoBeanByNameLookupForConstructorParametersIntegrationTests {
+
+ final ExampleService service0A;
+
+ final ExampleService service0B;
+
+ final ExampleService service0C;
+
+ final ExampleService nonExisting;
+
+
+ MockitoBeanByNameLookupForConstructorParametersIntegrationTests(
+ @MockitoBean ExampleService s0A,
+ @MockitoBean(name = "s0B") ExampleService service0B,
+ @MockitoBean @Qualifier("s0C") ExampleService service0C,
+ @MockitoBean("nonExistingBean") ExampleService nonExisting) {
+
+ this.service0A = s0A;
+ this.service0B = service0B;
+ this.service0C = service0C;
+ this.nonExisting = nonExisting;
+ }
+
+
+ @Test
+ void parameterNameIsUsedAsBeanName(ApplicationContext ctx) {
+ assertThat(this.service0A)
+ .satisfies(MockitoAssertions::assertIsMock)
+ .isSameAs(ctx.getBean("s0A"));
+
+ assertThat(this.service0A.greeting()).as("mocked greeting").isNull();
+ }
+
+ @Test
+ void explicitBeanNameOverridesParameterName(ApplicationContext ctx) {
+ assertThat(this.service0B)
+ .satisfies(MockitoAssertions::assertIsMock)
+ .isSameAs(ctx.getBean("s0B"));
+
+ assertThat(this.service0B.greeting()).as("mocked greeting").isNull();
+ }
+
+ @Test
+ void qualifierIsUsedToResolveByName(ApplicationContext ctx) {
+ assertThat(this.service0C)
+ .satisfies(MockitoAssertions::assertIsMock)
+ .isSameAs(ctx.getBean("s0C"));
+
+ assertThat(this.service0C.greeting()).as("mocked greeting").isNull();
+ }
+
+ @Test
+ void mockIsCreatedWhenNoBeanExistsWithProvidedName(ApplicationContext ctx) {
+ assertThat(this.nonExisting)
+ .satisfies(MockitoAssertions::assertIsMock)
+ .isSameAs(ctx.getBean("nonExistingBean"));
+
+ assertThat(this.nonExisting.greeting()).as("mocked greeting").isNull();
+ }
+
+
+ @Nested
+ class NestedTests {
+
+ @Autowired
+ @Qualifier("s0A")
+ ExampleService localService0A;
+
+ @Autowired
+ @Qualifier("nonExistingBean")
+ ExampleService localNonExisting;
+
+ final ExampleService nestedNonExisting;
+
+
+ NestedTests(@MockitoBean("nestedNonExistingBean") ExampleService nestedNonExisting) {
+ this.nestedNonExisting = nestedNonExisting;
+ }
+
+
+ @Test
+ void mockFromEnclosingClassIsAccessibleViaAutowiring(ApplicationContext ctx) {
+ assertThat(this.localService0A)
+ .satisfies(MockitoAssertions::assertIsMock)
+ .isSameAs(service0A)
+ .isSameAs(ctx.getBean("s0A"));
+
+ assertThat(this.localService0A.greeting()).as("mocked greeting").isNull();
+ }
+
+ @Test
+ void mockForNonExistingBeanFromEnclosingClassIsAccessibleViaAutowiring(ApplicationContext ctx) {
+ assertThat(this.localNonExisting)
+ .satisfies(MockitoAssertions::assertIsMock)
+ .isSameAs(nonExisting)
+ .isSameAs(ctx.getBean("nonExistingBean"));
+
+ assertThat(this.localNonExisting.greeting()).as("mocked greeting").isNull();
+ }
+
+ @Test
+ void nestedConstructorParameterIsMockedWhenNoBeanExistsWithProvidedName(ApplicationContext ctx) {
+ assertThat(this.nestedNonExisting)
+ .satisfies(MockitoAssertions::assertIsMock)
+ .isSameAs(ctx.getBean("nestedNonExistingBean"));
+
+ assertThat(this.nestedNonExisting.greeting()).as("mocked greeting").isNull();
+ }
+ }
+
+
+ @Configuration(proxyBeanMethods = false)
+ static class Config {
+
+ @Bean
+ ExampleService s0A() {
+ return new RealExampleService("prod s0A");
+ }
+
+ @Bean
+ ExampleService s0B() {
+ return new RealExampleService("prod s0B");
+ }
+
+ @Bean
+ ExampleService s0C() {
+ return new RealExampleService("prod s0C");
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoBeanByTypeLookupForConstructorParametersIntegrationRecordTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoBeanByTypeLookupForConstructorParametersIntegrationRecordTests.java
new file mode 100644
index 00000000000..958b3c4ccca
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoBeanByTypeLookupForConstructorParametersIntegrationRecordTests.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2002-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.constructor;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
+
+/**
+ * Integration tests for {@link MockitoBean @MockitoBean} that use by-type lookup
+ * on constructor parameters in a Java record.
+ *
+ * @author Sam Brannen
+ * @since 7.1
+ * @see gh-36096
+ */
+@SpringJUnitConfig
+record MockitoBeanByTypeLookupForConstructorParametersIntegrationRecordTests(
+ @MockitoBean ExampleService exampleService) {
+
+ @Test
+ void test() {
+ assertIsMock(this.exampleService);
+
+ when(this.exampleService.greeting()).thenReturn("Mocked greeting");
+
+ assertThat(this.exampleService.greeting()).isEqualTo("Mocked greeting");
+ verify(this.exampleService, times(1)).greeting();
+ verifyNoMoreInteractions(this.exampleService);
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoBeanByTypeLookupForConstructorParametersIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoBeanByTypeLookupForConstructorParametersIntegrationTests.java
new file mode 100644
index 00000000000..c88587bd6aa
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoBeanByTypeLookupForConstructorParametersIntegrationTests.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2002-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.constructor;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.test.context.bean.override.example.CustomQualifier;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.RealExampleService;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+import org.springframework.test.mockito.MockitoAssertions;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
+
+/**
+ * Integration tests for {@link MockitoBean @MockitoBean} that use by-type lookup
+ * on constructor parameters.
+ *
+ * @author Sam Brannen
+ * @since 7.1
+ * @see gh-36096
+ * @see org.springframework.test.context.bean.override.mockito.MockitoBeanByTypeLookupIntegrationTests
+ */
+@SpringJUnitConfig
+class MockitoBeanByTypeLookupForConstructorParametersIntegrationTests {
+
+ final AnotherService serviceIsNotABean;
+
+ final ExampleService anyNameForService;
+
+ final StringBuilder ambiguous;
+
+ final StringBuilder ambiguousMeta;
+
+
+ MockitoBeanByTypeLookupForConstructorParametersIntegrationTests(
+ @MockitoBean AnotherService serviceIsNotABean,
+ @MockitoBean ExampleService anyNameForService,
+ @MockitoBean @Qualifier("prefer") StringBuilder ambiguous,
+ @MockitoBean @CustomQualifier StringBuilder ambiguousMeta) {
+
+ this.serviceIsNotABean = serviceIsNotABean;
+ this.anyNameForService = anyNameForService;
+ this.ambiguous = ambiguous;
+ this.ambiguousMeta = ambiguousMeta;
+ }
+
+
+ @Test
+ void mockIsCreatedWhenNoCandidateIsFound() {
+ assertIsMock(this.serviceIsNotABean);
+
+ when(this.serviceIsNotABean.hello()).thenReturn("Mocked hello");
+
+ assertThat(this.serviceIsNotABean.hello()).isEqualTo("Mocked hello");
+ verify(this.serviceIsNotABean, times(1)).hello();
+ verifyNoMoreInteractions(this.serviceIsNotABean);
+ }
+
+ @Test
+ void overrideIsFoundByType(ApplicationContext ctx) {
+ assertThat(this.anyNameForService)
+ .satisfies(MockitoAssertions::assertIsMock)
+ .isSameAs(ctx.getBean("example"))
+ .isSameAs(ctx.getBean(ExampleService.class));
+
+ when(this.anyNameForService.greeting()).thenReturn("Mocked greeting");
+
+ assertThat(this.anyNameForService.greeting()).isEqualTo("Mocked greeting");
+ verify(this.anyNameForService, times(1)).greeting();
+ verifyNoMoreInteractions(this.anyNameForService);
+ }
+
+ @Test
+ void overrideIsFoundByTypeAndDisambiguatedByQualifier(ApplicationContext ctx) {
+ assertThat(this.ambiguous)
+ .satisfies(MockitoAssertions::assertIsMock)
+ .isSameAs(ctx.getBean("ambiguous2"));
+
+ assertThatExceptionOfType(NoUniqueBeanDefinitionException.class)
+ .isThrownBy(() -> ctx.getBean(StringBuilder.class))
+ .satisfies(ex -> assertThat(ex.getBeanNamesFound()).containsOnly("ambiguous1", "ambiguous2"));
+
+ assertThat(this.ambiguous).isEmpty();
+ assertThat(this.ambiguous.substring(0)).isNull();
+ verify(this.ambiguous, times(1)).length();
+ verify(this.ambiguous, times(1)).substring(anyInt());
+ verifyNoMoreInteractions(this.ambiguous);
+ }
+
+ @Test
+ void overrideIsFoundByTypeAndDisambiguatedByMetaQualifier(ApplicationContext ctx) {
+ assertThat(this.ambiguousMeta)
+ .satisfies(MockitoAssertions::assertIsMock)
+ .isSameAs(ctx.getBean("ambiguous1"));
+
+ assertThatExceptionOfType(NoUniqueBeanDefinitionException.class)
+ .isThrownBy(() -> ctx.getBean(StringBuilder.class))
+ .satisfies(ex -> assertThat(ex.getBeanNamesFound()).containsOnly("ambiguous1", "ambiguous2"));
+
+ assertThat(this.ambiguousMeta).isEmpty();
+ assertThat(this.ambiguousMeta.substring(0)).isNull();
+ verify(this.ambiguousMeta, times(1)).length();
+ verify(this.ambiguousMeta, times(1)).substring(anyInt());
+ verifyNoMoreInteractions(this.ambiguousMeta);
+ }
+
+
+ @Nested
+ class NestedTests {
+
+ @Autowired
+ ExampleService localAnyNameForService;
+
+ final NestedService nestedService;
+
+
+ NestedTests(@MockitoBean NestedService nestedService) {
+ this.nestedService = nestedService;
+ }
+
+
+ @Test
+ void mockFromEnclosingClassConstructorParameterIsAccessibleViaAutowiring(ApplicationContext ctx) {
+ assertThat(this.localAnyNameForService)
+ .satisfies(MockitoAssertions::assertIsMock)
+ .isSameAs(anyNameForService)
+ .isSameAs(ctx.getBean("example"))
+ .isSameAs(ctx.getBean(ExampleService.class));
+ }
+
+ @Test
+ void nestedConstructorParameterIsAMock() {
+ assertIsMock(this.nestedService);
+
+ when(this.nestedService.hello()).thenReturn("Nested hello");
+ assertThat(this.nestedService.hello()).isEqualTo("Nested hello");
+ verify(this.nestedService).hello();
+ verifyNoMoreInteractions(this.nestedService);
+ }
+ }
+
+
+ public interface AnotherService {
+
+ String hello();
+ }
+
+ public interface NestedService {
+
+ String hello();
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ static class Config {
+
+ @Bean("example")
+ ExampleService bean1() {
+ return new RealExampleService("Production hello");
+ }
+
+ @Bean("ambiguous1")
+ @Order(1)
+ @CustomQualifier
+ StringBuilder bean2() {
+ return new StringBuilder("bean2");
+ }
+
+ @Bean("ambiguous2")
+ @Order(2)
+ @Qualifier("prefer")
+ StringBuilder bean3() {
+ return new StringBuilder("bean3");
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoSpyBeanByNameLookupForConstructorParametersIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoSpyBeanByNameLookupForConstructorParametersIntegrationTests.java
new file mode 100644
index 00000000000..0852a2d41cd
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoSpyBeanByNameLookupForConstructorParametersIntegrationTests.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2002-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.constructor;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+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.bean.override.mockito.MockitoSpyBean;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+import org.springframework.test.mockito.MockitoAssertions;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+/**
+ * Integration tests for {@link MockitoSpyBean @MockitoSpyBean} that use by-name
+ * lookup on constructor parameters.
+ *
+ * @author Sam Brannen
+ * @since 7.1
+ * @see gh-36096
+ * @see org.springframework.test.context.bean.override.mockito.MockitoSpyBeanByNameLookupTestMethodScopedExtensionContextIntegrationTests
+ */
+@SpringJUnitConfig
+class MockitoSpyBeanByNameLookupForConstructorParametersIntegrationTests {
+
+ final ExampleService service1;
+
+ final ExampleService service2;
+
+ final ExampleService service3;
+
+
+ MockitoSpyBeanByNameLookupForConstructorParametersIntegrationTests(
+ @MockitoSpyBean ExampleService s1,
+ @MockitoSpyBean("s2") ExampleService service2,
+ @MockitoSpyBean @Qualifier("s3") ExampleService service3) {
+
+ this.service1 = s1;
+ this.service2 = service2;
+ this.service3 = service3;
+ }
+
+
+ @Test
+ void parameterNameIsUsedAsBeanName(ApplicationContext ctx) {
+ assertThat(this.service1)
+ .satisfies(MockitoAssertions::assertIsSpy)
+ .isSameAs(ctx.getBean("s1"));
+
+ assertThat(this.service1.greeting()).isEqualTo("prod 1");
+ verify(this.service1).greeting();
+ verifyNoMoreInteractions(this.service1);
+ }
+
+ @Test
+ void explicitBeanNameOverridesParameterName(ApplicationContext ctx) {
+ assertThat(this.service2)
+ .satisfies(MockitoAssertions::assertIsSpy)
+ .isSameAs(ctx.getBean("s2"));
+
+ assertThat(this.service2.greeting()).isEqualTo("prod 2");
+ verify(this.service2).greeting();
+ verifyNoMoreInteractions(this.service2);
+ }
+
+ @Test
+ void qualifierIsUsedToResolveByName(ApplicationContext ctx) {
+ assertThat(this.service3)
+ .satisfies(MockitoAssertions::assertIsSpy)
+ .isSameAs(ctx.getBean("s3"));
+
+ assertThat(this.service3.greeting()).isEqualTo("prod 3");
+ verify(this.service3).greeting();
+ verifyNoMoreInteractions(this.service3);
+ }
+
+
+ @Nested
+ class NestedTests {
+
+ @Autowired
+ @Qualifier("s1")
+ ExampleService localService1;
+
+ final ExampleService nestedSpy;
+
+
+ NestedTests(@MockitoSpyBean("s4") ExampleService nestedSpy) {
+ this.nestedSpy = nestedSpy;
+ }
+
+
+ @Test
+ void spyFromEnclosingClassIsAccessibleViaAutowiring(ApplicationContext ctx) {
+ assertThat(this.localService1)
+ .satisfies(MockitoAssertions::assertIsSpy)
+ .isSameAs(service1)
+ .isSameAs(ctx.getBean("s1"));
+
+ assertThat(this.localService1.greeting()).isEqualTo("prod 1");
+ verify(this.localService1).greeting();
+ verifyNoMoreInteractions(this.localService1);
+ }
+
+ @Test
+ void nestedConstructorParameterIsASpy(ApplicationContext ctx) {
+ assertThat(this.nestedSpy)
+ .satisfies(MockitoAssertions::assertIsSpy)
+ .isSameAs(ctx.getBean("s4"));
+
+ assertThat(this.nestedSpy.greeting()).isEqualTo("prod 4");
+ verify(this.nestedSpy).greeting();
+ verifyNoMoreInteractions(this.nestedSpy);
+ }
+ }
+
+
+ @Configuration(proxyBeanMethods = false)
+ static class Config {
+
+ @Bean
+ ExampleService s1() {
+ return new RealExampleService("prod 1");
+ }
+
+ @Bean
+ ExampleService s2() {
+ return new RealExampleService("prod 2");
+ }
+
+ @Bean
+ ExampleService s3() {
+ return new RealExampleService("prod 3");
+ }
+
+ @Bean
+ ExampleService s4() {
+ return new RealExampleService("prod 4");
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoSpyBeanByTypeLookupForConstructorParametersIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoSpyBeanByTypeLookupForConstructorParametersIntegrationTests.java
new file mode 100644
index 00000000000..ca6b3bb6c87
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoSpyBeanByTypeLookupForConstructorParametersIntegrationTests.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2002-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.constructor;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.test.context.bean.override.example.CustomQualifier;
+import org.springframework.test.context.bean.override.example.ExampleService;
+import org.springframework.test.context.bean.override.example.RealExampleService;
+import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+import org.springframework.test.mockito.MockitoAssertions;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatException;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+/**
+ * Integration tests for {@link MockitoSpyBean @MockitoSpyBean} that use by-type
+ * lookup on constructor parameters.
+ *
+ * @author Sam Brannen
+ * @since 7.1
+ * @see gh-36096
+ * @see org.springframework.test.context.bean.override.mockito.MockitoSpyBeanByTypeLookupIntegrationTests
+ */
+@SpringJUnitConfig
+class MockitoSpyBeanByTypeLookupForConstructorParametersIntegrationTests {
+
+ final ExampleService anyNameForService;
+
+ final StringHolder ambiguous;
+
+ final StringHolder ambiguousMeta;
+
+
+ MockitoSpyBeanByTypeLookupForConstructorParametersIntegrationTests(
+ @MockitoSpyBean ExampleService anyNameForService,
+ @MockitoSpyBean @Qualifier("prefer") StringHolder ambiguous,
+ @MockitoSpyBean @CustomQualifier StringHolder ambiguousMeta) {
+
+ this.anyNameForService = anyNameForService;
+ this.ambiguous = ambiguous;
+ this.ambiguousMeta = ambiguousMeta;
+ }
+
+
+ @Test
+ void overrideIsFoundByType(ApplicationContext ctx) {
+ assertThat(this.anyNameForService)
+ .satisfies(MockitoAssertions::assertIsSpy)
+ .isSameAs(ctx.getBean("example"))
+ .isSameAs(ctx.getBean(ExampleService.class));
+
+ assertThat(this.anyNameForService.greeting()).isEqualTo("Production hello");
+ verify(this.anyNameForService).greeting();
+ verifyNoMoreInteractions(this.anyNameForService);
+ }
+
+ @Test
+ void overrideIsFoundByTypeAndDisambiguatedByQualifier(ApplicationContext ctx) {
+ assertThat(this.ambiguous)
+ .satisfies(MockitoAssertions::assertIsSpy)
+ .isSameAs(ctx.getBean("ambiguous2"));
+
+ assertThatException()
+ .isThrownBy(() -> ctx.getBean(StringHolder.class))
+ .withMessageEndingWith("but found 2: ambiguous1,ambiguous2");
+
+ assertThat(this.ambiguous.getValue()).isEqualTo("bean3");
+ assertThat(this.ambiguous.size()).isEqualTo(5);
+ verify(this.ambiguous).getValue();
+ verify(this.ambiguous).size();
+ verifyNoMoreInteractions(this.ambiguous);
+ }
+
+ @Test
+ void overrideIsFoundByTypeAndDisambiguatedByMetaQualifier(ApplicationContext ctx) {
+ assertThat(this.ambiguousMeta)
+ .satisfies(MockitoAssertions::assertIsSpy)
+ .isSameAs(ctx.getBean("ambiguous1"));
+
+ assertThatException()
+ .isThrownBy(() -> ctx.getBean(StringHolder.class))
+ .withMessageEndingWith("but found 2: ambiguous1,ambiguous2");
+
+ assertThat(this.ambiguousMeta.getValue()).isEqualTo("bean2");
+ assertThat(this.ambiguousMeta.size()).isEqualTo(5);
+ verify(this.ambiguousMeta).getValue();
+ verify(this.ambiguousMeta).size();
+ verifyNoMoreInteractions(this.ambiguousMeta);
+ }
+
+
+ @Nested
+ class NestedTests {
+
+ @Autowired
+ ExampleService localAnyNameForService;
+
+ final AnotherService nestedSpy;
+
+
+ NestedTests(@MockitoSpyBean AnotherService nestedSpy) {
+ this.nestedSpy = nestedSpy;
+ }
+
+
+ @Test
+ void spyFromEnclosingClassConstructorParameterIsAccessibleViaAutowiring(ApplicationContext ctx) {
+ assertThat(this.localAnyNameForService)
+ .satisfies(MockitoAssertions::assertIsSpy)
+ .isSameAs(anyNameForService)
+ .isSameAs(ctx.getBean("example"))
+ .isSameAs(ctx.getBean(ExampleService.class));
+
+ assertThat(this.localAnyNameForService.greeting()).isEqualTo("Production hello");
+ verify(this.localAnyNameForService).greeting();
+ verifyNoMoreInteractions(this.localAnyNameForService);
+ }
+
+ @Test
+ void nestedConstructorParameterIsASpy(ApplicationContext ctx) {
+ assertThat(this.nestedSpy)
+ .satisfies(MockitoAssertions::assertIsSpy)
+ .isSameAs(ctx.getBean("anotherService"))
+ .isSameAs(ctx.getBean(AnotherService.class));
+
+ assertThat(this.nestedSpy.hello()).isEqualTo("Another hello");
+ verify(this.nestedSpy).hello();
+ verifyNoMoreInteractions(this.nestedSpy);
+ }
+ }
+
+
+ interface AnotherService {
+
+ String hello();
+ }
+
+ static class StringHolder {
+
+ private final String value;
+
+ StringHolder(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return this.value;
+ }
+
+ public int size() {
+ return this.value.length();
+ }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ static class Config {
+
+ @Bean("example")
+ ExampleService bean1() {
+ return new RealExampleService("Production hello");
+ }
+
+ @Bean("ambiguous1")
+ @Order(1)
+ @CustomQualifier
+ StringHolder bean2() {
+ return new StringHolder("bean2");
+ }
+
+ @Bean("ambiguous2")
+ @Order(2)
+ @Qualifier("prefer")
+ StringHolder bean3() {
+ return new StringHolder("bean3");
+ }
+
+ @Bean
+ AnotherService anotherService() {
+ return new AnotherService() {
+ @Override
+ public String hello() {
+ return "Another hello";
+ }
+ };
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/ConstructorService01.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/ConstructorService01.java
new file mode 100644
index 00000000000..adc28db41fa
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/ConstructorService01.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.bean.override.mockito.typelevel;
+
+interface ConstructorService01 extends Service {
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoBeansByNameIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoBeansByNameIntegrationTests.java
index 87424d76770..52f576a6a96 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoBeansByNameIntegrationTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoBeansByNameIntegrationTests.java
@@ -48,6 +48,10 @@ import static org.springframework.test.mockito.MockitoAssertions.assertIsNotMock
@MockitoBean(name = "s2", types = ExampleService.class)
class MockitoBeansByNameIntegrationTests {
+ final ExampleService service0A;
+ final ExampleService service0B;
+ final ExampleService service0C;
+
@Autowired
ExampleService s1;
@@ -62,6 +66,16 @@ class MockitoBeansByNameIntegrationTests {
ExampleService service4;
+ MockitoBeansByNameIntegrationTests(@MockitoBean ExampleService s0A,
+ @MockitoBean(name = "s0B") ExampleService service0B,
+ @MockitoBean @Qualifier("s0C") ExampleService service0C) {
+
+ this.service0A = s0A;
+ this.service0B = service0B;
+ this.service0C = service0C;
+ }
+
+
@BeforeEach
void configureMocks() {
assertIsMock(s1, "s1");
@@ -86,6 +100,21 @@ class MockitoBeansByNameIntegrationTests {
@Configuration
static class Config {
+ @Bean
+ ExampleService s0A() {
+ return () -> "prod 0A";
+ }
+
+ @Bean
+ ExampleService s0B() {
+ return () -> "prod 0B";
+ }
+
+ @Bean
+ ExampleService s0C() {
+ return () -> "prod 0C";
+ }
+
@Bean
ExampleService s1() {
return () -> "prod 1";
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoBeansByTypeIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoBeansByTypeIntegrationTests.java
index e3e5207b4de..f6f529220cc 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoBeansByTypeIntegrationTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoBeansByTypeIntegrationTests.java
@@ -63,10 +63,17 @@ class MockitoBeansByTypeIntegrationTests implements MockTestInterface01 {
@Autowired
Service06 service06;
+ final ConstructorService01 constructorService01;
+
@MockitoBean
Service07 service07;
+ MockitoBeansByTypeIntegrationTests(@MockitoBean ConstructorService01 constructorService01) {
+ this.constructorService01 = constructorService01;
+ }
+
+
@BeforeEach
void configureMocks() {
assertIsMock(service01, "service01");
@@ -75,6 +82,7 @@ class MockitoBeansByTypeIntegrationTests implements MockTestInterface01 {
assertIsMock(service04, "service04");
assertIsMock(service05, "service05");
assertIsMock(service06, "service06");
+ assertIsMock(constructorService01, "constructorService01");
assertIsMock(service07, "service07");
given(service01.greeting()).willReturn("mock 01");
@@ -83,6 +91,7 @@ class MockitoBeansByTypeIntegrationTests implements MockTestInterface01 {
given(service04.greeting()).willReturn("mock 04");
given(service05.greeting()).willReturn("mock 05");
given(service06.greeting()).willReturn("mock 06");
+ given(constructorService01.greeting()).willReturn("mock constructor 01");
given(service07.greeting()).willReturn("mock 07");
}
@@ -94,6 +103,7 @@ class MockitoBeansByTypeIntegrationTests implements MockTestInterface01 {
assertThat(service04.greeting()).isEqualTo("mock 04");
assertThat(service05.greeting()).isEqualTo("mock 05");
assertThat(service06.greeting()).isEqualTo("mock 06");
+ assertThat(constructorService01.greeting()).isEqualTo("mock constructor 01");
assertThat(service07.greeting()).isEqualTo("mock 07");
}
@@ -133,6 +143,7 @@ class MockitoBeansByTypeIntegrationTests implements MockTestInterface01 {
assertIsMock(service04, "service04");
assertIsMock(service05, "service05");
assertIsMock(service06, "service06");
+ assertIsMock(constructorService01, "constructorService01");
assertIsMock(service07, "service07");
assertIsMock(service08, "service08");
assertIsMock(service09, "service09");
@@ -157,6 +168,7 @@ class MockitoBeansByTypeIntegrationTests implements MockTestInterface01 {
assertThat(service04.greeting()).isEqualTo("mock 04");
assertThat(service05.greeting()).isEqualTo("mock 05");
assertThat(service06.greeting()).isEqualTo("mock 06");
+ assertThat(constructorService01.greeting()).isEqualTo("mock constructor 01");
assertThat(service07.greeting()).isEqualTo("mock 07");
assertThat(service08.greeting()).isEqualTo("mock 08");
assertThat(service09.greeting()).isEqualTo("mock 09");
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoBeansTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoBeansTests.java
index b7714e3e512..6eaa8e0123c 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoBeansTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoBeansTests.java
@@ -44,7 +44,7 @@ class MockitoBeansTests {
StreamSingleton Semantics
*
@@ -62,7 +67,7 @@ import org.springframework.core.style.ToStringCreator;
*