Skip to content

Commit e31ce35

Browse files
committed
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
1 parent b336bbe commit e31ce35

37 files changed

+1100
-202
lines changed

framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc

+104-54
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,60 @@
11
[[spring-testing-annotation-beanoverriding-mockitobean]]
22
= `@MockitoBean` and `@MockitoSpyBean`
33

4-
`@MockitoBean` and `@MockitoSpyBean` are used on non-static fields in test classes to
5-
override beans in the test's `ApplicationContext` with a Mockito _mock_ or _spy_,
6-
respectively. In the latter case, an early instance of the original bean is captured and
7-
wrapped by the spy.
4+
`@MockitoBean` and `@MockitoSpyBean` can be used in test classes to override a bean in
5+
the test's `ApplicationContext` with a Mockito _mock_ or _spy_, respectively. In the
6+
latter case, an early instance of the original bean is captured and wrapped by the spy.
7+
8+
The annotations can be applied in the following ways.
9+
10+
* On a non-static field in a test class or any of its superclasses.
11+
* On a non-static field in an enclosing class for a `@Nested` test class or in any class
12+
in the type hierarchy or enclosing class hierarchy above the `@Nested` test class.
13+
* At the type level on a test class or any superclass or implemented interface in the
14+
type hierarchy above the test class.
15+
* At the type level on an enclosing class for a `@Nested` test class or on any class or
16+
interface in the type hierarchy or enclosing class hierarchy above the `@Nested` test
17+
class.
18+
19+
When `@MockitoBean` or `@MockitoSpyBean` is declared on a field, the bean to mock or spy
20+
is inferred from the type of the annotated field. If multiple candidates exist in the
21+
`ApplicationContext`, a `@Qualifier` annotation can be declared on the field to help
22+
disambiguate. In the absence of a `@Qualifier` annotation, the name of the annotated
23+
field will be used as a _fallback qualifier_. Alternatively, you can explicitly specify a
24+
bean name to mock or spy by setting the `value` or `name` attribute in the annotation.
25+
26+
When `@MockitoBean` or `@MockitoSpyBean` is declared at the type level, the type of bean
27+
(or beans) to mock or spy must be supplied via the `types` attribute in the annotation –
28+
for example, `@MockitoBean(types = {OrderService.class, UserService.class})`. If multiple
29+
candidates exist in the `ApplicationContext`, you can explicitly specify a bean name to
30+
mock or spy by setting the `name` attribute. Note, however, that the `types` attribute
31+
must contain a single type if an explicit bean `name` is configured – for example,
32+
`@MockitoBean(name = "ps1", types = PrintingService.class)`.
833

9-
By default, the annotated field's type is used to search for candidate beans to override.
10-
If multiple candidates match, `@Qualifier` can be provided to narrow the candidate to
11-
override. Alternatively, a candidate whose bean name matches the name of the field will
12-
match.
34+
To support reuse of mock configuration, `@MockitoBean` and `@MockitoSpyBean` may be used
35+
as meta-annotations to create custom _composed annotations_ – for example, to define
36+
common mock or spy configuration in a single annotation that can be reused across a test
37+
suite. `@MockitoBean` and `@MockitoSpyBean` can also be used as repeatable annotations at
38+
the type level — for example, to mock or spy several beans by name.
1339

1440
[WARNING]
1541
====
16-
Qualifiers, including the name of the field, are used to determine if a separate
42+
Qualifiers, including the name of a field, are used to determine if a separate
1743
`ApplicationContext` needs to be created. If you are using this feature to mock or spy
18-
the same bean in several test classes, make sure to name the field consistently to avoid
44+
the same bean in several test classes, make sure to name the fields consistently to avoid
1945
creating unnecessary contexts.
2046
====
2147

2248
Each annotation also defines Mockito-specific attributes to fine-tune the mocking behavior.
2349

2450
The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE`
25-
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding].
26-
If no existing bean matches, a new bean is created on the fly. However, you can switch to
27-
the `REPLACE` strategy by setting the `enforceOverride` attribute to `true`. See the
28-
following section for an example.
51+
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy for bean overrides].
52+
If a corresponding bean does not exist, a new bean will be created. However, you can
53+
switch to the `REPLACE` strategy by setting the `enforceOverride` attribute to `true`
54+
for example, `@MockitoBean(enforceOverride = true)`.
2955

3056
The `@MockitoSpyBean` annotation uses the `WRAP`
31-
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy],
57+
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy],
3258
and the original instance is wrapped in a Mockito spy. This strategy requires that
3359
exactly one candidate bean exists.
3460

@@ -56,15 +82,8 @@ or `private` depending on the needs or coding practices of the project.
5682
[[spring-testing-annotation-beanoverriding-mockitobean-examples]]
5783
== `@MockitoBean` Examples
5884

59-
When using `@MockitoBean`, a new bean will be created if a corresponding bean does not
60-
exist. However, if you would like for the test to fail when a corresponding bean does not
61-
exist, you can set the `enforceOverride` attribute to `true` – for example,
62-
`@MockitoBean(enforceOverride = true)`.
63-
64-
To use a by-name override rather than a by-type override, specify the `name` (or `value`)
65-
attribute of the annotation.
66-
67-
The following example shows how to use the default behavior of the `@MockitoBean` annotation:
85+
The following example shows how to use the default behavior of the `@MockitoBean`
86+
annotation.
6887

6988
[tabs]
7089
======
@@ -81,7 +100,7 @@ Java::
81100
// tests...
82101
}
83102
----
84-
<1> Replace the bean with type `CustomService` with a Mockito `mock`.
103+
<1> Replace the bean with type `CustomService` with a Mockito mock.
85104
======
86105

87106
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
90109
`CustomService` beans you want to override. If no such bean exists, a bean will be
91110
created with an auto-generated bean name.
92111

93-
The following example uses a by-name lookup, rather than a by-type lookup:
112+
The following example uses a by-name lookup, rather than a by-type lookup. If no bean
113+
named `service` exists, one is created.
94114

95115
[tabs]
96116
======
@@ -108,32 +128,9 @@ Java::
108128
109129
}
110130
----
111-
<1> Replace the bean named `service` with a Mockito `mock`.
131+
<1> Replace the bean named `service` with a Mockito mock.
112132
======
113133

114-
If no bean named `service` exists, one is created.
115-
116-
`@MockitoBean` can also be used at the type level:
117-
118-
- on a test class or any superclass or implemented interface in the type hierarchy above
119-
the test class
120-
- on an enclosing class for a `@Nested` test class or on any class or interface in the
121-
type hierarchy or enclosing class hierarchy above the `@Nested` test class
122-
123-
When `@MockitoBean` is declared at the type level, the type of bean (or beans) to mock
124-
must be supplied via the `types` attribute – for example,
125-
`@MockitoBean(types = {OrderService.class, UserService.class})`. If multiple candidates
126-
exist in the application context, you can explicitly specify a bean name to mock by
127-
setting the `name` attribute. Note, however, that the `types` attribute must contain a
128-
single type if an explicit bean `name` is configured – for example,
129-
`@MockitoBean(name = "ps1", types = PrintingService.class)`.
130-
131-
To support reuse of mock configuration, `@MockitoBean` may be used as a meta-annotation
132-
to create custom _composed annotations_ — for example, to define common mock
133-
configuration in a single annotation that can be reused across a test suite.
134-
`@MockitoBean` can also be used as a repeatable annotation at the type level — for
135-
example, to mock several beans by name.
136-
137134
The following `@SharedMocks` annotation registers two mocks by-type and one mock by-name.
138135

139136
[tabs]
@@ -191,7 +188,7 @@ APIs.
191188
== `@MockitoSpyBean` Examples
192189

193190
The following example shows how to use the default behavior of the `@MockitoSpyBean`
194-
annotation:
191+
annotation.
195192

196193
[tabs]
197194
======
@@ -208,15 +205,15 @@ Java::
208205
// tests...
209206
}
210207
----
211-
<1> Wrap the bean with type `CustomService` with a Mockito `spy`.
208+
<1> Wrap the bean with type `CustomService` with a Mockito spy.
212209
======
213210

214211
In the example above, we are wrapping the bean with type `CustomService`. If more than
215212
one bean of that type exists, the bean named `customService` is considered. Otherwise,
216213
the test will fail, and you will need to provide a qualifier of some sort to identify
217214
which of the `CustomService` beans you want to spy.
218215

219-
The following example uses a by-name lookup, rather than a by-type lookup:
216+
The following example uses a by-name lookup, rather than a by-type lookup.
220217

221218
[tabs]
222219
======
@@ -233,5 +230,58 @@ Java::
233230
// tests...
234231
}
235232
----
236-
<1> Wrap the bean named `service` with a Mockito `spy`.
233+
<1> Wrap the bean named `service` with a Mockito spy.
234+
======
235+
236+
The following `@SharedSpies` annotation registers two spies by-type and one spy by-name.
237+
238+
[tabs]
239+
======
240+
Java::
241+
+
242+
[source,java,indent=0,subs="verbatim,quotes"]
243+
----
244+
@Target(ElementType.TYPE)
245+
@Retention(RetentionPolicy.RUNTIME)
246+
@MockitoSpyBean(types = {OrderService.class, UserService.class}) // <1>
247+
@MockitoSpyBean(name = "ps1", types = PrintingService.class) // <2>
248+
public @interface SharedSpies {
249+
}
250+
----
251+
<1> Register `OrderService` and `UserService` spies by-type.
252+
<2> Register `PrintingService` spy by-name.
253+
======
254+
255+
The following demonstrates how `@SharedSpies` can be used on a test class.
256+
257+
[tabs]
258+
======
259+
Java::
260+
+
261+
[source,java,indent=0,subs="verbatim,quotes"]
262+
----
263+
@SpringJUnitConfig(TestConfig.class)
264+
@SharedSpies // <1>
265+
class BeanOverrideTests {
266+
267+
@Autowired OrderService orderService; // <2>
268+
269+
@Autowired UserService userService; // <2>
270+
271+
@Autowired PrintingService ps1; // <2>
272+
273+
// Inject other components that rely on the spies.
274+
275+
@Test
276+
void testThatDependsOnMocks() {
277+
// ...
278+
}
279+
}
280+
----
281+
<1> Register common spies via the custom `@SharedSpies` annotation.
282+
<2> Optionally inject spies to _stub_ or _verify_ them.
237283
======
284+
285+
TIP: The spies can also be injected into `@Configuration` classes or other test-related
286+
components in the `ApplicationContext` in order to configure them with Mockito's stubbing
287+
APIs.

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

+7-6
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
= Bean Overriding in Tests
33

44
Bean overriding in tests refers to the ability to override specific beans in the
5-
`ApplicationContext` for a test class, by annotating one or more non-static fields in the
6-
test class.
5+
`ApplicationContext` for a test class, by annotating the test class or one or more
6+
non-static fields in the test class.
77

88
NOTE: This feature is intended as a less risky alternative to the practice of registering
99
a bean via `@Bean` with the `DefaultListableBeanFactory`
@@ -42,15 +42,16 @@ The `spring-test` module registers implementations of the latter two
4242
{spring-framework-code}/spring-test/src/main/resources/META-INF/spring.factories[`META-INF/spring.factories`
4343
properties file].
4444

45-
The bean overriding infrastructure searches in test classes for any non-static field that
46-
is meta-annotated with `@BeanOverride` and instantiates the corresponding
47-
`BeanOverrideProcessor` which is responsible for creating an appropriate
48-
`BeanOverrideHandler`.
45+
The bean overriding infrastructure searches for annotations on test classes as well as
46+
annotations on non-static fields in test classes that are meta-annotated with
47+
`@BeanOverride` and instantiates the corresponding `BeanOverrideProcessor` which is
48+
responsible for creating an appropriate `BeanOverrideHandler`.
4949

5050
The internal `BeanOverrideBeanFactoryPostProcessor` then uses bean override handlers to
5151
alter the test's `ApplicationContext` by creating, replacing, or wrapping beans as
5252
defined by the corresponding `BeanOverrideStrategy`:
5353

54+
[[testcontext-bean-overriding-strategy]]
5455
`REPLACE`::
5556
Replaces the bean. Throws an exception if a corresponding bean does not exist.
5657
`REPLACE_OR_CREATE`::

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

+15-14
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@
3131

3232
/**
3333
* {@code @MockitoBean} is an annotation that can be used in test classes to
34-
* override beans in a test's
34+
* override a bean in the test's
3535
* {@link org.springframework.context.ApplicationContext ApplicationContext}
36-
* using Mockito mocks.
36+
* with a Mockito mock.
3737
*
3838
* <p>{@code @MockitoBean} can be applied in the following ways.
3939
* <ul>
@@ -49,18 +49,19 @@
4949
* </ul>
5050
*
5151
* <p>When {@code @MockitoBean} is declared on a field, the bean to mock is inferred
52-
* from the type of the annotated field. If multiple candidates exist, a
53-
* {@code @Qualifier} annotation can be declared on the field to help disambiguate.
54-
* In the absence of a {@code @Qualifier} annotation, the name of the annotated
55-
* field will be used as a fallback qualifier. Alternatively, you can explicitly
56-
* specify a bean name to mock by setting the {@link #value() value} or
57-
* {@link #name() name} attribute.
52+
* from the type of the annotated field. If multiple candidates exist in the
53+
* {@code ApplicationContext}, a {@code @Qualifier} annotation can be declared
54+
* on the field to help disambiguate. In the absence of a {@code @Qualifier}
55+
* annotation, the name of the annotated field will be used as a <em>fallback
56+
* qualifier</em>. Alternatively, you can explicitly specify a bean name to mock
57+
* by setting the {@link #value() value} or {@link #name() name} attribute.
5858
*
5959
* <p>When {@code @MockitoBean} is declared at the type level, the type of bean
60-
* to mock must be supplied via the {@link #types() types} attribute. If multiple
61-
* candidates exist, you can explicitly specify a bean name to mock by setting the
62-
* {@link #name() name} attribute. Note, however, that the {@code types} attribute
63-
* must contain a single type if an explicit bean {@code name} is configured.
60+
* (or beans) to mock must be supplied via the {@link #types() types} attribute.
61+
* If multiple candidates exist in the {@code ApplicationContext}, you can
62+
* explicitly specify a bean name to mock by setting the {@link #name() name}
63+
* attribute. Note, however, that the {@code types} attribute must contain a
64+
* single type if an explicit bean {@code name} is configured.
6465
*
6566
* <p>A bean will be created if a corresponding bean does not exist. However, if
6667
* you would like for the test to fail when a corresponding bean does not exist,
@@ -111,7 +112,7 @@
111112
public @interface MockitoBean {
112113

113114
/**
114-
* Alias for {@link #name()}.
115+
* Alias for {@link #name() name}.
115116
* <p>Intended to be used when no other attributes are needed &mdash; for
116117
* example, {@code @MockitoBean("customBeanName")}.
117118
* @see #name()
@@ -136,7 +137,7 @@
136137
* <p>Each type specified will result in a mock being created and registered
137138
* with the {@code ApplicationContext}.
138139
* <p>Types must be omitted when the annotation is used on a field.
139-
* <p>When {@code @MockitoBean} also defines a {@link #name}, this attribute
140+
* <p>When {@code @MockitoBean} also defines a {@link #name name}, this attribute
140141
* can only contain a single value.
141142
* @return the types to mock
142143
* @since 6.2.2

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

+30-15
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ public AbstractMockitoBeanOverrideHandler createHandler(Annotation overrideAnnot
4545
"The @MockitoBean 'types' attribute must be omitted when declared on a field");
4646
return new MockitoBeanOverrideHandler(field, ResolvableType.forField(field, testClass), mockitoBean);
4747
}
48-
else if (overrideAnnotation instanceof MockitoSpyBean spyBean) {
49-
return new MockitoSpyBeanOverrideHandler(field, ResolvableType.forField(field, testClass), spyBean);
48+
else if (overrideAnnotation instanceof MockitoSpyBean mockitoSpyBean) {
49+
Assert.state(mockitoSpyBean.types().length == 0,
50+
"The @MockitoSpyBean 'types' attribute must be omitted when declared on a field");
51+
return new MockitoSpyBeanOverrideHandler(field, ResolvableType.forField(field, testClass), mockitoSpyBean);
5052
}
5153
throw new IllegalStateException("""
5254
Invalid annotation passed to MockitoBeanOverrideProcessor: \
@@ -56,21 +58,34 @@ else if (overrideAnnotation instanceof MockitoSpyBean spyBean) {
5658

5759
@Override
5860
public List<BeanOverrideHandler> createHandlers(Annotation overrideAnnotation, Class<?> testClass) {
59-
if (!(overrideAnnotation instanceof MockitoBean mockitoBean)) {
60-
throw new IllegalStateException("""
61-
Invalid annotation passed to MockitoBeanOverrideProcessor: \
62-
expected @MockitoBean on test class """ + testClass.getName());
61+
if (overrideAnnotation instanceof MockitoBean mockitoBean) {
62+
Class<?>[] types = mockitoBean.types();
63+
Assert.state(types.length > 0,
64+
"The @MockitoBean 'types' attribute must not be empty when declared on a class");
65+
Assert.state(mockitoBean.name().isEmpty() || types.length == 1,
66+
"The @MockitoBean 'name' attribute cannot be used when mocking multiple types");
67+
List<BeanOverrideHandler> handlers = new ArrayList<>();
68+
for (Class<?> type : types) {
69+
handlers.add(new MockitoBeanOverrideHandler(ResolvableType.forClass(type), mockitoBean));
70+
}
71+
return handlers;
6372
}
64-
Class<?>[] types = mockitoBean.types();
65-
Assert.state(types.length > 0,
66-
"The @MockitoBean 'types' attribute must not be empty when declared on a class");
67-
Assert.state(mockitoBean.name().isEmpty() || types.length == 1,
68-
"The @MockitoBean 'name' attribute cannot be used when mocking multiple types");
69-
List<BeanOverrideHandler> handlers = new ArrayList<>();
70-
for (Class<?> type : types) {
71-
handlers.add(new MockitoBeanOverrideHandler(ResolvableType.forClass(type), mockitoBean));
73+
else if (overrideAnnotation instanceof MockitoSpyBean mockitoSpyBean) {
74+
Class<?>[] types = mockitoSpyBean.types();
75+
Assert.state(types.length > 0,
76+
"The @MockitoSpyBean 'types' attribute must not be empty when declared on a class");
77+
Assert.state(mockitoSpyBean.name().isEmpty() || types.length == 1,
78+
"The @MockitoSpyBean 'name' attribute cannot be used when mocking multiple types");
79+
List<BeanOverrideHandler> handlers = new ArrayList<>();
80+
for (Class<?> type : types) {
81+
handlers.add(new MockitoSpyBeanOverrideHandler(ResolvableType.forClass(type), mockitoSpyBean));
82+
}
83+
return handlers;
7284
}
73-
return handlers;
85+
throw new IllegalStateException("""
86+
Invalid annotation passed to MockitoBeanOverrideProcessor: \
87+
expected either @MockitoBean or @MockitoSpyBean on test class %s"""
88+
.formatted(testClass.getName()));
7489
}
7590

7691
}

0 commit comments

Comments
 (0)