Merge branch '6.2.x'

This commit is contained in:
Sam Brannen 2025-02-12 15:55:33 +01:00
commit 8a53525209
37 changed files with 1106 additions and 208 deletions

View File

@ -1,34 +1,60 @@
[[spring-testing-annotation-beanoverriding-mockitobean]] [[spring-testing-annotation-beanoverriding-mockitobean]]
= `@MockitoBean` and `@MockitoSpyBean` = `@MockitoBean` and `@MockitoSpyBean`
`@MockitoBean` and `@MockitoSpyBean` are used on non-static fields in test classes to `@MockitoBean` and `@MockitoSpyBean` can be used in test classes to override a bean in
override beans in the test's `ApplicationContext` with a Mockito _mock_ or _spy_, the test's `ApplicationContext` with a Mockito _mock_ or _spy_, respectively. In the
respectively. In the latter case, an early instance of the original bean is captured and latter case, an early instance of the original bean is captured and wrapped by the spy.
wrapped by the spy.
By default, the annotated field's type is used to search for candidate beans to override. The annotations can be applied in the following ways.
If multiple candidates match, `@Qualifier` can be provided to narrow the candidate to
override. Alternatively, a candidate whose bean name matches the name of the field will * On a non-static field in a test class or any of its superclasses.
match. * 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.
* 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 at the type level, the type of bean
(or beans) to mock or spy must be supplied via the `types` attribute in the annotation
for example, `@MockitoBean(types = {OrderService.class, UserService.class})`. If multiple
candidates exist in the `ApplicationContext`, you can explicitly specify a bean name to
mock or spy by setting the `name` attribute. Note, however, that the `types` attribute
must contain a single type if an explicit bean `name` is configured for example,
`@MockitoBean(name = "ps1", types = PrintingService.class)`.
To support reuse of mock configuration, `@MockitoBean` and `@MockitoSpyBean` may be used
as meta-annotations to create custom _composed annotations_ for example, to define
common mock or spy configuration in a single annotation that can be reused across a test
suite. `@MockitoBean` and `@MockitoSpyBean` can also be used as repeatable annotations at
the type level — for example, to mock or spy several beans by name.
[WARNING] [WARNING]
==== ====
Qualifiers, including the name of the field, are used to determine if a separate Qualifiers, including the name of a field, are used to determine if a separate
`ApplicationContext` needs to be created. If you are using this feature to mock or spy `ApplicationContext` needs to be created. If you are using this feature to mock or spy
the same bean in several test classes, make sure to name the field consistently to avoid the same bean in several test classes, make sure to name the fields consistently to avoid
creating unnecessary contexts. creating unnecessary contexts.
==== ====
Each annotation also defines Mockito-specific attributes to fine-tune the mocking behavior. Each annotation also defines Mockito-specific attributes to fine-tune the mocking behavior.
The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE` The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE`
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding]. xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy for bean overrides].
If no existing bean matches, a new bean is created on the fly. However, you can switch to If a corresponding bean does not exist, a new bean will be created. However, you can
the `REPLACE` strategy by setting the `enforceOverride` attribute to `true`. See the switch to the `REPLACE` strategy by setting the `enforceOverride` attribute to `true`
following section for an example. for example, `@MockitoBean(enforceOverride = true)`.
The `@MockitoSpyBean` annotation uses the `WRAP` The `@MockitoSpyBean` annotation uses the `WRAP`
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy], xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy],
and the original instance is wrapped in a Mockito spy. This strategy requires that and the original instance is wrapped in a Mockito spy. This strategy requires that
exactly one candidate bean exists. exactly one candidate bean exists.
@ -56,15 +82,8 @@ or `private` depending on the needs or coding practices of the project.
[[spring-testing-annotation-beanoverriding-mockitobean-examples]] [[spring-testing-annotation-beanoverriding-mockitobean-examples]]
== `@MockitoBean` Examples == `@MockitoBean` Examples
When using `@MockitoBean`, a new bean will be created if a corresponding bean does not The following example shows how to use the default behavior of the `@MockitoBean`
exist. However, if you would like for the test to fail when a corresponding bean does not annotation.
exist, you can set the `enforceOverride` attribute to `true` for example,
`@MockitoBean(enforceOverride = true)`.
To use a by-name override rather than a by-type override, specify the `name` (or `value`)
attribute of the annotation.
The following example shows how to use the default behavior of the `@MockitoBean` annotation:
[tabs] [tabs]
====== ======
@ -81,7 +100,7 @@ Java::
// tests... // tests...
} }
---- ----
<1> Replace the bean with type `CustomService` with a Mockito `mock`. <1> Replace the bean with type `CustomService` with a Mockito mock.
====== ======
In the example above, we are creating a mock for `CustomService`. If more than one bean In the example above, we are creating a mock for `CustomService`. If more than one bean
@ -90,7 +109,8 @@ will fail, and you will need to provide a qualifier of some sort to identify whi
`CustomService` beans you want to override. If no such bean exists, a bean will be `CustomService` beans you want to override. If no such bean exists, a bean will be
created with an auto-generated bean name. created with an auto-generated bean name.
The following example uses a by-name lookup, rather than a by-type lookup: The following example uses a by-name lookup, rather than a by-type lookup. If no bean
named `service` exists, one is created.
[tabs] [tabs]
====== ======
@ -108,32 +128,9 @@ Java::
} }
---- ----
<1> Replace the bean named `service` with a Mockito `mock`. <1> Replace the bean named `service` with a Mockito mock.
====== ======
If no bean named `service` exists, one is created.
`@MockitoBean` can also be used at the type level:
- on a test class or any superclass or implemented interface in the type hierarchy above
the test class
- 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` is declared at the type level, the type of bean (or beans) to mock
must be supplied via the `types` attribute for example,
`@MockitoBean(types = {OrderService.class, UserService.class})`. If multiple candidates
exist in the application context, you can explicitly specify a bean name to mock by
setting the `name` attribute. Note, however, that the `types` attribute must contain a
single type if an explicit bean `name` is configured for example,
`@MockitoBean(name = "ps1", types = PrintingService.class)`.
To support reuse of mock configuration, `@MockitoBean` may be used as a meta-annotation
to create custom _composed annotations_ — for example, to define common mock
configuration in a single annotation that can be reused across a test suite.
`@MockitoBean` can also be used as a repeatable annotation at the type level — for
example, to mock several beans by name.
The following `@SharedMocks` annotation registers two mocks by-type and one mock by-name. The following `@SharedMocks` annotation registers two mocks by-type and one mock by-name.
[tabs] [tabs]
@ -191,7 +188,7 @@ APIs.
== `@MockitoSpyBean` Examples == `@MockitoSpyBean` Examples
The following example shows how to use the default behavior of the `@MockitoSpyBean` The following example shows how to use the default behavior of the `@MockitoSpyBean`
annotation: annotation.
[tabs] [tabs]
====== ======
@ -208,7 +205,7 @@ Java::
// tests... // tests...
} }
---- ----
<1> Wrap the bean with type `CustomService` with a Mockito `spy`. <1> Wrap the bean with type `CustomService` with a Mockito spy.
====== ======
In the example above, we are wrapping the bean with type `CustomService`. If more than In the example above, we are wrapping the bean with type `CustomService`. If more than
@ -216,7 +213,7 @@ one bean of that type exists, the bean named `customService` is considered. Othe
the test will fail, and you will need to provide a qualifier of some sort to identify the test will fail, and you will need to provide a qualifier of some sort to identify
which of the `CustomService` beans you want to spy. which of the `CustomService` beans you want to spy.
The following example uses a by-name lookup, rather than a by-type lookup: The following example uses a by-name lookup, rather than a by-type lookup.
[tabs] [tabs]
====== ======
@ -233,5 +230,58 @@ Java::
// tests... // tests...
} }
---- ----
<1> Wrap the bean named `service` with a Mockito `spy`. <1> Wrap the bean named `service` with a Mockito spy.
====== ======
The following `@SharedSpies` annotation registers two spies by-type and one spy by-name.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@MockitoSpyBean(types = {OrderService.class, UserService.class}) // <1>
@MockitoSpyBean(name = "ps1", types = PrintingService.class) // <2>
public @interface SharedSpies {
}
----
<1> Register `OrderService` and `UserService` spies by-type.
<2> Register `PrintingService` spy by-name.
======
The following demonstrates how `@SharedSpies` can be used on a test class.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig(TestConfig.class)
@SharedSpies // <1>
class BeanOverrideTests {
@Autowired OrderService orderService; // <2>
@Autowired UserService userService; // <2>
@Autowired PrintingService ps1; // <2>
// Inject other components that rely on the spies.
@Test
void testThatDependsOnMocks() {
// ...
}
}
----
<1> Register common spies via the custom `@SharedSpies` annotation.
<2> Optionally inject spies to _stub_ or _verify_ them.
======
TIP: The spies can also be injected into `@Configuration` classes or other test-related
components in the `ApplicationContext` in order to configure them with Mockito's stubbing
APIs.

View File

@ -2,8 +2,8 @@
= Bean Overriding in Tests = Bean Overriding in Tests
Bean overriding in tests refers to the ability to override specific beans in the Bean overriding in tests refers to the ability to override specific beans in the
`ApplicationContext` for a test class, by annotating one or more non-static fields in the `ApplicationContext` for a test class, by annotating the test class or one or more
test class. non-static fields in the test class.
NOTE: This feature is intended as a less risky alternative to the practice of registering NOTE: This feature is intended as a less risky alternative to the practice of registering
a bean via `@Bean` with the `DefaultListableBeanFactory` a bean via `@Bean` with the `DefaultListableBeanFactory`
@ -42,15 +42,16 @@ 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` {spring-framework-code}/spring-test/src/main/resources/META-INF/spring.factories[`META-INF/spring.factories`
properties file]. properties file].
The bean overriding infrastructure searches in test classes for any non-static field that The bean overriding infrastructure searches for annotations on test classes as well as
is meta-annotated with `@BeanOverride` and instantiates the corresponding annotations on non-static fields in test classes that are meta-annotated with
`BeanOverrideProcessor` which is responsible for creating an appropriate `@BeanOverride` and instantiates the corresponding `BeanOverrideProcessor` which is
`BeanOverrideHandler`. responsible for creating an appropriate `BeanOverrideHandler`.
The internal `BeanOverrideBeanFactoryPostProcessor` then uses bean override handlers to The internal `BeanOverrideBeanFactoryPostProcessor` then uses bean override handlers to
alter the test's `ApplicationContext` by creating, replacing, or wrapping beans as alter the test's `ApplicationContext` by creating, replacing, or wrapping beans as
defined by the corresponding `BeanOverrideStrategy`: defined by the corresponding `BeanOverrideStrategy`:
[[testcontext-bean-overriding-strategy]]
`REPLACE`:: `REPLACE`::
Replaces the bean. Throws an exception if a corresponding bean does not exist. Replaces the bean. Throws an exception if a corresponding bean does not exist.
`REPLACE_OR_CREATE`:: `REPLACE_OR_CREATE`::

View File

@ -31,9 +31,9 @@ import org.springframework.test.context.bean.override.BeanOverride;
/** /**
* {@code @MockitoBean} is an annotation that can be used in test classes to * {@code @MockitoBean} is an annotation that can be used in test classes to
* override beans in a test's * override a bean in the test's
* {@link org.springframework.context.ApplicationContext ApplicationContext} * {@link org.springframework.context.ApplicationContext ApplicationContext}
* using Mockito mocks. * with a Mockito mock.
* *
* <p>{@code @MockitoBean} can be applied in the following ways. * <p>{@code @MockitoBean} can be applied in the following ways.
* <ul> * <ul>
@ -49,18 +49,19 @@ import org.springframework.test.context.bean.override.BeanOverride;
* </ul> * </ul>
* *
* <p>When {@code @MockitoBean} is declared on a field, the bean to mock is inferred * <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, a * from the type of the annotated field. If multiple candidates exist in the
* {@code @Qualifier} annotation can be declared on the field to help disambiguate. * {@code ApplicationContext}, a {@code @Qualifier} annotation can be declared
* In the absence of a {@code @Qualifier} annotation, the name of the annotated * on the field to help disambiguate. In the absence of a {@code @Qualifier}
* field will be used as a fallback qualifier. Alternatively, you can explicitly * annotation, the name of the annotated field will be used as a <em>fallback
* specify a bean name to mock by setting the {@link #value() value} or * qualifier</em>. Alternatively, you can explicitly specify a bean name to mock
* {@link #name() name} attribute. * 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 * <p>When {@code @MockitoBean} is declared at the type level, the type of bean
* to mock must be supplied via the {@link #types() types} attribute. If multiple * (or beans) to mock must be supplied via the {@link #types() types} attribute.
* candidates exist, you can explicitly specify a bean name to mock by setting the * If multiple candidates exist in the {@code ApplicationContext}, you can
* {@link #name() name} attribute. Note, however, that the {@code types} attribute * explicitly specify a bean name to mock by setting the {@link #name() name}
* must contain a single type if an explicit bean {@code name} is configured. * attribute. Note, however, that the {@code types} attribute must contain a
* single type if an explicit bean {@code name} is configured.
* *
* <p>A bean will be created if a corresponding bean does not exist. However, if * <p>A bean will be created if a corresponding bean does not exist. However, if
* you would like for the test to fail when a corresponding bean does not exist, * you would like for the test to fail when a corresponding bean does not exist,
@ -111,7 +112,7 @@ import org.springframework.test.context.bean.override.BeanOverride;
public @interface MockitoBean { public @interface MockitoBean {
/** /**
* Alias for {@link #name()}. * Alias for {@link #name() name}.
* <p>Intended to be used when no other attributes are needed &mdash; for * <p>Intended to be used when no other attributes are needed &mdash; for
* example, {@code @MockitoBean("customBeanName")}. * example, {@code @MockitoBean("customBeanName")}.
* @see #name() * @see #name()
@ -136,7 +137,7 @@ public @interface MockitoBean {
* <p>Each type specified will result in a mock being created and registered * <p>Each type specified will result in a mock being created and registered
* with the {@code ApplicationContext}. * 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.
* <p>When {@code @MockitoBean} also defines a {@link #name}, this attribute * <p>When {@code @MockitoBean} also defines a {@link #name name}, this attribute
* can only contain a single value. * can only contain a single value.
* @return the types to mock * @return the types to mock
* @since 6.2.2 * @since 6.2.2

View File

@ -45,8 +45,10 @@ class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
"The @MockitoBean 'types' attribute must be omitted when declared on a field"); "The @MockitoBean 'types' attribute must be omitted when declared on a field");
return new MockitoBeanOverrideHandler(field, ResolvableType.forField(field, testClass), mockitoBean); return new MockitoBeanOverrideHandler(field, ResolvableType.forField(field, testClass), mockitoBean);
} }
else if (overrideAnnotation instanceof MockitoSpyBean spyBean) { else if (overrideAnnotation instanceof MockitoSpyBean mockitoSpyBean) {
return new MockitoSpyBeanOverrideHandler(field, ResolvableType.forField(field, testClass), spyBean); Assert.state(mockitoSpyBean.types().length == 0,
"The @MockitoSpyBean 'types' attribute must be omitted when declared on a field");
return new MockitoSpyBeanOverrideHandler(field, ResolvableType.forField(field, testClass), mockitoSpyBean);
} }
throw new IllegalStateException(""" throw new IllegalStateException("""
Invalid annotation passed to MockitoBeanOverrideProcessor: \ Invalid annotation passed to MockitoBeanOverrideProcessor: \
@ -56,11 +58,7 @@ class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
@Override @Override
public List<BeanOverrideHandler> createHandlers(Annotation overrideAnnotation, Class<?> testClass) { public List<BeanOverrideHandler> createHandlers(Annotation overrideAnnotation, Class<?> testClass) {
if (!(overrideAnnotation instanceof MockitoBean mockitoBean)) { if (overrideAnnotation instanceof MockitoBean mockitoBean) {
throw new IllegalStateException("""
Invalid annotation passed to MockitoBeanOverrideProcessor: \
expected @MockitoBean on test class """ + testClass.getName());
}
Class<?>[] types = mockitoBean.types(); Class<?>[] types = mockitoBean.types();
Assert.state(types.length > 0, Assert.state(types.length > 0,
"The @MockitoBean 'types' attribute must not be empty when declared on a class"); "The @MockitoBean 'types' attribute must not be empty when declared on a class");
@ -72,5 +70,22 @@ class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
} }
return handlers; return handlers;
} }
else if (overrideAnnotation instanceof MockitoSpyBean mockitoSpyBean) {
Class<?>[] types = mockitoSpyBean.types();
Assert.state(types.length > 0,
"The @MockitoSpyBean 'types' attribute must not be empty when declared on a class");
Assert.state(mockitoSpyBean.name().isEmpty() || types.length == 1,
"The @MockitoSpyBean 'name' attribute cannot be used when mocking multiple types");
List<BeanOverrideHandler> handlers = new ArrayList<>();
for (Class<?> type : types) {
handlers.add(new MockitoSpyBeanOverrideHandler(ResolvableType.forClass(type), mockitoSpyBean));
}
return handlers;
}
throw new IllegalStateException("""
Invalid annotation passed to MockitoBeanOverrideProcessor: \
expected either @MockitoBean or @MockitoSpyBean on test class %s"""
.formatted(testClass.getName()));
}
} }

View File

@ -18,6 +18,7 @@ package org.springframework.test.context.bean.override.mockito;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
@ -26,19 +27,40 @@ import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.bean.override.BeanOverride; import org.springframework.test.context.bean.override.BeanOverride;
/** /**
* {@code @MockitoSpyBean} is an annotation that can be applied to a non-static * {@code @MockitoSpyBean} is an annotation that can be used in test classes to
* field in a test class to override a bean in the test's * override a bean in the test's
* {@link org.springframework.context.ApplicationContext ApplicationContext} * {@link org.springframework.context.ApplicationContext ApplicationContext}
* with a Mockito spy that wraps the original bean instance. * with a Mockito spy that wraps the original bean instance.
* *
* <p>By default, the bean to spy is inferred from the type of the annotated * <p>{@code @MockitoSpyBean} can be applied in the following ways.
* field. If multiple candidates exist, a {@code @Qualifier} annotation can be * <ul>
* used to help disambiguate. In the absence of a {@code @Qualifier} annotation, * <li>On a non-static field in a test class or any of its superclasses.</li>
* the name of the annotated field will be used as a fallback qualifier. * <li>On a non-static field in an enclosing class for a {@code @Nested} test class
* Alternatively, you can explicitly specify a bean name to spy by setting the * or in any class in the type hierarchy or enclosing class hierarchy above the
* {@link #value() value} or {@link #name() name} attribute. If a bean name is * {@code @Nested} test class.</li>
* specified, it is required that a target bean with that name has been previously * <li>At the type level on a test class or any superclass or implemented interface
* registered in the application context. * in the type hierarchy above the test class.</li>
* <li>At the type level on an enclosing class for a {@code @Nested} test class
* or on any class or interface in the type hierarchy or enclosing class hierarchy
* 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 at the type level, the type of bean
* (or beans) to spy must be supplied via the {@link #types() types} attribute.
* If multiple candidates exist in the {@code ApplicationContext}, you can
* explicitly specify a bean name to spy by setting the {@link #name() name}
* attribute. Note, however, that the {@code types} attribute must contain a
* single type if an explicit bean {@code name} is configured.
* *
* <p>A spy cannot be created for components which are known to the application * <p>A spy cannot be created for components which are known to the application
* context but are not beans &mdash; for example, components * context but are not beans &mdash; for example, components
@ -56,24 +78,33 @@ import org.springframework.test.context.bean.override.BeanOverride;
* (default visibility), or {@code private} depending on the needs or coding * (default visibility), or {@code private} depending on the needs or coding
* practices of the project. * practices of the project.
* *
* <p>{@code @MockitoSpyBean} fields will be inherited from an enclosing test class by default. * <p>{@code @MockitoSpyBean} fields and type-level {@code @MockitoSpyBean} declarations
* See {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} * will be inherited from an enclosing test class by default. See
* {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
* for details. * for details.
* *
* <p>{@code @MockitoSpyBean} may be used as a <em>meta-annotation</em> to create
* custom <em>composed annotations</em> &mdash; for example, to define common spy
* configuration in a single annotation that can be reused across a test suite.
* {@code @MockitoSpyBean} can also be used as a <em>{@linkplain Repeatable repeatable}</em>
* annotation at the type level &mdash; for example, to spy on several beans by
* {@link #name() name}.
*
* @author Simon Baslé * @author Simon Baslé
* @author Sam Brannen * @author Sam Brannen
* @since 6.2 * @since 6.2
* @see org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean * @see org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean
* @see org.springframework.test.context.bean.override.convention.TestBean @TestBean * @see org.springframework.test.context.bean.override.convention.TestBean @TestBean
*/ */
@Target(ElementType.FIELD) @Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Documented @Documented
@Repeatable(MockitoSpyBeans.class)
@BeanOverride(MockitoBeanOverrideProcessor.class) @BeanOverride(MockitoBeanOverrideProcessor.class)
public @interface MockitoSpyBean { public @interface MockitoSpyBean {
/** /**
* Alias for {@link #name()}. * Alias for {@link #name() name}.
* <p>Intended to be used when no other attributes are needed &mdash; for * <p>Intended to be used when no other attributes are needed &mdash; for
* example, {@code @MockitoSpyBean("customBeanName")}. * example, {@code @MockitoSpyBean("customBeanName")}.
* @see #name() * @see #name()
@ -84,13 +115,27 @@ public @interface MockitoSpyBean {
/** /**
* Name of the bean to spy. * Name of the bean to spy.
* <p>If left unspecified, the bean to spy is selected according to the * <p>If left unspecified, the bean to spy is selected according to the
* annotated field's type, taking qualifiers into account if necessary. See * configured {@link #types() types} or the annotated field's type, taking
* the {@linkplain MockitoSpyBean class-level documentation} for details. * qualifiers into account if necessary. See the {@linkplain MockitoSpyBean
* class-level documentation} for details.
* @see #value() * @see #value()
*/ */
@AliasFor("value") @AliasFor("value")
String name() default ""; String name() default "";
/**
* One or more types to spy.
* <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>When {@code @MockitoSpyBean} also defines a {@link #name name}, this
* attribute can only contain a single value.
* @return the types to spy
* @since 6.2.3
*/
Class<?>[] types() default {};
/** /**
* The reset mode to apply to the spied bean. * The reset mode to apply to the spied bean.
* <p>The default is {@link MockReset#AFTER} meaning that spies are automatically * <p>The default is {@link MockReset#AFTER} meaning that spies are automatically

View File

@ -48,7 +48,11 @@ class MockitoSpyBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
new SpringAopBypassingVerificationStartedListener(); new SpringAopBypassingVerificationStartedListener();
MockitoSpyBeanOverrideHandler(Field field, ResolvableType typeToSpy, MockitoSpyBean spyBean) { MockitoSpyBeanOverrideHandler(ResolvableType typeToSpy, MockitoSpyBean spyBean) {
this(null, typeToSpy, spyBean);
}
MockitoSpyBeanOverrideHandler(@Nullable Field field, ResolvableType typeToSpy, MockitoSpyBean spyBean) {
super(field, typeToSpy, (StringUtils.hasText(spyBean.name()) ? spyBean.name() : null), super(field, typeToSpy, (StringUtils.hasText(spyBean.name()) ? spyBean.name() : null),
BeanOverrideStrategy.WRAP, spyBean.reset()); BeanOverrideStrategy.WRAP, spyBean.reset());
Assert.notNull(typeToSpy, "typeToSpy must not be null"); Assert.notNull(typeToSpy, "typeToSpy must not be null");

View File

@ -0,0 +1,41 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.bean.override.mockito;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Container for {@link MockitoSpyBean @MockitoSpyBean} annotations which allows
* {@code @MockitoSpyBean} to be used as a {@linkplain java.lang.annotation.Repeatable
* repeatable annotation} at the type level &mdash; for example, on test classes
* or interfaces implemented by test classes.
*
* @author Sam Brannen
* @since 6.2.3
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MockitoSpyBeans {
MockitoSpyBean[] value();
}

View File

@ -105,6 +105,19 @@ class MockitoBeanOverrideProcessorTests {
@Nested @Nested
class CreateHandlersTests { class CreateHandlersTests {
@Test
void otherAnnotationThrows() {
Annotation annotation = getClass().getAnnotation(Nested.class);
assertThatIllegalStateException()
.isThrownBy(() -> processor.createHandlers(annotation, getClass()))
.withMessage("Invalid annotation passed to MockitoBeanOverrideProcessor: expected either " +
"@MockitoBean or @MockitoSpyBean on test class %s", getClass().getName());
}
@Nested
class MockitoBeanTests {
@Test @Test
void missingTypes() { void missingTypes() {
Class<?> testClass = MissingTypesTestCase.class; Class<?> testClass = MissingTypesTestCase.class;
@ -193,4 +206,97 @@ class MockitoBeanOverrideProcessorTests {
} }
} }
@Nested
class MockitoSpyBeanTests {
@Test
void missingTypes() {
Class<?> testClass = MissingTypesTestCase.class;
MockitoSpyBean annotation = testClass.getAnnotation(MockitoSpyBean.class);
assertThatIllegalStateException()
.isThrownBy(() -> processor.createHandlers(annotation, testClass))
.withMessage("The @MockitoSpyBean 'types' attribute must not be empty when declared on a class");
}
@Test
void nameNotSupportedWithMultipleTypes() {
Class<?> testClass = NameNotSupportedWithMultipleTypesTestCase.class;
MockitoSpyBean annotation = testClass.getAnnotation(MockitoSpyBean.class);
assertThatIllegalStateException()
.isThrownBy(() -> processor.createHandlers(annotation, testClass))
.withMessage("The @MockitoSpyBean 'name' attribute cannot be used when mocking multiple types");
}
@Test
void singleSpyByType() {
Class<?> testClass = SingleSpyByTypeTestCase.class;
MockitoSpyBean annotation = testClass.getAnnotation(MockitoSpyBean.class);
List<BeanOverrideHandler> handlers = processor.createHandlers(annotation, testClass);
assertThat(handlers).singleElement().isInstanceOfSatisfying(MockitoSpyBeanOverrideHandler.class, handler -> {
assertThat(handler.getField()).isNull();
assertThat(handler.getBeanName()).isNull();
assertThat(handler.getBeanType().resolve()).isEqualTo(Integer.class);
});
}
@Test
void singleSpyByName() {
Class<?> testClass = SingleSpyByNameTestCase.class;
MockitoSpyBean annotation = testClass.getAnnotation(MockitoSpyBean.class);
List<BeanOverrideHandler> handlers = processor.createHandlers(annotation, testClass);
assertThat(handlers).singleElement().isInstanceOfSatisfying(MockitoSpyBeanOverrideHandler.class, handler -> {
assertThat(handler.getField()).isNull();
assertThat(handler.getBeanName()).isEqualTo("enigma");
assertThat(handler.getBeanType().resolve()).isEqualTo(Integer.class);
});
}
@Test
void multipleSpies() {
Class<?> testClass = MultipleSpiesTestCase.class;
MockitoSpyBean annotation = testClass.getAnnotation(MockitoSpyBean.class);
List<BeanOverrideHandler> handlers = processor.createHandlers(annotation, testClass);
assertThat(handlers).satisfiesExactly(
handler1 -> {
assertThat(handler1.getField()).isNull();
assertThat(handler1.getBeanName()).isNull();
assertThat(handler1.getBeanType().resolve()).isEqualTo(Integer.class);
},
handler2 -> {
assertThat(handler2.getField()).isNull();
assertThat(handler2.getBeanName()).isNull();
assertThat(handler2.getBeanType().resolve()).isEqualTo(Float.class);
}
);
}
@MockitoSpyBean
static class MissingTypesTestCase {
}
@MockitoSpyBean(name = "bogus", types = { Integer.class, Float.class })
static class NameNotSupportedWithMultipleTypesTestCase {
}
@MockitoSpyBean(types = Integer.class)
static class SingleSpyByTypeTestCase {
}
@MockitoSpyBean(name = "enigma", types = Integer.class)
static class SingleSpyByNameTestCase {
}
@MockitoSpyBean(types = { Integer.class, Float.class })
static class MultipleSpiesTestCase {
}
}
}
} }

View File

@ -14,10 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
@MockitoBean(types = Service01.class) @MockitoBean(types = Service01.class)
interface TestInterface01 { interface MockTestInterface01 {
} }

View File

@ -14,10 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
@MockitoBean(types = Service08.class) @MockitoBean(types = Service08.class)
interface TestInterface08 { interface MockTestInterface08 {
} }

View File

@ -14,10 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
@MockitoBean(types = Service11.class) @MockitoBean(types = Service11.class)
interface TestInterface11 { interface MockTestInterface11 {
} }

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -30,6 +30,8 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
import static org.springframework.test.mockito.MockitoAssertions.assertIsNotMock;
/** /**
* Integration tests for {@link MockitoBeans @MockitoBeans} and * Integration tests for {@link MockitoBeans @MockitoBeans} and
@ -69,12 +71,18 @@ class MockitoBeansByNameIntegrationTests {
@Test @Test
void checkMocksAndStandardBean() { void checkMocksAndStandardBean() {
assertIsMock(s1, "s1");
assertIsMock(s2, "s2");
assertIsMock(service3, "service3");
assertIsNotMock(service4, "service4");
assertThat(s1.greeting()).isEqualTo("mock 1"); assertThat(s1.greeting()).isEqualTo("mock 1");
assertThat(s2.greeting()).isEqualTo("mock 2"); assertThat(s2.greeting()).isEqualTo("mock 2");
assertThat(service3.greeting()).isEqualTo("mock 3"); assertThat(service3.greeting()).isEqualTo("mock 3");
assertThat(service4.greeting()).isEqualTo("prod 4"); assertThat(service4.greeting()).isEqualTo("prod 4");
} }
@Configuration @Configuration
static class Config { static class Config {

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
@ -27,6 +27,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
/** /**
* Integration tests for {@link MockitoBeans @MockitoBeans} and * Integration tests for {@link MockitoBeans @MockitoBeans} and
@ -42,7 +43,7 @@ import static org.mockito.BDDMockito.given;
@MockitoBean(types = {Service04.class, Service05.class}) @MockitoBean(types = {Service04.class, Service05.class})
@SharedMocks // Intentionally declared between local @MockitoBean declarations @SharedMocks // Intentionally declared between local @MockitoBean declarations
@MockitoBean(types = Service06.class) @MockitoBean(types = Service06.class)
class MockitoBeansByTypeIntegrationTests implements TestInterface01 { class MockitoBeansByTypeIntegrationTests implements MockTestInterface01 {
@Autowired @Autowired
Service01 service01; Service01 service01;
@ -79,6 +80,14 @@ class MockitoBeansByTypeIntegrationTests implements TestInterface01 {
@Test @Test
void checkMocks() { void checkMocks() {
assertIsMock(service01, "service01");
assertIsMock(service02, "service02");
assertIsMock(service03, "service03");
assertIsMock(service04, "service04");
assertIsMock(service05, "service05");
assertIsMock(service06, "service06");
assertIsMock(service07, "service07");
assertThat(service01.greeting()).isEqualTo("mock 01"); assertThat(service01.greeting()).isEqualTo("mock 01");
assertThat(service02.greeting()).isEqualTo("mock 02"); assertThat(service02.greeting()).isEqualTo("mock 02");
assertThat(service03.greeting()).isEqualTo("mock 03"); assertThat(service03.greeting()).isEqualTo("mock 03");
@ -90,7 +99,7 @@ class MockitoBeansByTypeIntegrationTests implements TestInterface01 {
@MockitoBean(types = Service09.class) @MockitoBean(types = Service09.class)
class BaseTestCase implements TestInterface08 { class BaseTestCase implements MockTestInterface08 {
@Autowired @Autowired
Service08 service08; Service08 service08;
@ -104,7 +113,7 @@ class MockitoBeansByTypeIntegrationTests implements TestInterface01 {
@Nested @Nested
@MockitoBean(types = Service12.class) @MockitoBean(types = Service12.class)
class NestedTests extends BaseTestCase implements TestInterface11 { class NestedTests extends BaseTestCase implements MockTestInterface11 {
@Autowired @Autowired
Service11 service11; Service11 service11;
@ -128,6 +137,20 @@ class MockitoBeansByTypeIntegrationTests implements TestInterface01 {
@Test @Test
void checkMocks() { void checkMocks() {
assertIsMock(service01, "service01");
assertIsMock(service02, "service02");
assertIsMock(service03, "service03");
assertIsMock(service04, "service04");
assertIsMock(service05, "service05");
assertIsMock(service06, "service06");
assertIsMock(service07, "service07");
assertIsMock(service08, "service08");
assertIsMock(service09, "service09");
assertIsMock(service10, "service10");
assertIsMock(service11, "service11");
assertIsMock(service12, "service12");
assertIsMock(service13, "service13");
assertThat(service01.greeting()).isEqualTo("mock 01"); assertThat(service01.greeting()).isEqualTo("mock 01");
assertThat(service02.greeting()).isEqualTo("mock 02"); assertThat(service02.greeting()).isEqualTo("mock 02");
assertThat(service03.greeting()).isEqualTo("mock 03"); assertThat(service03.greeting()).isEqualTo("mock 03");

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
import java.util.stream.Stream; import java.util.stream.Stream;

View File

@ -0,0 +1,127 @@
/*
* Copyright 2002-2025 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;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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.mockito.MockitoSpyBean;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBeans;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.mockito.MockitoAssertions.assertIsNotMock;
import static org.springframework.test.mockito.MockitoAssertions.assertIsNotSpy;
import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy;
/**
* Integration tests for {@link MockitoSpyBeans @MockitoSpyBeans} and
* {@link MockitoSpyBean @MockitoSpyBean} declared "by name" at the class level
* as a repeatable annotation.
*
* @author Sam Brannen
* @since 6.2.3
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34408">gh-34408</a>
* @see MockitoSpyBeansByTypeIntegrationTests
*/
@SpringJUnitConfig
@MockitoSpyBean(name = "s1", types = ExampleService.class)
@MockitoSpyBean(name = "s2", types = ExampleService.class)
class MockitoSpyBeansByNameIntegrationTests {
@Autowired
ExampleService s1;
@Autowired
ExampleService s2;
@MockitoSpyBean(name = "s3")
ExampleService service3;
@Autowired
@Qualifier("s4")
ExampleService service4;
@BeforeEach
void configureSpies() {
given(s1.greeting()).willReturn("spy 1");
given(s2.greeting()).willReturn("spy 2");
given(service3.greeting()).willReturn("spy 3");
}
@Test
void checkSpiesAndStandardBean() {
assertIsSpy(s1, "s1");
assertIsSpy(s2, "s2");
assertIsSpy(service3, "service3");
assertIsNotMock(service4, "service4");
assertIsNotSpy(service4, "service4");
assertThat(s1.greeting()).isEqualTo("spy 1");
assertThat(s2.greeting()).isEqualTo("spy 2");
assertThat(service3.greeting()).isEqualTo("spy 3");
assertThat(service4.greeting()).isEqualTo("prod 4");
}
@Configuration
static class Config {
@Bean
ExampleService s1() {
return new ExampleService() {
@Override
public String greeting() {
return "prod 1";
}
};
}
@Bean
ExampleService s2() {
return new ExampleService() {
@Override
public String greeting() {
return "prod 2";
}
};
}
@Bean
ExampleService s3() {
return new ExampleService() {
@Override
public String greeting() {
return "prod 3";
}
};
}
@Bean
ExampleService s4() {
return () -> "prod 4";
}
}
}

View File

@ -0,0 +1,307 @@
/*
* Copyright 2002-2025 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;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBeans;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy;
/**
* Integration tests for {@link MockitoSpyBeans @MockitoSpyBeans} and
* {@link MockitoSpyBean @MockitoSpyBean} declared "by type" at the class level,
* as a repeatable annotation, and via a custom composed annotation.
*
* @author Sam Brannen
* @since 6.2.3
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34408">gh-34408</a>
* @see MockitoSpyBeansByNameIntegrationTests
*/
@SpringJUnitConfig
@MockitoSpyBean(types = {Service04.class, Service05.class})
@SharedSpies // Intentionally declared between local @MockitoSpyBean declarations
@MockitoSpyBean(types = Service06.class)
class MockitoSpyBeansByTypeIntegrationTests implements SpyTestInterface01 {
@Autowired
Service01 service01;
@Autowired
Service02 service02;
@Autowired
Service03 service03;
@Autowired
Service04 service04;
@Autowired
Service05 service05;
@Autowired
Service06 service06;
@MockitoSpyBean
Service07 service07;
@BeforeEach
void configureSpies() {
given(service01.greeting()).willReturn("spy 01");
given(service02.greeting()).willReturn("spy 02");
given(service03.greeting()).willReturn("spy 03");
given(service04.greeting()).willReturn("spy 04");
given(service05.greeting()).willReturn("spy 05");
given(service06.greeting()).willReturn("spy 06");
given(service07.greeting()).willReturn("spy 07");
}
@Test
void checkSpies() {
assertIsSpy(service01, "service01");
assertIsSpy(service02, "service02");
assertIsSpy(service03, "service03");
assertIsSpy(service04, "service04");
assertIsSpy(service05, "service05");
assertIsSpy(service06, "service06");
assertIsSpy(service07, "service07");
assertThat(service01.greeting()).isEqualTo("spy 01");
assertThat(service02.greeting()).isEqualTo("spy 02");
assertThat(service03.greeting()).isEqualTo("spy 03");
assertThat(service04.greeting()).isEqualTo("spy 04");
assertThat(service05.greeting()).isEqualTo("spy 05");
assertThat(service06.greeting()).isEqualTo("spy 06");
assertThat(service07.greeting()).isEqualTo("spy 07");
}
@MockitoSpyBean(types = Service09.class)
class BaseTestCase implements SpyTestInterface08 {
@Autowired
Service08 service08;
@Autowired
Service09 service09;
@MockitoSpyBean
Service10 service10;
}
@Nested
@MockitoSpyBean(types = Service12.class)
class NestedTests extends BaseTestCase implements SpyTestInterface11 {
@Autowired
Service11 service11;
@Autowired
Service12 service12;
@MockitoSpyBean
Service13 service13;
@BeforeEach
void configureSpies() {
given(service08.greeting()).willReturn("spy 08");
given(service09.greeting()).willReturn("spy 09");
given(service10.greeting()).willReturn("spy 10");
given(service11.greeting()).willReturn("spy 11");
given(service12.greeting()).willReturn("spy 12");
given(service13.greeting()).willReturn("spy 13");
}
@Test
void checkSpies() {
assertIsSpy(service01, "service01");
assertIsSpy(service02, "service02");
assertIsSpy(service03, "service03");
assertIsSpy(service04, "service04");
assertIsSpy(service05, "service05");
assertIsSpy(service06, "service06");
assertIsSpy(service07, "service07");
assertIsSpy(service08, "service08");
assertIsSpy(service09, "service09");
assertIsSpy(service10, "service10");
assertIsSpy(service11, "service11");
assertIsSpy(service12, "service12");
assertIsSpy(service13, "service13");
assertThat(service01.greeting()).isEqualTo("spy 01");
assertThat(service02.greeting()).isEqualTo("spy 02");
assertThat(service03.greeting()).isEqualTo("spy 03");
assertThat(service04.greeting()).isEqualTo("spy 04");
assertThat(service05.greeting()).isEqualTo("spy 05");
assertThat(service06.greeting()).isEqualTo("spy 06");
assertThat(service07.greeting()).isEqualTo("spy 07");
assertThat(service08.greeting()).isEqualTo("spy 08");
assertThat(service09.greeting()).isEqualTo("spy 09");
assertThat(service10.greeting()).isEqualTo("spy 10");
assertThat(service11.greeting()).isEqualTo("spy 11");
assertThat(service12.greeting()).isEqualTo("spy 12");
assertThat(service13.greeting()).isEqualTo("spy 13");
}
}
@Configuration
static class Config {
@Bean
Service01 service01() {
return new Service01() {
@Override
public String greeting() {
return "prod 1";
}
};
}
@Bean
Service02 service02() {
return new Service02() {
@Override
public String greeting() {
return "prod 2";
}
};
}
@Bean
Service03 service03() {
return new Service03() {
@Override
public String greeting() {
return "prod 3";
}
};
}
@Bean
Service04 service04() {
return new Service04() {
@Override
public String greeting() {
return "prod 4";
}
};
}
@Bean
Service05 service05() {
return new Service05() {
@Override
public String greeting() {
return "prod 5";
}
};
}
@Bean
Service06 service06() {
return new Service06() {
@Override
public String greeting() {
return "prod 6";
}
};
}
@Bean
Service07 service07() {
return new Service07() {
@Override
public String greeting() {
return "prod 7";
}
};
}
@Bean
Service08 service08() {
return new Service08() {
@Override
public String greeting() {
return "prod 8";
}
};
}
@Bean
Service09 service09() {
return new Service09() {
@Override
public String greeting() {
return "prod 9";
}
};
}
@Bean
Service10 service10() {
return new Service10() {
@Override
public String greeting() {
return "prod 10";
}
};
}
@Bean
Service11 service11() {
return new Service11() {
@Override
public String greeting() {
return "prod 11";
}
};
}
@Bean
Service12 service12() {
return new Service12() {
@Override
public String greeting() {
return "prod 12";
}
};
}
@Bean
Service13 service13() {
return new Service13() {
@Override
public String greeting() {
return "prod 13";
}
};
}
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright 2002-2025 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;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.springframework.core.ResolvableType;
import org.springframework.test.context.bean.override.BeanOverrideHandler;
import org.springframework.test.context.bean.override.BeanOverrideTestUtils;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBeans;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link MockitoSpyBeans @MockitoSpyBeans}: {@link MockitoSpyBean @MockitoSpyBean}
* declared at the class level, as a repeatable annotation, and via a custom composed
* annotation.
*
* @author Sam Brannen
* @since 6.2.3
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34408">gh-34408</a>
*/
class MockitoSpyBeansTests {
@Test
void registrationOrderForTopLevelClass() {
Stream<Class<?>> mockedServices = getRegisteredMockTypes(MockitoSpyBeansByTypeIntegrationTests.class);
assertThat(mockedServices).containsExactly(
Service01.class, Service02.class, Service03.class, Service04.class,
Service05.class, Service06.class, Service07.class);
}
@Test
void registrationOrderForNestedClass() {
Stream<Class<?>> mockedServices = getRegisteredMockTypes(MockitoSpyBeansByTypeIntegrationTests.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,
Service13.class);
}
private static Stream<Class<?>> getRegisteredMockTypes(Class<?> testClass) {
return BeanOverrideTestUtils.findAllHandlers(testClass)
.stream()
.map(BeanOverrideHandler::getBeanType)
.map(ResolvableType::getRawClass);
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
interface Service { interface Service {
String greeting(); String greeting();

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
interface Service01 extends Service { interface Service01 extends Service {
} }

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
interface Service02 extends Service { interface Service02 extends Service {
} }

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
interface Service03 extends Service { interface Service03 extends Service {
} }

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
interface Service04 extends Service { interface Service04 extends Service {
} }

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
interface Service05 extends Service { interface Service05 extends Service {
} }

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
interface Service06 extends Service { interface Service06 extends Service {
} }

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
interface Service07 extends Service { interface Service07 extends Service {
} }

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
interface Service08 extends Service { interface Service08 extends Service {
} }

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
interface Service09 extends Service { interface Service09 extends Service {
} }

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
interface Service10 extends Service { interface Service10 extends Service {
} }

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
interface Service11 extends Service { interface Service11 extends Service {
} }

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
interface Service12 extends Service { interface Service12 extends Service {
} }

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
interface Service13 extends Service { interface Service13 extends Service {
} }

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context.bean.override.mockito.mockbeans; package org.springframework.test.context.bean.override.mockito.typelevel;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;

View File

@ -0,0 +1,31 @@
/*
* Copyright 2002-2025 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;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@MockitoSpyBean(types = Service02.class)
@MockitoSpyBean(types = Service03.class)
@interface SharedSpies {
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2002-2025 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;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
@MockitoSpyBean(types = Service01.class)
interface SpyTestInterface01 {
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2002-2025 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;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
@MockitoSpyBean(types = Service08.class)
interface SpyTestInterface08 {
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2002-2025 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;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
@MockitoSpyBean(types = Service11.class)
interface SpyTestInterface11 {
}

View File

@ -31,10 +31,12 @@ public abstract class MockitoAssertions {
public static void assertIsMock(Object obj) { public static void assertIsMock(Object obj) {
assertThat(isMock(obj)).as("is a Mockito mock").isTrue(); assertThat(isMock(obj)).as("is a Mockito mock").isTrue();
assertIsNotSpy(obj);
} }
public static void assertIsMock(Object obj, String message) { public static void assertIsMock(Object obj, String message) {
assertThat(isMock(obj)).as("%s is a Mockito mock", message).isTrue(); assertThat(isMock(obj)).as("%s is a Mockito mock", message).isTrue();
assertIsNotSpy(obj, message);
} }
public static void assertIsNotMock(Object obj) { public static void assertIsNotMock(Object obj) {