Support @⁠MockitoSpyBean at the type level on test classes

Prior to this commit, @⁠MockitoSpyBean could only be declared on fields
within test classes, which prevented developers from being able to
easily reuse spy configuration across a test suite.

With this commit, @⁠MockitoSpyBean is now supported at the type level
on test classes, their superclasses, and interfaces implemented by
those classes. @⁠MockitoSpyBean is also supported on enclosing classes
for @⁠Nested test classes, their superclasses, and interfaces
implemented by those classes, while honoring @⁠NestedTestConfiguration
semantics.

In addition, @⁠MockitoSpyBean:

- has a new `types` attribute that can be used to declare the type or
  types to spy when @⁠MockitoSpyBean is declared at the type level

- can be declared as a repeatable annotation at the type level

- can be declared as a meta-annotation on a custom composed annotation
  which can be reused across a test suite (see the @⁠SharedSpies
  example in the reference manual)

To support these new features, this commit also includes the following
changes.

- MockitoSpyBeanOverrideProcessor has been revised to support
  @⁠MockitoSpyBean at the type level.

- The "Bean Overriding in Tests" and "@⁠MockitoBean and
  @⁠MockitoSpyBean" sections of the reference manual have been fully
  revised.

See gh-34408
Closes gh-33925
This commit is contained in:
Sam Brannen 2025-02-11 17:46:01 +01:00
parent b336bbe539
commit e31ce359a1
37 changed files with 1106 additions and 208 deletions

View File

@ -1,34 +1,60 @@
[[spring-testing-annotation-beanoverriding-mockitobean]]
= `@MockitoBean` and `@MockitoSpyBean`
`@MockitoBean` and `@MockitoSpyBean` are used on non-static fields in test classes to
override beans in the test's `ApplicationContext` with a Mockito _mock_ or _spy_,
respectively. In the latter case, an early instance of the original bean is captured and
wrapped by the spy.
`@MockitoBean` and `@MockitoSpyBean` can be used in test classes to override a bean in
the test's `ApplicationContext` with a Mockito _mock_ or _spy_, respectively. In the
latter case, an early instance of the original bean is captured and wrapped by the spy.
By default, the annotated field's type is used to search for candidate beans to override.
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
match.
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.
* 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]
====
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
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.
====
Each annotation also defines Mockito-specific attributes to fine-tune the mocking behavior.
The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE`
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding].
If no existing bean matches, a new bean is created on the fly. However, you can switch to
the `REPLACE` strategy by setting the `enforceOverride` attribute to `true`. See the
following section for an example.
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy for bean overrides].
If a corresponding bean does not exist, a new bean will be created. However, you can
switch to the `REPLACE` strategy by setting the `enforceOverride` attribute to `true`
for example, `@MockitoBean(enforceOverride = true)`.
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
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]]
== `@MockitoBean` Examples
When using `@MockitoBean`, a new 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 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:
The following example shows how to use the default behavior of the `@MockitoBean`
annotation.
[tabs]
======
@ -81,7 +100,7 @@ Java::
// 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
@ -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
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]
======
@ -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.
[tabs]
@ -191,7 +188,7 @@ APIs.
== `@MockitoSpyBean` Examples
The following example shows how to use the default behavior of the `@MockitoSpyBean`
annotation:
annotation.
[tabs]
======
@ -208,7 +205,7 @@ Java::
// 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
@ -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
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]
======
@ -233,5 +230,58 @@ Java::
// 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 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
test class.
`ApplicationContext` for a test class, by annotating the test class or one or more
non-static fields in 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,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`
properties file].
The bean overriding infrastructure searches in test classes for any non-static field that
is meta-annotated with `@BeanOverride` and instantiates the corresponding
`BeanOverrideProcessor` which is responsible for creating an appropriate
`BeanOverrideHandler`.
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
responsible for creating an appropriate `BeanOverrideHandler`.
The internal `BeanOverrideBeanFactoryPostProcessor` then uses bean override handlers to
alter the test's `ApplicationContext` by creating, replacing, or wrapping beans as
defined by the corresponding `BeanOverrideStrategy`:
[[testcontext-bean-overriding-strategy]]
`REPLACE`::
Replaces the bean. Throws an exception if a corresponding bean does not exist.
`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
* override beans in a test's
* override a bean in the test's
* {@link org.springframework.context.ApplicationContext ApplicationContext}
* using Mockito mocks.
* with a Mockito mock.
*
* <p>{@code @MockitoBean} can be applied in the following ways.
* <ul>
@ -49,18 +49,19 @@ import org.springframework.test.context.bean.override.BeanOverride;
* </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, a
* {@code @Qualifier} annotation can be declared on the field to help disambiguate.
* In the absence of a {@code @Qualifier} annotation, the name of the annotated
* field will be used as a fallback qualifier. Alternatively, you can explicitly
* specify a bean name to mock by setting the {@link #value() value} or
* {@link #name() name} attribute.
* 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 at the type level, the type of bean
* to mock must be supplied via the {@link #types() types} attribute. If multiple
* candidates exist, you can explicitly specify a bean name to mock 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.
* (or beans) to mock 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 mock 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 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,
@ -111,7 +112,7 @@ import org.springframework.test.context.bean.override.BeanOverride;
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
* example, {@code @MockitoBean("customBeanName")}.
* @see #name()
@ -136,7 +137,7 @@ public @interface MockitoBean {
* <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>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.
* @return the types to mock
* @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");
return new MockitoBeanOverrideHandler(field, ResolvableType.forField(field, testClass), mockitoBean);
}
else if (overrideAnnotation instanceof MockitoSpyBean spyBean) {
return new MockitoSpyBeanOverrideHandler(field, ResolvableType.forField(field, testClass), spyBean);
else if (overrideAnnotation instanceof MockitoSpyBean mockitoSpyBean) {
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("""
Invalid annotation passed to MockitoBeanOverrideProcessor: \
@ -56,21 +58,34 @@ class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
@Override
public List<BeanOverrideHandler> createHandlers(Annotation overrideAnnotation, Class<?> testClass) {
if (!(overrideAnnotation instanceof MockitoBean mockitoBean)) {
throw new IllegalStateException("""
Invalid annotation passed to MockitoBeanOverrideProcessor: \
expected @MockitoBean on test class """ + testClass.getName());
if (overrideAnnotation instanceof MockitoBean mockitoBean) {
Class<?>[] types = mockitoBean.types();
Assert.state(types.length > 0,
"The @MockitoBean 'types' attribute must not be empty when declared on a class");
Assert.state(mockitoBean.name().isEmpty() || types.length == 1,
"The @MockitoBean 'name' attribute cannot be used when mocking multiple types");
List<BeanOverrideHandler> handlers = new ArrayList<>();
for (Class<?> type : types) {
handlers.add(new MockitoBeanOverrideHandler(ResolvableType.forClass(type), mockitoBean));
}
return handlers;
}
Class<?>[] types = mockitoBean.types();
Assert.state(types.length > 0,
"The @MockitoBean 'types' attribute must not be empty when declared on a class");
Assert.state(mockitoBean.name().isEmpty() || types.length == 1,
"The @MockitoBean 'name' attribute cannot be used when mocking multiple types");
List<BeanOverrideHandler> handlers = new ArrayList<>();
for (Class<?> type : types) {
handlers.add(new MockitoBeanOverrideHandler(ResolvableType.forClass(type), mockitoBean));
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;
}
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.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@ -26,19 +27,40 @@ import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.bean.override.BeanOverride;
/**
* {@code @MockitoSpyBean} is an annotation that can be applied to a non-static
* field in a test class to override a bean in the test's
* {@code @MockitoSpyBean} is an annotation that can be used in test classes to
* override a bean in the test's
* {@link org.springframework.context.ApplicationContext ApplicationContext}
* 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
* field. If multiple candidates exist, a {@code @Qualifier} annotation can be
* used to help disambiguate. In the absence of a {@code @Qualifier} annotation,
* the name of the annotated field will be used as a fallback qualifier.
* Alternatively, you can explicitly specify a bean name to spy by setting the
* {@link #value() value} or {@link #name() name} attribute. If a bean name is
* specified, it is required that a target bean with that name has been previously
* registered in the application context.
* <p>{@code @MockitoSpyBean} can be applied in the following ways.
* <ul>
* <li>On a non-static field in a test class or any of its superclasses.</li>
* <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>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
* 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
* 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
* practices of the project.
*
* <p>{@code @MockitoSpyBean} fields will be inherited from an enclosing test class by default.
* See {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
* <p>{@code @MockitoSpyBean} fields and type-level {@code @MockitoSpyBean} declarations
* will be inherited from an enclosing test class by default. See
* {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
* 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 Sam Brannen
* @since 6.2
* @see org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean
* @see org.springframework.test.context.bean.override.convention.TestBean @TestBean
*/
@Target(ElementType.FIELD)
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(MockitoSpyBeans.class)
@BeanOverride(MockitoBeanOverrideProcessor.class)
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
* example, {@code @MockitoSpyBean("customBeanName")}.
* @see #name()
@ -84,13 +115,27 @@ public @interface MockitoSpyBean {
/**
* Name of the bean to spy.
* <p>If left unspecified, the bean to spy is selected according to 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 annotated field's type, taking
* qualifiers into account if necessary. See the {@linkplain MockitoSpyBean
* class-level documentation} for details.
* @see #value()
*/
@AliasFor("value")
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.
* <p>The default is {@link MockReset#AFTER} meaning that spies are automatically

View File

@ -48,7 +48,11 @@ class MockitoSpyBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
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),
BeanOverrideStrategy.WRAP, spyBean.reset());
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

@ -107,91 +107,197 @@ class MockitoBeanOverrideProcessorTests {
class CreateHandlersTests {
@Test
void missingTypes() {
Class<?> testClass = MissingTypesTestCase.class;
MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
void otherAnnotationThrows() {
Annotation annotation = getClass().getAnnotation(Nested.class);
assertThatIllegalStateException()
.isThrownBy(() -> processor.createHandlers(annotation, testClass))
.withMessage("The @MockitoBean 'types' attribute must not be empty when declared on a class");
.isThrownBy(() -> processor.createHandlers(annotation, getClass()))
.withMessage("Invalid annotation passed to MockitoBeanOverrideProcessor: expected either " +
"@MockitoBean or @MockitoSpyBean on test class %s", getClass().getName());
}
@Test
void nameNotSupportedWithMultipleTypes() {
Class<?> testClass = NameNotSupportedWithMultipleTypesTestCase.class;
MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
@Nested
class MockitoBeanTests {
assertThatIllegalStateException()
.isThrownBy(() -> processor.createHandlers(annotation, testClass))
.withMessage("The @MockitoBean 'name' attribute cannot be used when mocking multiple types");
@Test
void missingTypes() {
Class<?> testClass = MissingTypesTestCase.class;
MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
assertThatIllegalStateException()
.isThrownBy(() -> processor.createHandlers(annotation, testClass))
.withMessage("The @MockitoBean 'types' attribute must not be empty when declared on a class");
}
@Test
void nameNotSupportedWithMultipleTypes() {
Class<?> testClass = NameNotSupportedWithMultipleTypesTestCase.class;
MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
assertThatIllegalStateException()
.isThrownBy(() -> processor.createHandlers(annotation, testClass))
.withMessage("The @MockitoBean 'name' attribute cannot be used when mocking multiple types");
}
@Test
void singleMockByType() {
Class<?> testClass = SingleMockByTypeTestCase.class;
MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
List<BeanOverrideHandler> handlers = processor.createHandlers(annotation, testClass);
assertThat(handlers).singleElement().isInstanceOfSatisfying(MockitoBeanOverrideHandler.class, handler -> {
assertThat(handler.getField()).isNull();
assertThat(handler.getBeanName()).isNull();
assertThat(handler.getBeanType().resolve()).isEqualTo(Integer.class);
});
}
@Test
void singleMockByName() {
Class<?> testClass = SingleMockByNameTestCase.class;
MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
List<BeanOverrideHandler> handlers = processor.createHandlers(annotation, testClass);
assertThat(handlers).singleElement().isInstanceOfSatisfying(MockitoBeanOverrideHandler.class, handler -> {
assertThat(handler.getField()).isNull();
assertThat(handler.getBeanName()).isEqualTo("enigma");
assertThat(handler.getBeanType().resolve()).isEqualTo(Integer.class);
});
}
@Test
void multipleMocks() {
Class<?> testClass = MultipleMocksTestCase.class;
MockitoBean annotation = testClass.getAnnotation(MockitoBean.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);
}
);
}
@MockitoBean
static class MissingTypesTestCase {
}
@MockitoBean(name = "bogus", types = { Integer.class, Float.class })
static class NameNotSupportedWithMultipleTypesTestCase {
}
@MockitoBean(types = Integer.class)
static class SingleMockByTypeTestCase {
}
@MockitoBean(name = "enigma", types = Integer.class)
static class SingleMockByNameTestCase {
}
@MockitoBean(types = { Integer.class, Float.class })
static class MultipleMocksTestCase {
}
}
@Test
void singleMockByType() {
Class<?> testClass = SingleMockByTypeTestCase.class;
MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
List<BeanOverrideHandler> handlers = processor.createHandlers(annotation, testClass);
@Nested
class MockitoSpyBeanTests {
assertThat(handlers).singleElement().isInstanceOfSatisfying(MockitoBeanOverrideHandler.class, handler -> {
assertThat(handler.getField()).isNull();
assertThat(handler.getBeanName()).isNull();
assertThat(handler.getBeanType().resolve()).isEqualTo(Integer.class);
});
@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 {
}
}
@Test
void singleMockByName() {
Class<?> testClass = SingleMockByNameTestCase.class;
MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
List<BeanOverrideHandler> handlers = processor.createHandlers(annotation, testClass);
assertThat(handlers).singleElement().isInstanceOfSatisfying(MockitoBeanOverrideHandler.class, handler -> {
assertThat(handler.getField()).isNull();
assertThat(handler.getBeanName()).isEqualTo("enigma");
assertThat(handler.getBeanType().resolve()).isEqualTo(Integer.class);
});
}
@Test
void multipleMocks() {
Class<?> testClass = MultipleMocksTestCase.class;
MockitoBean annotation = testClass.getAnnotation(MockitoBean.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);
}
);
}
@MockitoBean
static class MissingTypesTestCase {
}
@MockitoBean(name = "bogus", types = { Integer.class, Float.class })
static class NameNotSupportedWithMultipleTypesTestCase {
}
@MockitoBean(types = Integer.class)
static class SingleMockByTypeTestCase {
}
@MockitoBean(name = "enigma", types = Integer.class)
static class SingleMockByNameTestCase {
}
@MockitoBean(types = { Integer.class, Float.class })
static class MultipleMocksTestCase {
}
}
}

View File

@ -14,10 +14,10 @@
* 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;
@MockitoBean(types = Service01.class)
interface TestInterface01 {
interface MockTestInterface01 {
}

View File

@ -14,10 +14,10 @@
* 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;
@MockitoBean(types = Service08.class)
interface TestInterface08 {
interface MockTestInterface08 {
}

View File

@ -14,10 +14,10 @@
* 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;
@MockitoBean(types = Service11.class)
interface TestInterface11 {
interface MockTestInterface11 {
}

View File

@ -14,7 +14,7 @@
* 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.Test;
@ -30,6 +30,8 @@ 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.assertIsMock;
import static org.springframework.test.mockito.MockitoAssertions.assertIsNotMock;
/**
* Integration tests for {@link MockitoBeans @MockitoBeans} and
@ -69,12 +71,18 @@ class MockitoBeansByNameIntegrationTests {
@Test
void checkMocksAndStandardBean() {
assertIsMock(s1, "s1");
assertIsMock(s2, "s2");
assertIsMock(service3, "service3");
assertIsNotMock(service4, "service4");
assertThat(s1.greeting()).isEqualTo("mock 1");
assertThat(s2.greeting()).isEqualTo("mock 2");
assertThat(service3.greeting()).isEqualTo("mock 3");
assertThat(service4.greeting()).isEqualTo("prod 4");
}
@Configuration
static class Config {

View File

@ -14,7 +14,7 @@
* 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.Nested;
@ -27,6 +27,7 @@ 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.assertIsMock;
/**
* Integration tests for {@link MockitoBeans @MockitoBeans} and
@ -42,7 +43,7 @@ import static org.mockito.BDDMockito.given;
@MockitoBean(types = {Service04.class, Service05.class})
@SharedMocks // Intentionally declared between local @MockitoBean declarations
@MockitoBean(types = Service06.class)
class MockitoBeansByTypeIntegrationTests implements TestInterface01 {
class MockitoBeansByTypeIntegrationTests implements MockTestInterface01 {
@Autowired
Service01 service01;
@ -79,6 +80,14 @@ class MockitoBeansByTypeIntegrationTests implements TestInterface01 {
@Test
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(service02.greeting()).isEqualTo("mock 02");
assertThat(service03.greeting()).isEqualTo("mock 03");
@ -90,7 +99,7 @@ class MockitoBeansByTypeIntegrationTests implements TestInterface01 {
@MockitoBean(types = Service09.class)
class BaseTestCase implements TestInterface08 {
class BaseTestCase implements MockTestInterface08 {
@Autowired
Service08 service08;
@ -104,7 +113,7 @@ class MockitoBeansByTypeIntegrationTests implements TestInterface01 {
@Nested
@MockitoBean(types = Service12.class)
class NestedTests extends BaseTestCase implements TestInterface11 {
class NestedTests extends BaseTestCase implements MockTestInterface11 {
@Autowired
Service11 service11;
@ -128,6 +137,20 @@ class MockitoBeansByTypeIntegrationTests implements TestInterface01 {
@Test
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(service02.greeting()).isEqualTo("mock 02");
assertThat(service03.greeting()).isEqualTo("mock 03");

View File

@ -14,7 +14,7 @@
* 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;

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.
*/
package org.springframework.test.context.bean.override.mockito.mockbeans;
package org.springframework.test.context.bean.override.mockito.typelevel;
interface Service {
String greeting();

View File

@ -14,7 +14,7 @@
* 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 {
}

View File

@ -14,7 +14,7 @@
* 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 {
}

View File

@ -14,7 +14,7 @@
* 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 {
}

View File

@ -14,7 +14,7 @@
* 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 {
}

View File

@ -14,7 +14,7 @@
* 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 {
}

View File

@ -14,7 +14,7 @@
* 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 {
}

View File

@ -14,7 +14,7 @@
* 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 {
}

View File

@ -14,7 +14,7 @@
* 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 {
}

View File

@ -14,7 +14,7 @@
* 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 {
}

View File

@ -14,7 +14,7 @@
* 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 {
}

View File

@ -14,7 +14,7 @@
* 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 {
}

View File

@ -14,7 +14,7 @@
* 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 {
}

View File

@ -14,7 +14,7 @@
* 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 {
}

View File

@ -14,7 +14,7 @@
* 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.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) {
assertThat(isMock(obj)).as("is a Mockito mock").isTrue();
assertIsNotSpy(obj);
}
public static void assertIsMock(Object obj, String message) {
assertThat(isMock(obj)).as("%s is a Mockito mock", message).isTrue();
assertIsNotSpy(obj, message);
}
public static void assertIsNotMock(Object obj) {