Skip to content

Commit 28f62ab

Browse files
committed
Use the field name as a fallback qualifier for Bean Overriding
This commit harmonizes how a candidate bean definition is determined for overriding using `@TestBean`, `@MockitoBean`, and `@MockitoSpyBean`. Previously, a qualifier was necessary even if the name of the annotated field matches the name of a candidate. After this commit, such candidate will be picked up transparently, the same it is done for regular autowiring. This commit also reviews the documentation of the feature as considering the field means that its name is taken into account to compute a cache key if by-type lookup is requested. Closes gh-32939
1 parent 4c73747 commit 28f62ab

File tree

10 files changed

+357
-95
lines changed

10 files changed

+357
-95
lines changed

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

+86-14
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,35 @@ case, the original bean definition is not replaced, but instead an early instanc
77
bean is captured and wrapped by the spy.
88

99
By default, the annotated field's type is used to search for candidate definitions to
10-
override, but note that `@Qualifier` annotations are also taken into account for the
11-
purpose of matching. Users can also make things entirely explicit by specifying a bean
12-
`name` in the annotation.
10+
override. If multiple candidates match, the usual `@Qualifier` can be provided to
11+
narrow the candidate to override. Alternatively, a candidate whose bean definition name
12+
matches the name of the field will match.
13+
14+
To use a by-name override rather than a by-type override, specify the `name` attribute
15+
of the annotation.
16+
17+
[WARNING]
18+
====
19+
The qualifiers, including the name of the field are used to determine if a separate
20+
`ApplicationContext` needs to be created. If you are using this feature to mock or
21+
spy the same bean in several tests, make sure to name the field consistently to avoid
22+
creating unnecessary contexts.
23+
====
1324

1425
Each annotation also defines Mockito-specific attributes to fine-tune the mocking details.
1526

1627
The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE_DEFINITION`
1728
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding].
1829

19-
It requires that at most one matching candidate definition exists if a bean name
20-
is specified, or exactly one if no bean name is specified.
30+
If no definition matches, then a definition is created on-the-fly.
2131

2232
The `@MockitoSpyBean` annotation uses the `WRAP_BEAN`
2333
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy],
2434
and the original instance is wrapped in a Mockito spy.
2535

2636
It requires that exactly one candidate definition exists.
2737

28-
The following example shows how to configure the bean name via `@MockitoBean` and
29-
`@MockitoSpyBean`:
38+
The following example shows how to use the default behavior of the `@MockitoBean` annotation:
3039

3140
[tabs]
3241
======
@@ -35,17 +44,80 @@ Java::
3544
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
3645
----
3746
class OverrideBeanTests {
47+
@MockitoBean // <1>
48+
private CustomService customService;
49+
50+
// test case body...
51+
}
52+
----
53+
<1> Replace the bean with type `CustomService` with a Mockito `mock`.
54+
======
55+
56+
In the example above, we are creating a mock for `CustomService`. If more that
57+
one bean with such type exist, the bean named `customService` is considered. Otherwise,
58+
the test will fail and you will need to provide a qualifier of some sort to identify which
59+
of the `CustomService` beans you want to override. If no such bean exists, a bean
60+
definition will be created with an auto-generated bean name.
3861

39-
@MockitoBean(name = "service1") // <1>
40-
private CustomService mockService;
62+
The following example uses a by-name lookup, rather than a by-type lookup:
4163

42-
@MockitoSpyBean(name = "service2") // <2>
43-
private CustomService spyService; // <3>
64+
[tabs]
65+
======
66+
Java::
67+
+
68+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
69+
----
70+
class OverrideBeanTests {
71+
@MockitoBean(name = "service") // <1>
72+
private CustomService customService;
4473
4574
// test case body...
75+
76+
}
77+
----
78+
<1> Replace the bean named `service` with a Mockito `mock`.
79+
======
80+
81+
If no bean definition named `service` exists, one is created.
82+
83+
The following example shows how to use the default behavior of the `@MockitoSpyBean` annotation:
84+
85+
[tabs]
86+
======
87+
Java::
88+
+
89+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
90+
----
91+
class OverrideBeanTests {
92+
@MockitoSpyBean // <1>
93+
private CustomService customService;
94+
95+
// test case body...
96+
}
97+
----
98+
<1> Wrap the bean with type `CustomService` with a Mockito `spy`.
99+
======
100+
101+
In the example above, we are wrapping the bean with type `CustomService`. If more that
102+
one bean with such type exist, the bean named `customService` is considered. Otherwise,
103+
the test will fail and you will need to provide a qualifier of some sort to identify which
104+
of the `CustomService` beans you want to spy.
105+
106+
The following example uses a by-name lookup, rather than a by-type lookup:
107+
108+
[tabs]
109+
======
110+
Java::
111+
+
112+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
113+
----
114+
class OverrideBeanTests {
115+
@MockitoSpyBean(name = "service") // <1>
116+
private CustomService customService;
117+
118+
// test case body...
119+
46120
}
47121
----
48-
<1> Mark `mockService` as a Mockito mock override of bean `service1` in this test class.
49-
<2> Mark `spyService` as a Mockito spy override of bean `service2` in this test class.
50-
<3> The fields will be injected with the Mockito mock and spy, respectively.
122+
<1> Wrap the bean named `service` with a Mockito `spy`.
51123
======

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

+23-8
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,21 @@ with the type of the bean to override is expected. To make things more explicit,
1111
you'd rather use a different name, the annotation allows for a specific method name to
1212
be provided.
1313

14+
By default, the annotated field's type is used to search for candidate definitions to
15+
override. If multiple candidates match, the usual `@Qualifier` can be provided to
16+
narrow the candidate to override. Alternatively, a candidate whose bean definition name
17+
matches the name of the field will match.
1418

15-
By default, the annotated field's type is used to search for candidate definitions to override.
16-
In that case it is required that exactly one definition matches, but note that `@Qualifier`
17-
annotations are also taken into account for the purpose of matching.
18-
Users can also make things entirely explicit by specifying a bean `name` in the annotation.
19+
To use a by-name override rather than a by-type override, specify the `name` attribute
20+
of the annotation.
21+
22+
[WARNING]
23+
====
24+
The qualifiers, including the name of the field are used to determine if a separate
25+
`ApplicationContext` needs to be created. If you are using this feature to override
26+
the same bean in several tests, make sure to name the field consistently to avoid
27+
creating unnecessary contexts.
28+
====
1929

2030
The following example shows how to use the default behavior of the `@TestBean` annotation:
2131

@@ -27,11 +37,11 @@ Java::
2737
----
2838
class OverrideBeanTests {
2939
@TestBean // <1>
30-
private CustomService service;
40+
private CustomService customService;
3141
3242
// test case body...
3343
34-
private static CustomService service() { // <2>
44+
private static CustomService customService() { // <2>
3545
return new MyFakeCustomService();
3646
}
3747
}
@@ -40,8 +50,13 @@ Java::
4050
<2> The result of this static method will be used as the instance and injected into the field.
4151
======
4252

53+
In the example above, we are overriding the bean with type `CustomService`. If more that
54+
one bean with such type exist, the bean named `customService` is considered. Otherwise,
55+
the test will fail and you will need to provide a qualifier of some sort to identify which
56+
of the `CustomService` beans you want to override.
57+
4358

44-
The following example shows how to fully configure the `@TestBean` annotation:
59+
The following example uses a by-name lookup, rather than a by-type lookup:
4560

4661
[tabs]
4762
======
@@ -51,7 +66,7 @@ Java::
5166
----
5267
class OverrideBeanTests {
5368
@TestBean(name = "service", methodName = "createCustomService") // <1>
54-
private CustomService service;
69+
private CustomService customService;
5570
5671
// test case body...
5772

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

+7
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,13 @@ private Set<String> getExistingBeanNamesByType(ConfigurableListableBeanFactory b
259259
else {
260260
beans.removeIf(ScopedProxyUtils::isScopedTarget);
261261
}
262+
// In case of multiple matches, last resort fallback on the field's name
263+
if (beans.size() > 1) {
264+
String fieldName = metadata.getField().getName();
265+
if (beans.contains(fieldName)) {
266+
return Set.of(fieldName);
267+
}
268+
}
262269
return beans;
263270
}
264271

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

+13-5
Original file line numberDiff line numberDiff line change
@@ -167,16 +167,24 @@ public boolean equals(Object obj) {
167167
return false;
168168
}
169169
OverrideMetadata that = (OverrideMetadata) obj;
170-
return Objects.equals(this.beanType.getType(), that.beanType.getType()) &&
171-
Objects.equals(this.beanName, that.beanName) &&
172-
Objects.equals(this.strategy, that.strategy) &&
170+
if (!Objects.equals(this.beanType.getType(), that.beanType.getType()) ||
171+
!Objects.equals(this.beanName, that.beanName) ||
172+
!Objects.equals(this.strategy, that.strategy)) {
173+
return false;
174+
}
175+
if (this.beanName != null) {
176+
return true;
177+
}
178+
// by type lookup
179+
return Objects.equals(this.field.getName(), that.field.getName()) &&
173180
Arrays.equals(this.field.getAnnotations(), that.field.getAnnotations());
174181
}
175182

176183
@Override
177184
public int hashCode() {
178-
return Objects.hash(this.beanType.getType(), this.beanName, this.strategy,
179-
Arrays.hashCode(this.field.getAnnotations()));
185+
int hash = Objects.hash(this.beanType.getType(), this.beanName, this.strategy);
186+
return (this.beanName != null ? hash : hash +
187+
Objects.hash(this.field.getName(), Arrays.hashCode(this.field.getAnnotations())));
180188
}
181189

182190
@Override

0 commit comments

Comments
 (0)