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;
*