Browse Source

Support @⁠MockitoBean and @⁠MockitoSpyBean on test constructor parameters

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
main
Sam Brannen 1 week ago
parent
commit
f9523a785b
  1. 164
      framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc
  2. 11
      framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc
  3. 4
      framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc
  4. 6
      spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java
  5. 82
      spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java
  6. 104
      spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java
  7. 31
      spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java
  8. 123
      spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideUtils.java
  9. 10
      spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java
  10. 26
      spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java
  11. 20
      spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java
  12. 21
      spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java
  13. 30
      spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java
  14. 9
      spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java
  15. 26
      spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java
  16. 2
      spring-test/src/test/java/org/springframework/test/context/bean/override/example/CustomQualifier.java
  17. 70
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanConfigurationErrorTests.java
  18. 2
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java
  19. 76
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessorTests.java
  20. 55
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanConfigurationErrorTests.java
  21. 175
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoBeanByNameLookupForConstructorParametersIntegrationTests.java
  22. 55
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoBeanByTypeLookupForConstructorParametersIntegrationRecordTests.java
  23. 207
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoBeanByTypeLookupForConstructorParametersIntegrationTests.java
  24. 165
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoSpyBeanByNameLookupForConstructorParametersIntegrationTests.java
  25. 213
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoSpyBeanByTypeLookupForConstructorParametersIntegrationTests.java
  26. 20
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/ConstructorService01.java
  27. 29
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoBeansByNameIntegrationTests.java
  28. 12
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoBeansByTypeIntegrationTests.java
  29. 6
      spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoBeansTests.java
  30. 44
      spring-test/src/test/kotlin/org/springframework/test/context/bean/override/mockito/MockitoBeanByTypeLookupForConstructorParametersIntegrationKotlinDataClassTests.kt
  31. 44
      spring-test/src/test/kotlin/org/springframework/test/context/bean/override/mockito/MockitoBeanByTypeLookupForConstructorParametersIntegrationKotlinTests.kt

164
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. @@ -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:: @@ -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:: @@ -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]

11
framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc

@ -2,8 +2,9 @@ @@ -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 @@ -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

4
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 @@ -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 &mdash; 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

6
spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java

@ -34,8 +34,10 @@ import org.springframework.aot.hint.annotation.Reflective; @@ -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.
*
* <p>For concrete examples of such composed annotations, see
* {@link org.springframework.test.context.bean.override.convention.TestBean @TestBean},

82
spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java

@ -16,7 +16,7 @@ @@ -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; @@ -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.
*
* <p>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, @@ -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, @@ -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, @@ -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<String> candidateNames = getExistingBeanNamesByType(beanFactory, handler, true);
String uniqueCandidate = determineUniqueCandidate(beanFactory, candidateNames, beanType, field);
String uniqueCandidate = determineUniqueCandidate(beanFactory, candidateNames, beanType, handler);
if (uniqueCandidate != null) {
beanName = uniqueCandidate;
}
@ -271,11 +271,11 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, @@ -271,11 +271,11 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
there are no beans of 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(beanType, requiredByField(field));
.formatted(beanType, requiredByDescription(handler));
}
else {
message += "found %d beans of type %s%s: %s"
.formatted(candidateCount, beanType, requiredByField(field), candidateNames);
.formatted(candidateCount, beanType, requiredByDescription(handler), candidateNames);
}
throw new IllegalStateException(message);
}
@ -289,7 +289,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, @@ -289,7 +289,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
Unable to wrap 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, beanType, requiredByField(field)));
.formatted(beanName, beanType, requiredByDescription(handler)));
}
}
@ -301,11 +301,10 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, @@ -301,11 +301,10 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
private static @Nullable String getBeanNameForType(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler,
boolean requireExistingBean) {
Field field = handler.getField();
ResolvableType beanType = handler.getBeanType();
Set<String> candidateNames = getExistingBeanNamesByType(beanFactory, handler, true);
String uniqueCandidate = determineUniqueCandidate(beanFactory, candidateNames, beanType, field);
String uniqueCandidate = determineUniqueCandidate(beanFactory, candidateNames, beanType, handler);
if (uniqueCandidate != null) {
return uniqueCandidate;
}
@ -317,20 +316,19 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, @@ -317,20 +316,19 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
Unable to override bean: there are no beans of 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(beanType, requiredByField(field)));
.formatted(beanType, requiredByDescription(handler)));
}
return null;
}
throw new IllegalStateException(
"Unable to select a bean to override: found %d beans of type %s%s: %s"
.formatted(candidateCount, beanType, requiredByField(field), candidateNames));
.formatted(candidateCount, beanType, requiredByDescription(handler), candidateNames));
}
private static Set<String> getExistingBeanNamesByType(ConfigurableListableBeanFactory beanFactory,
BeanOverrideHandler handler, boolean checkAutowiredCandidate) {
Field field = handler.getField();
ResolvableType resolvableType = handler.getBeanType();
Class<?> type = resolvableType.toClass();
@ -348,9 +346,11 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, @@ -348,9 +346,11 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
}
// Filter out non-matching autowire candidates.
if (field != null && checkAutowiredCandidate) {
DependencyDescriptor descriptor = new DependencyDescriptor(field, true);
beanNames.removeIf(beanName -> !beanFactory.isAutowireCandidate(beanName, descriptor));
if (checkAutowiredCandidate) {
DependencyDescriptor descriptor = handler.fieldOrParameterDependencyDescriptor();
if (descriptor != null) {
beanNames.removeIf(beanName -> !beanFactory.isAutowireCandidate(beanName, descriptor));
}
}
// Filter out scoped proxy targets.
beanNames.removeIf(ScopedProxyUtils::isScopedTarget);
@ -361,13 +361,14 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, @@ -361,13 +361,14 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
/**
* Determine the unique candidate in the given set of bean names.
* <p>Honors both <em>primary</em> and <em>fallback</em> semantics, and
* otherwise matches against the field name as a <em>fallback qualifier</em>.
* otherwise matches against the field name or parameter name as a <em>fallback
* qualifier</em>.
* @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<String> candidateNames, ResolvableType beanType, @Nullable Field field) {
Set<String> candidateNames, ResolvableType beanType, BeanOverrideHandler handler) {
// Step 0: none or only one
int candidateCount = candidateNames.size();
@ -384,12 +385,10 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, @@ -384,12 +385,10 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
return primaryCandidate;
}
// Step 2: use the field name as a fallback qualifier
if (field != null) {
String fieldName = field.getName();
if (candidateNames.contains(fieldName)) {
return fieldName;
}
// Step 2: use the field name or parameter name as a fallback qualifier
String fieldOrPropertyName = handler.fieldOrParameterName();
if (fieldOrPropertyName != null && candidateNames.contains(fieldOrPropertyName)) {
return fieldOrPropertyName;
}
return null;
@ -445,8 +444,9 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, @@ -445,8 +444,9 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
* whose {@linkplain RootBeanDefinition#getTargetType() target type} and
* {@linkplain RootBeanDefinition#getQualifiedElement() qualified element} are
* the {@linkplain BeanOverrideHandler#getBeanType() bean type} and
* the {@linkplain BeanOverrideHandler#getField() field} of the {@code BeanOverrideHandler},
* respectively.
* the {@linkplain BeanOverrideHandler#getField() field} or
* {@linkplain BeanOverrideHandler#getParameter() parameter} of the
* {@code BeanOverrideHandler}, respectively.
* <p>The returned bean definition should <strong>not</strong> be used to create
* a bean instance but rather only for the purpose of having suitable bean
* definition metadata available in the {@code BeanFactory} &mdash; for example,
@ -462,15 +462,16 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, @@ -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}.
* <p>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, @@ -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) : "");
}
}

104
spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java

@ -17,7 +17,9 @@ @@ -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; @@ -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; @@ -49,7 +54,7 @@ import org.springframework.core.style.ToStringCreator;
* <p>Concrete implementations of {@code BeanOverrideHandler} can store additional
* metadata to use during override {@linkplain #createOverrideInstance instance
* creation} &mdash; 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.
*
* <h3>Singleton Semantics</h3>
*
@ -62,7 +67,7 @@ import org.springframework.core.style.ToStringCreator; @@ -62,7 +67,7 @@ import org.springframework.core.style.ToStringCreator;
* <p>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.
*
* <p>When wrapping a bean created by a
* {@link org.springframework.beans.factory.FactoryBean FactoryBean}, the object
@ -79,6 +84,8 @@ public abstract class BeanOverrideHandler { @@ -79,6 +84,8 @@ public abstract class BeanOverrideHandler {
private final @Nullable Field field;
private final @Nullable Parameter parameter;
private final Set<Annotation> qualifierAnnotations;
private final ResolvableType beanType;
@ -128,8 +135,36 @@ public abstract class BeanOverrideHandler { @@ -128,8 +135,36 @@ public abstract class BeanOverrideHandler {
protected BeanOverrideHandler(@Nullable Field field, ResolvableType beanType, @Nullable String beanName,
String contextName, BeanOverrideStrategy strategy) {
this(field, null, beanType, beanName, contextName, strategy);
}
/**
* Construct a new {@code BeanOverrideHandler} from the supplied values.
* @param parameter the constructor {@link Parameter} annotated with
* {@link BeanOverride @BeanOverride}
* @param beanType the {@linkplain ResolvableType type} of bean to override
* @param beanName the name of the bean to override, or {@code null} to look
* for a single matching bean by type
* @param contextName the name of the context hierarchy level in which the
* handler should be applied, or an empty string to indicate that the handler
* should be applied to all application contexts within a context hierarchy
* @param strategy the {@link BeanOverrideStrategy} to use
* @since 7.1
*/
protected BeanOverrideHandler(Parameter parameter, ResolvableType beanType, @Nullable String beanName,
String contextName, BeanOverrideStrategy strategy) {
this(null, parameter, beanType, beanName, contextName, strategy);
}
private BeanOverrideHandler(@Nullable Field field, @Nullable Parameter parameter, ResolvableType beanType,
@Nullable String beanName, String contextName, BeanOverrideStrategy strategy) {
Assert.state(field == null || parameter == null, "The field and parameter cannot both be non-null");
this.field = field;
this.qualifierAnnotations = getQualifierAnnotations(field);
this.parameter = parameter;
this.qualifierAnnotations = getQualifierAnnotations(field != null ? field : parameter);
this.beanType = beanType;
this.beanName = beanName;
this.strategy = strategy;
@ -156,12 +191,58 @@ public abstract class BeanOverrideHandler { @@ -156,12 +191,58 @@ public abstract class BeanOverrideHandler {
/**
* Get the {@link Field} annotated with {@link BeanOverride @BeanOverride}.
* Get the {@link Field} annotated with {@link BeanOverride @BeanOverride},
* or {@code null} if this handler was not created for a field.
*/
public final @Nullable Field getField() {
return this.field;
}
/**
* Get the constructor {@link Parameter} annotated with {@link BeanOverride @BeanOverride},
* or {@code null} if this handler was not created for a parameter.
* @since 7.1
*/
public final @Nullable Parameter getParameter() {
return this.parameter;
}
final @Nullable AnnotatedElement fieldOrParameter() {
return (this.field != null ? this.field : this.parameter);
}
final @Nullable String fieldOrParameterName() {
if (this.field != null) {
return this.field.getName();
}
if (this.parameter != null) {
return this.parameter.getName();
}
return null;
}
final @Nullable String fieldOrParameterDescription() {
if (this.field != null) {
return "field '%s.%s'".formatted(this.field.getDeclaringClass().getSimpleName(),
this.field.getName());
}
if (this.parameter != null) {
return "parameter '%s' in constructor for %s".formatted(this.parameter.getName(),
this.parameter.getDeclaringExecutable().getName());
}
return null;
}
final @Nullable DependencyDescriptor fieldOrParameterDependencyDescriptor() {
if (this.field != null) {
return new DependencyDescriptor(this.field, true);
}
if (this.parameter != null) {
return new DependencyDescriptor(MethodParameter.forParameter(this.parameter), true);
}
return null;
}
/**
* Get the bean {@linkplain ResolvableType type} to override.
*/
@ -276,24 +357,21 @@ public abstract class BeanOverrideHandler { @@ -276,24 +357,21 @@ public abstract class BeanOverrideHandler {
}
// by-type lookup
if (this.field == null) {
return (that.field == null);
}
return (that.field != null && this.field.getName().equals(that.field.getName()) &&
return (Objects.equals(fieldOrParameterName(), that.fieldOrParameterName()) &&
this.qualifierAnnotations.equals(that.qualifierAnnotations));
}
@Override
public int hashCode() {
int hash = Objects.hash(getClass(), this.beanType.getType(), this.beanName, this.contextName, this.strategy);
return (this.beanName != null ? hash : hash +
Objects.hash((this.field != null ? this.field.getName() : null), this.qualifierAnnotations));
return (this.beanName != null ? hash : hash + Objects.hash(fieldOrParameterName(), this.qualifierAnnotations));
}
@Override
public String toString() {
return new ToStringCreator(this)
.append("field", this.field)
.append("parameter", this.parameter)
.append("beanType", this.beanType)
.append("beanName", this.beanName)
.append("contextName", this.contextName)
@ -302,11 +380,11 @@ public abstract class BeanOverrideHandler { @@ -302,11 +380,11 @@ public abstract class BeanOverrideHandler {
}
private static Set<Annotation> getQualifierAnnotations(@Nullable Field field) {
if (field == null) {
private static Set<Annotation> getQualifierAnnotations(@Nullable AnnotatedElement element) {
if (element == null) {
return Collections.emptySet();
}
Annotation[] candidates = field.getDeclaredAnnotations();
Annotation[] candidates = element.getDeclaredAnnotations();
if (candidates.length == 0) {
return Collections.emptySet();
}

31
spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java

@ -18,9 +18,12 @@ package org.springframework.test.context.bean.override; @@ -18,9 +18,12 @@ package org.springframework.test.context.bean.override;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Parameter;
import java.util.Collections;
import java.util.List;
import org.jspecify.annotations.Nullable;
/**
* Strategy interface for Bean Override processing, which creates
* {@link BeanOverrideHandler} instances that drive how target beans are
@ -51,10 +54,37 @@ public interface BeanOverrideProcessor { @@ -51,10 +54,37 @@ public interface BeanOverrideProcessor {
* @param testClass the test class to process
* @param field the annotated field
* @return the {@code BeanOverrideHandler} that should handle the given field
* @see #createHandler(Annotation, Class, Parameter)
* @see #createHandlers(Annotation, Class)
*/
BeanOverrideHandler createHandler(Annotation overrideAnnotation, Class<?> testClass, Field field);
/**
* Create a {@link BeanOverrideHandler} for the given test class constructor
* parameter.
* <p>This method will only be invoked when a {@link BeanOverride @BeanOverride}
* annotation is declared on a constructor parameter &mdash; for example, if
* the supplied constructor parameter is annotated with {@code @MockitoBean}.
* <p>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 { @@ -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<BeanOverrideHandler> createHandlers(Annotation overrideAnnotation, Class<?> testClass) {
return Collections.emptyList();

123
spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideUtils.java

@ -18,9 +18,12 @@ package org.springframework.test.context.bean.override; @@ -18,9 +18,12 @@ package org.springframework.test.context.bean.override;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
@ -28,7 +31,15 @@ import java.util.Set; @@ -28,7 +31,15 @@ import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import kotlin.jvm.JvmClassMappingKt;
import kotlin.reflect.KClass;
import kotlin.reflect.KFunction;
import kotlin.reflect.full.KClasses;
import kotlin.reflect.jvm.ReflectJvmMapping;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.BeanUtils;
import org.springframework.core.KotlinDetector;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.test.context.TestContextAnnotationUtils;
@ -47,6 +58,8 @@ import static org.springframework.core.annotation.MergedAnnotations.SearchStrate @@ -47,6 +58,8 @@ import static org.springframework.core.annotation.MergedAnnotations.SearchStrate
*/
public abstract class BeanOverrideUtils {
private static final boolean KOTLIN_REFLECT_PRESENT = KotlinDetector.isKotlinReflectPresent();
private static final Comparator<MergedAnnotation<? extends Annotation>> reversedMetaDistance =
Comparator.<MergedAnnotation<? extends Annotation>> comparingInt(MergedAnnotation::getDistance).reversed();
@ -55,8 +68,10 @@ public abstract class BeanOverrideUtils { @@ -55,8 +68,10 @@ 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 its type hierarchy.
* <p>This method does not search the enclosing class hierarchy and does not
* search for {@code @BeanOverride} declarations on classes or interfaces.
* <p>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 { @@ -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.
* <p>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 { @@ -83,7 +99,7 @@ public abstract class BeanOverrideUtils {
private static List<BeanOverrideHandler> findHandlers(Class<?> testClass, boolean localFieldsOnly) {
List<BeanOverrideHandler> handlers = new ArrayList<>();
findHandlers(testClass, testClass, handlers, localFieldsOnly, new HashSet<>());
findHandlers(testClass, testClass, handlers, localFieldsOnly, false, new HashSet<>());
return handlers;
}
@ -97,11 +113,12 @@ public abstract class BeanOverrideUtils { @@ -97,11 +113,12 @@ public abstract class BeanOverrideUtils {
* @param testClass the original test class
* @param handlers the list of handlers found
* @param localFieldsOnly whether to search only on local fields within the type hierarchy
* @param fromNestedClass whether the search originated from a nested test class
* @param visitedTypes the set of types already visited
* @since 6.2.2
*/
private static void findHandlers(Class<?> clazz, Class<?> testClass, List<BeanOverrideHandler> handlers,
boolean localFieldsOnly, Set<Class<?>> visitedTypes) {
boolean localFieldsOnly, boolean fromNestedClass, Set<Class<?>> visitedTypes) {
// 0) Ensure that we do not process the same class or interface multiple times.
if (!visitedTypes.add(clazz)) {
@ -110,26 +127,39 @@ public abstract class BeanOverrideUtils { @@ -110,26 +127,39 @@ public abstract class BeanOverrideUtils {
// 1) Search enclosing class hierarchy.
if (!localFieldsOnly && TestContextAnnotationUtils.searchEnclosingClass(clazz)) {
findHandlers(clazz.getEnclosingClass(), testClass, handlers, localFieldsOnly, visitedTypes);
findHandlers(clazz.getEnclosingClass(), testClass, handlers, localFieldsOnly, true, visitedTypes);
}
// 2) Search class hierarchy.
Class<?> superclass = clazz.getSuperclass();
if (superclass != null && superclass != Object.class) {
findHandlers(superclass, testClass, handlers, localFieldsOnly, visitedTypes);
findHandlers(superclass, testClass, handlers, localFieldsOnly, false, visitedTypes);
}
if (!localFieldsOnly) {
// 3) Search interfaces.
for (Class<?> ifc : clazz.getInterfaces()) {
findHandlers(ifc, testClass, handlers, localFieldsOnly, visitedTypes);
findHandlers(ifc, testClass, handlers, localFieldsOnly, false, visitedTypes);
}
// 4) Process current class.
processClass(clazz, testClass, handlers);
// 5) Process test class constructor parameters.
// Specifically, we only process the constructor for the current test class
// and enclosing test classes. In other words, we do not process constructors
// for superclasses.
if (testClass == clazz || fromNestedClass) {
Constructor<?> constructor = findConstructorWithParameters(clazz);
if (constructor != null) {
for (Parameter parameter : constructor.getParameters()) {
processParameter(parameter, testClass, handlers);
}
}
}
}
// 5) Process fields in current class.
// 6) Process fields in current class.
ReflectionUtils.doWithLocalFields(clazz, field -> processField(field, testClass, handlers));
}
@ -138,7 +168,33 @@ public abstract class BeanOverrideUtils { @@ -138,7 +168,33 @@ public abstract class BeanOverrideUtils {
processor.createHandlers(composedAnnotation, testClass).forEach(handlers::add));
}
private static void processParameter(Parameter parameter, Class<?> testClass, List<BeanOverrideHandler> handlers) {
AtomicBoolean overrideAnnotationFound = new AtomicBoolean();
processElement(parameter, (processor, composedAnnotation) -> {
Assert.state(overrideAnnotationFound.compareAndSet(false, true),
() -> "Multiple @BeanOverride annotations found on parameter: " + parameter);
BeanOverrideHandler handler = processor.createHandler(composedAnnotation, testClass, parameter);
Assert.state(handler != null,
() -> "BeanOverrideProcessor [%s] returned null BeanOverrideHandler for parameter [%s]"
.formatted(processor.getClass().getSimpleName(), parameter));
handlers.add(handler);
});
}
private static void processField(Field field, Class<?> testClass, List<BeanOverrideHandler> handlers) {
Class<?> declaringClass = field.getDeclaringClass();
// For Java records, the Java compiler propagates @BeanOverride annotations from
// canonical constructor parameters to the corresponding component fields, resulting
// in duplicates. Similarly for Kotlin types, the Kotlin compiler propagates
// @BeanOverride annotations from primary constructor parameters to their corresponding
// backing fields, resulting in duplicates. Thus, if we detect either of those scenarios,
// we ignore the field.
if (declaringClass.isRecord() || (KOTLIN_REFLECT_PRESENT &&
KotlinDetector.isKotlinType(declaringClass) &&
KotlinDelegate.isFieldForBeanOverrideConstructorParameter(field))) {
return;
}
AtomicBoolean overrideAnnotationFound = new AtomicBoolean();
processElement(field, (processor, composedAnnotation) -> {
Assert.state(!Modifier.isStatic(field.getModifiers()),
@ -164,4 +220,53 @@ public abstract class BeanOverrideUtils { @@ -164,4 +220,53 @@ public abstract class BeanOverrideUtils {
});
}
/**
* Find a single constructor for the supplied test class that declares parameters.
* <p>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<Constructor<?>> constructors = Arrays.stream(testClass.getDeclaredConstructors())
.filter(constructor -> !constructor.isSynthetic() && constructor.getParameterCount() > 0)
.toList();
return (constructors.size() == 1 ? constructors.get(0) : null);
}
/**
* Inner class to avoid a hard dependency on Kotlin at runtime.
* @since 7.1
*/
private static class KotlinDelegate {
/**
* Determine if the supplied field corresponds to a primary constructor
* parameter in the field's declaring Kotlin class, where the primary
* constructor parameter is annotated with {@link BeanOverride @BeanOverride}.
*/
static boolean isFieldForBeanOverrideConstructorParameter(Field field) {
KClass<?> kClass = JvmClassMappingKt.getKotlinClass(field.getDeclaringClass());
KFunction<?> primaryConstructor = KClasses.getPrimaryConstructor(kClass);
if (primaryConstructor == null) {
return false;
}
Constructor<?> javaConstructor = ReflectJvmMapping.getJavaConstructor(primaryConstructor);
if (javaConstructor == null) {
return false;
}
String fieldName = field.getName();
for (Parameter parameter : javaConstructor.getParameters()) {
if (parameter.getName().equals(fieldName) &&
MergedAnnotations.from(parameter).isPresent(BeanOverride.class)) {
return true;
}
}
return false;
}
}
}

10
spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java

@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
package org.springframework.test.context.bean.override.mockito;
import java.lang.reflect.Field;
import java.lang.reflect.Parameter;
import org.jspecify.annotations.Nullable;
@ -47,6 +48,14 @@ abstract class AbstractMockitoBeanOverrideHandler extends BeanOverrideHandler { @@ -47,6 +48,14 @@ abstract class AbstractMockitoBeanOverrideHandler extends BeanOverrideHandler {
this.reset = (reset != null ? reset : MockReset.AFTER);
}
protected AbstractMockitoBeanOverrideHandler(Parameter parameter, ResolvableType beanType,
@Nullable String beanName, String contextName, BeanOverrideStrategy strategy,
MockReset reset) {
super(parameter, beanType, beanName, contextName, strategy);
this.reset = (reset != null ? reset : MockReset.AFTER);
}
/**
* Return the mock reset mode.
@ -92,6 +101,7 @@ abstract class AbstractMockitoBeanOverrideHandler extends BeanOverrideHandler { @@ -92,6 +101,7 @@ abstract class AbstractMockitoBeanOverrideHandler extends BeanOverrideHandler {
public String toString() {
return new ToStringCreator(this)
.append("field", getField())
.append("parameter", getParameter())
.append("beanType", getBeanType())
.append("beanName", getBeanName())
.append("contextName", getContextName())

26
spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java

@ -41,6 +41,7 @@ import org.springframework.test.context.bean.override.BeanOverride; @@ -41,6 +41,7 @@ import org.springframework.test.context.bean.override.BeanOverride;
* <li>On a non-static field in an enclosing class for a {@code @Nested} test class
* or in any class in the type hierarchy or enclosing class hierarchy above the
* {@code @Nested} test class.</li>
* <li>On a parameter in the constructor for the test class.</li>
* <li>At the type level on a test class or any superclass or implemented interface
* in the type hierarchy above the test class.</li>
* <li>At the type level on an enclosing class for a {@code @Nested} test class
@ -48,13 +49,14 @@ import org.springframework.test.context.bean.override.BeanOverride; @@ -48,13 +49,14 @@ import org.springframework.test.context.bean.override.BeanOverride;
* above the {@code @Nested} test class.</li>
* </ul>
*
* <p>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 <em>fallback
* qualifier</em>. Alternatively, you can explicitly specify a bean name to mock
* by setting the {@link #value() value} or {@link #name() name} attribute.
* <p>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 <em>fallback qualifier</em>. Alternatively, you can explicitly
* specify a bean name to mock by setting the {@link #value() value} or
* {@link #name() name} attribute.
*
* <p>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; @@ -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 { @@ -135,9 +137,9 @@ public @interface MockitoBean {
/**
* Name of the bean to mock.
* <p>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 { @@ -148,7 +150,7 @@ public @interface MockitoBean {
* <p>Defaults to none.
* <p>Each type specified will result in a mock being created and registered
* with the {@code ApplicationContext}.
* <p>Types must be omitted when the annotation is used on a field.
* <p>Types must be omitted when the annotation is used on a field or parameter.
* <p>When {@code @MockitoBean} also defines a {@link #name name}, this attribute
* can only contain a single value.
* @return the types to mock

20
spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java

@ -17,6 +17,7 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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<Class<?>> asClassSet(Class<?>[] classes) {
if (classes.length == 0) {
@ -158,6 +175,7 @@ class MockitoBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler { @@ -158,6 +175,7 @@ class MockitoBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
public String toString() {
return new ToStringCreator(this)
.append("field", getField())
.append("parameter", getParameter())
.append("beanType", getBeanType())
.append("beanName", getBeanName())
.append("contextName", getContextName())

21
spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java

@ -18,9 +18,12 @@ package org.springframework.test.context.bean.override.mockito; @@ -18,9 +18,12 @@ 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.ArrayList;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.core.ResolvableType;
import org.springframework.test.context.bean.override.BeanOverrideHandler;
import org.springframework.test.context.bean.override.BeanOverrideProcessor;
@ -56,6 +59,24 @@ class MockitoBeanOverrideProcessor implements BeanOverrideProcessor { @@ -56,6 +59,24 @@ class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
.formatted(field.getDeclaringClass().getName(), field.getName()));
}
@Override
public @Nullable BeanOverrideHandler createHandler(Annotation overrideAnnotation, Class<?> testClass, Parameter parameter) {
if (overrideAnnotation instanceof MockitoBean mockitoBean) {
Assert.state(mockitoBean.types().length == 0,
"The @MockitoBean 'types' attribute must be omitted when declared on a parameter");
return new MockitoBeanOverrideHandler(parameter, ResolvableType.forParameter(parameter), mockitoBean);
}
else if (overrideAnnotation instanceof MockitoSpyBean mockitoSpyBean) {
Assert.state(mockitoSpyBean.types().length == 0,
"The @MockitoSpyBean 'types' attribute must be omitted when declared on a parameter");
return new MockitoSpyBeanOverrideHandler(parameter, ResolvableType.forParameter(parameter), mockitoSpyBean);
}
throw new IllegalStateException("""
Invalid annotation passed to MockitoBeanOverrideProcessor: \
expected either @MockitoBean or @MockitoSpyBean on parameter '%s' in constructor %s"""
.formatted(parameter.getName(), parameter.getDeclaringExecutable().getName()));
}
@Override
public List<BeanOverrideHandler> createHandlers(Annotation overrideAnnotation, Class<?> testClass) {
if (overrideAnnotation instanceof MockitoBean mockitoBean) {

30
spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java

@ -38,6 +38,7 @@ import org.springframework.test.context.bean.override.BeanOverride; @@ -38,6 +38,7 @@ import org.springframework.test.context.bean.override.BeanOverride;
* <li>On a non-static field in an enclosing class for a {@code @Nested} test class
* or in any class in the type hierarchy or enclosing class hierarchy above the
* {@code @Nested} test class.</li>
* <li>On a parameter in the constructor for the test class.</li>
* <li>At the type level on a test class or any superclass or implemented interface
* in the type hierarchy above the test class.</li>
* <li>At the type level on an enclosing class for a {@code @Nested} test class
@ -45,15 +46,16 @@ import org.springframework.test.context.bean.override.BeanOverride; @@ -45,15 +46,16 @@ import org.springframework.test.context.bean.override.BeanOverride;
* above the {@code @Nested} test class.</li>
* </ul>
*
* <p>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 <em>fallback
* qualifier</em>. 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.
* <p>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 <em>fallback qualifier</em>. 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.
*
* <p>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; @@ -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 { @@ -142,9 +144,9 @@ public @interface MockitoSpyBean {
/**
* Name of the bean to spy.
* <p>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 { @@ -155,7 +157,7 @@ public @interface MockitoSpyBean {
* <p>Defaults to none.
* <p>Each type specified will result in a spy being created and registered
* with the {@code ApplicationContext}.
* <p>Types must be omitted when the annotation is used on a field.
* <p>Types must be omitted when the annotation is used on a field or parameter.
* <p>When {@code @MockitoSpyBean} also defines a {@link #name name}, this
* attribute can only contain a single value.
* @return the types to spy

9
spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java

@ -17,6 +17,7 @@ @@ -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 { @@ -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 { @@ -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,

26
spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java

@ -24,6 +24,8 @@ import java.lang.reflect.Parameter; @@ -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; @@ -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 @@ -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)}.</li>
* <li>The parameter is of type {@link ApplicationContext} or a sub-type thereof.</li>
* <li>The parameter is annotated or meta-annotated with a
* {@link BeanOverride @BeanOverride} composed annotation &mdash; for example,
* {@code @MockitoBean} or {@code @MockitoSpyBean}.</li>
* <li>The parameter is of type {@link ApplicationEvents} or a sub-type thereof.</li>
* <li>{@link ParameterResolutionDelegate#isAutowirable} returns {@code true}.</li>
* </ol>
@ -396,6 +404,7 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes @@ -396,6 +404,7 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes
extensionContext.getConfigurationParameter(propertyName).orElse(null);
return (TestConstructorUtils.isAutowirableConstructor(executable, junitPropertyProvider) ||
ApplicationContext.class.isAssignableFrom(parameterType) ||
isBeanOverride(parameter) ||
supportsApplicationEvents(parameterType, executable) ||
ParameterResolutionDelegate.isAutowirable(parameter, parameterContext.getIndex()));
}
@ -414,7 +423,7 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes @@ -414,7 +423,7 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes
* retrieving the corresponding dependency from the test's {@link ApplicationContext}.
* <p>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 @@ -428,6 +437,16 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes
}
ApplicationContext applicationContext = getApplicationContext(extensionContext);
if (isBeanOverride(parameter)) {
Optional<String> beanName = BeanOverrideUtils.findAllHandlers(testClass).stream()
.filter(handler -> parameter.equals(handler.getParameter()))
.map(BeanOverrideHandler::getBeanName)
.filter(Objects::nonNull)
.findFirst();
if (beanName.isPresent()) {
return applicationContext.getBean(beanName.get());
}
}
return ParameterResolutionDelegate.resolveDependency(parameter, index, testClass,
applicationContext.getAutowireCapableBeanFactory());
}
@ -495,6 +514,11 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes @@ -495,6 +514,11 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes
return false;
}
private static boolean isBeanOverride(Parameter parameter) {
return (parameter.getDeclaringExecutable() instanceof Constructor &&
MergedAnnotations.from(parameter).isPresent(BeanOverride.class));
}
/**
* Find the properly scoped {@link ExtensionContext} for the supplied test class.
* <p>If the supplied {@code ExtensionContext} is already properly scoped, it

2
spring-test/src/test/java/org/springframework/test/context/bean/override/example/CustomQualifier.java

@ -24,7 +24,7 @@ import java.lang.annotation.Target; @@ -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

70
spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanConfigurationErrorTests.java

@ -88,6 +88,65 @@ class MockitoBeanConfigurationErrorTests { @@ -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 { @@ -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) {
}
}
}

2
spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java

@ -194,7 +194,7 @@ class MockitoBeanOverrideHandlerTests { @@ -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);
}

76
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; @@ -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 { @@ -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 {

55
spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanConfigurationErrorTests.java

@ -113,6 +113,50 @@ class MockitoSpyBeanConfigurationErrorTests { @@ -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 { @@ -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 {

175
spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoBeanByNameLookupForConstructorParametersIntegrationTests.java

@ -0,0 +1,175 @@ @@ -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 <a href="https://github.com/spring-projects/spring-framework/issues/36096">gh-36096</a>
* @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");
}
}
}

55
spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoBeanByTypeLookupForConstructorParametersIntegrationRecordTests.java

@ -0,0 +1,55 @@ @@ -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 <a href="https://github.com/spring-projects/spring-framework/issues/36096">gh-36096</a>
*/
@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);
}
}

207
spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoBeanByTypeLookupForConstructorParametersIntegrationTests.java

@ -0,0 +1,207 @@ @@ -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 <a href="https://github.com/spring-projects/spring-framework/issues/36096">gh-36096</a>
* @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");
}
}
}

165
spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoSpyBeanByNameLookupForConstructorParametersIntegrationTests.java

@ -0,0 +1,165 @@ @@ -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 <a href="https://github.com/spring-projects/spring-framework/issues/36096">gh-36096</a>
* @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");
}
}
}

213
spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/constructor/MockitoSpyBeanByTypeLookupForConstructorParametersIntegrationTests.java

@ -0,0 +1,213 @@ @@ -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 <a href="https://github.com/spring-projects/spring-framework/issues/36096">gh-36096</a>
* @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";
}
};
}
}
}

20
spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/ConstructorService01.java

@ -0,0 +1,20 @@ @@ -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 {
}

29
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 @@ -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 { @@ -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 { @@ -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";

12
spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoBeansByTypeIntegrationTests.java

@ -63,10 +63,17 @@ class MockitoBeansByTypeIntegrationTests implements MockTestInterface01 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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");

6
spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/typelevel/MockitoBeansTests.java

@ -44,7 +44,7 @@ class MockitoBeansTests { @@ -44,7 +44,7 @@ class MockitoBeansTests {
Stream<Class<?>> mockedServices = getRegisteredMockTypes(MockitoBeansByTypeIntegrationTests.class);
assertThat(mockedServices).containsExactly(
Service01.class, Service02.class, Service03.class, Service04.class,
Service05.class, Service06.class, Service07.class);
Service05.class, Service06.class, ConstructorService01.class, Service07.class);
}
@Test
@ -52,8 +52,8 @@ class MockitoBeansTests { @@ -52,8 +52,8 @@ class MockitoBeansTests {
Stream<Class<?>> mockedServices = getRegisteredMockTypes(MockitoBeansByTypeIntegrationTests.NestedTests.class);
assertThat(mockedServices).containsExactly(
Service01.class, Service02.class, Service03.class, Service04.class,
Service05.class, Service06.class, Service07.class, Service08.class,
Service09.class, Service10.class, Service11.class, Service12.class,
Service05.class, Service06.class, ConstructorService01.class, Service07.class,
Service08.class, Service09.class, Service10.class, Service11.class, Service12.class,
Service13.class);
}

44
spring-test/src/test/kotlin/org/springframework/test/context/bean/override/mockito/MockitoBeanByTypeLookupForConstructorParametersIntegrationKotlinDataClassTests.kt

@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
/*
* 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
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 org.springframework.test.mockito.MockitoAssertions.assertIsMock
/**
* Integration tests for [@MockitoBean][MockitoBean] that use by-type lookup
* on constructor parameters in a Kotlin data class.
*
* @author Sam Brannen
* @since 7.1
* @see <a href="https://github.com/spring-projects/spring-framework/issues/36096">gh-36096</a>
* @see MockitoBeanByTypeLookupForConstructorParametersIntegrationKotlinTests
*/
@SpringJUnitConfig
data class MockitoBeanByTypeLookupForConstructorParametersIntegrationKotlinDataClassTests(
@MockitoBean val exampleService: ExampleService) {
@Test
fun test() {
assertIsMock(exampleService)
}
}

44
spring-test/src/test/kotlin/org/springframework/test/context/bean/override/mockito/MockitoBeanByTypeLookupForConstructorParametersIntegrationKotlinTests.kt

@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
/*
* 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
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 org.springframework.test.mockito.MockitoAssertions.assertIsMock
/**
* Integration tests for [@MockitoBean][MockitoBean] that use by-type lookup
* on constructor parameters in Kotlin.
*
* @author Sam Brannen
* @since 7.1
* @see <a href="https://github.com/spring-projects/spring-framework/issues/36096">gh-36096</a>
* @see org.springframework.test.context.bean.override.mockito.MockitoBeanByNameLookupTestMethodScopedExtensionContextIntegrationTests
*/
@SpringJUnitConfig
class MockitoBeanByTypeLookupForConstructorParametersIntegrationKotlinTests(
@MockitoBean val exampleService: ExampleService) {
@Test
fun test() {
assertIsMock(exampleService)
}
}
Loading…
Cancel
Save