Skip to content

Commit 9181cce

Browse files
committed
Support @⁠MockitoBean at the type level on test classes
Prior to this commit, @⁠MockitoBean could only be declared on fields within test classes, which prevented developers from being able to easily reuse mock configuration across a test suite. With this commit, @⁠MockitoBean is now supported at the type level on test classes, their superclasses, and interfaces implemented by those classes. @⁠MockitoBean is also supported on enclosing classes for @⁠Nested test classes, their superclasses, and interfaces implemented by those classes, while honoring @⁠NestedTestConfiguration semantics. In addition, @⁠MockitoBean: - has a new `types` attribute that can be used to declare the type or types to mock when @⁠MockitoBean 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 @⁠SharedMocks example in the reference manual) To support these new features, this commit also includes the following changes. - The `field` property in BeanOverrideHandler is now @⁠Nullable. - BeanOverrideProcessor has a new `default` createHandlers() method which is invoked when a @⁠BeanOverride annotation is found at the type level. - MockitoBeanOverrideProcessor implements the new createHandlers() method. - The internal findHandlers() method in BeanOverrideHandler has been completely overhauled. - The @⁠MockitoBean and @⁠MockitoSpyBean section of the reference manual has been completely overhauled. Closes gh-33925
1 parent 8b6523a commit 9181cce

36 files changed

+1379
-144
lines changed

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

+115-27
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,21 @@ If multiple candidates match, `@Qualifier` can be provided to narrow the candida
1111
override. Alternatively, a candidate whose bean name matches the name of the field will
1212
match.
1313

14-
When using `@MockitoBean`, a new bean will be created if a corresponding bean does not
15-
exist. However, if you would like for the test to fail when a corresponding bean does not
16-
exist, you can set the `enforceOverride` attribute to `true` – for example,
17-
`@MockitoBean(enforceOverride = true)`.
18-
19-
To use a by-name override rather than a by-type override, specify the `name` attribute
20-
of the annotation.
21-
2214
[WARNING]
2315
====
2416
Qualifiers, including the name of the field, are used to determine if a separate
2517
`ApplicationContext` needs to be created. If you are using this feature to mock or spy
26-
the same bean in several tests, make sure to name the field consistently to avoid
18+
the same bean in several test classes, make sure to name the field consistently to avoid
2719
creating unnecessary contexts.
2820
====
2921

30-
Each annotation also defines Mockito-specific attributes to fine-tune the mocking details.
22+
Each annotation also defines Mockito-specific attributes to fine-tune the mocking behavior.
3123

32-
By default, the `@MockitoBean` annotation uses the `REPLACE_OR_CREATE`
24+
The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE`
3325
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding].
34-
If no existing bean matches, a new bean is created on the fly. As mentioned previously,
35-
you can switch to the `REPLACE` strategy by setting the `enforceOverride` attribute to
36-
`true`.
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.
3729

3830
The `@MockitoSpyBean` annotation uses the `WRAP`
3931
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy],
@@ -61,6 +53,17 @@ Such fields can therefore be `public`, `protected`, package-private (default vis
6153
or `private` depending on the needs or coding practices of the project.
6254
====
6355

56+
[[spring-testing-annotation-beanoverriding-mockitobean-examples]]
57+
== `@MockitoBean` Examples
58+
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+
6467
The following example shows how to use the default behavior of the `@MockitoBean` annotation:
6568

6669
[tabs]
@@ -69,11 +72,13 @@ Java::
6972
+
7073
[source,java,indent=0,subs="verbatim,quotes"]
7174
----
72-
class OverrideBeanTests {
75+
@SpringJUnitConfig(TestConfig.class)
76+
class BeanOverrideTests {
77+
7378
@MockitoBean // <1>
7479
CustomService customService;
7580
76-
// test case body...
81+
// tests...
7782
}
7883
----
7984
<1> Replace the bean with type `CustomService` with a Mockito `mock`.
@@ -82,8 +87,8 @@ Java::
8287
In the example above, we are creating a mock for `CustomService`. If more than one bean
8388
of that type exists, the bean named `customService` is considered. Otherwise, the test
8489
will fail, and you will need to provide a qualifier of some sort to identify which of the
85-
`CustomService` beans you want to override. If no such bean exists, a bean definition
86-
will be created with an auto-generated bean name.
90+
`CustomService` beans you want to override. If no such bean exists, a bean will be
91+
created with an auto-generated bean name.
8792

8893
The following example uses a by-name lookup, rather than a by-type lookup:
8994

@@ -93,32 +98,114 @@ Java::
9398
+
9499
[source,java,indent=0,subs="verbatim,quotes"]
95100
----
96-
class OverrideBeanTests {
101+
@SpringJUnitConfig(TestConfig.class)
102+
class BeanOverrideTests {
103+
97104
@MockitoBean("service") // <1>
98105
CustomService customService;
99106
100-
// test case body...
107+
// tests...
101108
102109
}
103110
----
104111
<1> Replace the bean named `service` with a Mockito `mock`.
105112
======
106113

107-
If no bean definition named `service` exists, one is created.
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
108122

109-
The following example shows how to use the default behavior of the `@MockitoSpyBean` annotation:
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+
137+
The following `@SharedMocks` annotation registers two mocks by-type and one mock by-name.
110138

111139
[tabs]
112140
======
113141
Java::
114142
+
115143
[source,java,indent=0,subs="verbatim,quotes"]
116144
----
117-
class OverrideBeanTests {
145+
@Target(ElementType.TYPE)
146+
@Retention(RetentionPolicy.RUNTIME)
147+
@MockitoBean(types = {OrderService.class, UserService.class}) // <1>
148+
@MockitoBean(name = "ps1", types = PrintingService.class) // <2>
149+
public @interface SharedMocks {
150+
}
151+
----
152+
<1> Register `OrderService` and `UserService` mocks by-type.
153+
<2> Register `PrintingService` mock by-name.
154+
======
155+
156+
The following demonstrates how `@SharedMocks` can be used on a test class.
157+
158+
[tabs]
159+
======
160+
Java::
161+
+
162+
[source,java,indent=0,subs="verbatim,quotes"]
163+
----
164+
@SpringJUnitConfig(TestConfig.class)
165+
@SharedMocks // <1>
166+
class BeanOverrideTests {
167+
168+
@Autowired OrderService orderService; // <2>
169+
170+
@Autowired UserService userService; // <2>
171+
172+
@Autowired PrintingService ps1; // <2>
173+
174+
// Inject other components that rely on the mocks.
175+
176+
@Test
177+
void testThatDependsOnMocks() {
178+
// ...
179+
}
180+
}
181+
----
182+
<1> Register common mocks via the custom `@SharedMocks` annotation.
183+
<2> Optionally inject mocks to _stub_ or _verify_ them.
184+
======
185+
186+
TIP: The mocks can also be injected into `@Configuration` classes or other test-related
187+
components in the `ApplicationContext` in order to configure them with Mockito's stubbing
188+
APIs.
189+
190+
[[spring-testing-annotation-beanoverriding-mockitospybean-examples]]
191+
== `@MockitoSpyBean` Examples
192+
193+
The following example shows how to use the default behavior of the `@MockitoSpyBean`
194+
annotation:
195+
196+
[tabs]
197+
======
198+
Java::
199+
+
200+
[source,java,indent=0,subs="verbatim,quotes"]
201+
----
202+
@SpringJUnitConfig(TestConfig.class)
203+
class BeanOverrideTests {
204+
118205
@MockitoSpyBean // <1>
119206
CustomService customService;
120207
121-
// test case body...
208+
// tests...
122209
}
123210
----
124211
<1> Wrap the bean with type `CustomService` with a Mockito `spy`.
@@ -137,12 +224,13 @@ Java::
137224
+
138225
[source,java,indent=0,subs="verbatim,quotes"]
139226
----
140-
class OverrideBeanTests {
227+
@SpringJUnitConfig(TestConfig.class)
228+
class BeanOverrideTests {
229+
141230
@MockitoSpyBean("service") // <1>
142231
CustomService customService;
143232
144-
// test case body...
145-
233+
// tests...
146234
}
147235
----
148236
<1> Wrap the bean named `service` with a Mockito `spy`.

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

+8-4
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,19 @@
2525
import org.springframework.aot.hint.annotation.Reflective;
2626

2727
/**
28-
* Mark a composed annotation as eligible for Bean Override processing.
28+
* Mark a <em>composed annotation</em> as eligible for Bean Override processing.
2929
*
3030
* <p>Specifying this annotation registers the configured {@link BeanOverrideProcessor}
3131
* which must be capable of handling the composed annotation and its attributes.
3232
*
33-
* <p>Since the composed annotation should only be applied to non-static fields, it is
34-
* expected that it is meta-annotated with {@link Target @Target(ElementType.FIELD)}.
33+
* <p>Since the composed annotation will typically only be applied to non-static
34+
* fields, it is expected that the composed annotation is meta-annotated with
35+
* {@link Target @Target(ElementType.FIELD)}. However, certain bean override
36+
* annotations may be declared with an additional {@code ElementType.TYPE} target
37+
* for use at the type level, as is the case for {@code @MockitoBean} which can
38+
* be declared on a field, test class, or test interface.
3539
*
36-
* <p>For concrete examples, see
40+
* <p>For concrete examples of such composed annotations, see
3741
* {@link org.springframework.test.context.bean.override.convention.TestBean @TestBean},
3842
* {@link org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean}, and
3943
* {@link org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean}.

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

+39-31
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -104,11 +104,10 @@ private void registerBeanOverride(ConfigurableListableBeanFactory beanFactory, B
104104
Set<String> generatedBeanNames) {
105105

106106
String beanName = handler.getBeanName();
107-
Field field = handler.getField();
108-
Assert.state(!BeanFactoryUtils.isFactoryDereference(beanName),() -> """
109-
Unable to override bean '%s' for field '%s.%s': a FactoryBean cannot be overridden. \
110-
To override the bean created by the FactoryBean, remove the '&' prefix.""".formatted(
111-
beanName, field.getDeclaringClass().getSimpleName(), field.getName()));
107+
Assert.state(!BeanFactoryUtils.isFactoryDereference(beanName), () -> """
108+
Unable to override bean '%s'%s: a FactoryBean cannot be overridden. \
109+
To override the bean created by the FactoryBean, remove the '&' prefix."""
110+
.formatted(beanName, forField(handler.getField())));
112111

113112
switch (handler.getStrategy()) {
114113
case REPLACE -> replaceOrCreateBean(beanFactory, handler, generatedBeanNames, true);
@@ -134,7 +133,6 @@ private void replaceOrCreateBean(ConfigurableListableBeanFactory beanFactory, Be
134133
// 4) Create bean by-name, with a provided name
135134

136135
String beanName = handler.getBeanName();
137-
Field field = handler.getField();
138136
BeanDefinition existingBeanDefinition = null;
139137
if (beanName == null) {
140138
beanName = getBeanNameForType(beanFactory, handler, requireExistingBean);
@@ -169,11 +167,10 @@ private void replaceOrCreateBean(ConfigurableListableBeanFactory beanFactory, Be
169167
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
170168
}
171169
else if (requireExistingBean) {
172-
throw new IllegalStateException("""
173-
Unable to replace bean: there is no bean with name '%s' and type %s \
174-
(as required by field '%s.%s')."""
175-
.formatted(beanName, handler.getBeanType(),
176-
field.getDeclaringClass().getSimpleName(), field.getName()));
170+
Field field = handler.getField();
171+
throw new IllegalStateException(
172+
"Unable to replace bean: there is no bean with name '%s' and type %s%s."
173+
.formatted(beanName, handler.getBeanType(), requiredByField(field)));
177174
}
178175
// 4) We are creating a bean by-name with the provided beanName.
179176
}
@@ -264,13 +261,11 @@ private void wrapBean(ConfigurableListableBeanFactory beanFactory, BeanOverrideH
264261
else {
265262
String message = "Unable to select a bean to wrap: ";
266263
if (candidateCount == 0) {
267-
message += "there are no beans of type %s (as required by field '%s.%s')."
268-
.formatted(beanType, field.getDeclaringClass().getSimpleName(), field.getName());
264+
message += "there are no beans of type %s%s.".formatted(beanType, requiredByField(field));
269265
}
270266
else {
271-
message += "found %d beans of type %s (as required by field '%s.%s'): %s"
272-
.formatted(candidateCount, beanType, field.getDeclaringClass().getSimpleName(),
273-
field.getName(), candidateNames);
267+
message += "found %d beans of type %s%s: %s"
268+
.formatted(candidateCount, beanType, requiredByField(field), candidateNames);
274269
}
275270
throw new IllegalStateException(message);
276271
}
@@ -281,11 +276,9 @@ private void wrapBean(ConfigurableListableBeanFactory beanFactory, BeanOverrideH
281276
// We are wrapping an existing bean by-name.
282277
Set<String> candidates = getExistingBeanNamesByType(beanFactory, handler, false);
283278
if (!candidates.contains(beanName)) {
284-
throw new IllegalStateException("""
285-
Unable to wrap bean: there is no bean with name '%s' and type %s \
286-
(as required by field '%s.%s')."""
287-
.formatted(beanName, beanType, field.getDeclaringClass().getSimpleName(),
288-
field.getName()));
279+
throw new IllegalStateException(
280+
"Unable to wrap bean: there is no bean with name '%s' and type %s%s."
281+
.formatted(beanName, beanType, requiredByField(field)));
289282
}
290283
}
291284

@@ -308,8 +301,8 @@ private String getBeanNameForType(ConfigurableListableBeanFactory beanFactory, B
308301
else if (candidateCount == 0) {
309302
if (requireExistingBean) {
310303
throw new IllegalStateException(
311-
"Unable to override bean: there are no beans of type %s (as required by field '%s.%s')."
312-
.formatted(beanType, field.getDeclaringClass().getSimpleName(), field.getName()));
304+
"Unable to override bean: there are no beans of type %s%s."
305+
.formatted(beanType, requiredByField(field)));
313306
}
314307
return null;
315308
}
@@ -320,14 +313,14 @@ else if (candidateCount == 0) {
320313
}
321314

322315
throw new IllegalStateException(
323-
"Unable to select a bean to override: found %d beans of type %s (as required by field '%s.%s'): %s"
324-
.formatted(candidateCount, beanType, field.getDeclaringClass().getSimpleName(),
325-
field.getName(), candidateNames));
316+
"Unable to select a bean to override: found %d beans of type %s%s: %s"
317+
.formatted(candidateCount, beanType, requiredByField(field), candidateNames));
326318
}
327319

328320
private Set<String> getExistingBeanNamesByType(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler,
329321
boolean checkAutowiredCandidate) {
330322

323+
Field field = handler.getField();
331324
ResolvableType resolvableType = handler.getBeanType();
332325
Class<?> type = resolvableType.toClass();
333326

@@ -345,16 +338,16 @@ private Set<String> getExistingBeanNamesByType(ConfigurableListableBeanFactory b
345338
}
346339

347340
// Filter out non-matching autowire candidates.
348-
if (checkAutowiredCandidate) {
349-
DependencyDescriptor descriptor = new DependencyDescriptor(handler.getField(), true);
341+
if (field != null && checkAutowiredCandidate) {
342+
DependencyDescriptor descriptor = new DependencyDescriptor(field, true);
350343
beanNames.removeIf(beanName -> !beanFactory.isAutowireCandidate(beanName, descriptor));
351344
}
352345
// Filter out scoped proxy targets.
353346
beanNames.removeIf(ScopedProxyUtils::isScopedTarget);
354347

355348
// In case of multiple matches, fall back on the field's name as a last resort.
356-
if (beanNames.size() > 1) {
357-
String fieldName = handler.getField().getName();
349+
if (field != null && beanNames.size() > 1) {
350+
String fieldName = field.getName();
358351
if (beanNames.contains(fieldName)) {
359352
return Set.of(fieldName);
360353
}
@@ -452,4 +445,19 @@ private static void destroySingleton(ConfigurableListableBeanFactory beanFactory
452445
dlbf.destroySingleton(beanName);
453446
}
454447

448+
private static String forField(@Nullable Field field) {
449+
if (field == null) {
450+
return "";
451+
}
452+
return " for field '%s.%s'".formatted(field.getDeclaringClass().getSimpleName(), field.getName());
453+
}
454+
455+
private static String requiredByField(@Nullable Field field) {
456+
if (field == null) {
457+
return "";
458+
}
459+
return " (as required by field '%s.%s')".formatted(
460+
field.getDeclaringClass().getSimpleName(), field.getName());
461+
}
462+
455463
}

0 commit comments

Comments
 (0)