Skip to content

Commit a86612a

Browse files
committed
Use by-type semantics in bean overriding if no explicit name is provided
This change switches default behavior of `@TestBean`, `@MockitoBean` and `@MockitoSpyBean` to match the bean definition / bean to override by type in the case there is no explicit bean name provided via the annotation. The previous behavior of using the annotated field's name is still an option for implementors, but no longer the default. Closes gh-32761
1 parent fc07946 commit a86612a

File tree

19 files changed

+484
-74
lines changed

19 files changed

+484
-74
lines changed

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,24 @@ the test's `ApplicationContext` with a Mockito mock or spy, respectively. In the
66
case, the original bean definition is not replaced, but instead an early instance of the
77
bean is captured and wrapped by the spy.
88

9-
By default, the name of the bean to override is derived from the annotated field's name,
10-
but both annotations allow for a specific `name` to be provided. Each annotation also
11-
defines Mockito-specific attributes to fine-tune the mocking details.
9+
Users are encouraged to make bean overriding as explicit and unambiguous as possible,
10+
typically by specifying a bean `name` in the annotation.
11+
If no bean `name` is specified, the annotated field's type is used to search for candidate
12+
definitions to override.
13+
Each annotation also defines Mockito-specific attributes to fine-tune the mocking details.
1214

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

18+
It requires that at most one candidate definition exists if a bean name is specified,
19+
or exactly one if no bean name is specified.
20+
1621
The `@MockitoSpyBean` annotation uses the `WRAP_BEAN`
1722
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy],
1823
and the original instance is wrapped in a Mockito spy.
1924

25+
It requires that exactly one candidate definition exists.
26+
2027
The following example shows how to configure the bean name via `@MockitoBean` and
2128
`@MockitoSpyBean`:
2229

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@
55
`ApplicationContext` with an instance provided by a conventionally named static factory
66
method.
77

8-
By default, the bean name and the associated factory method name are derived from the
9-
annotated field's name, but the annotation allows for specific values to be provided.
8+
By default, the associated factory method name is derived from the annotated field's name,
9+
but the annotation allows for a specific method name to be provided.
1010

1111
The `@TestBean` annotation uses the `REPLACE_DEFINITION`
1212
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding].
1313

14+
Users are encouraged to make bean overriding as explicit and unambiguous as possible,
15+
typically by specifying a bean `name` in the annotation.
16+
If no bean `name` is specified, the annotated field's type is used to search for candidate
17+
definitions to override. In that case it is required that exactly one definition matches.
18+
1419
The following example shows how to fully configure the `@TestBean` annotation, with
1520
explicit values equivalent to the defaults:
1621

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,14 @@ by the corresponding `BeanOverrideStrategy`:
5858
[NOTE]
5959
====
6060
In contrast to Spring's autowiring mechanism (for example, resolution of an `@Autowired`
61-
field), the bean overriding infrastructure in the TestContext framework does not perform
62-
any heuristics to locate a bean. Instead, the name of the bean to override must be
63-
explicitly provided to or computed by the `BeanOverrideProcessor`.
64-
65-
Typically, the user provides the bean name via a custom annotation, or the
66-
`BeanOverrideProcessor` determines the bean name based on some convention.
61+
field), the bean overriding infrastructure in the TestContext framework has limited
62+
heuristics it can perform to locate a bean. Either the `BeanOverrideProcessor` can compute
63+
the name of the bean to override, or it can be unambiguously selected given the type of
64+
the annotated field.
65+
66+
Typically, the user directly provides the bean name in the custom annotation in order to
67+
make things as explicit as possible. Alternatively, the bean is selected by type by the
68+
`BeanOverrideFactoryPostProcessor`.
69+
Some `BeanOverrideProcessor`s could also internally compute a bean name based on a
70+
convention or another advanced method.
6771
====

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

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,17 @@ private void registerReplaceDefinition(ConfigurableListableBeanFactory beanFacto
125125

126126
RootBeanDefinition beanDefinition = createBeanDefinition(overrideMetadata);
127127
String beanName = overrideMetadata.getBeanName();
128+
if (beanName == null) {
129+
final String[] candidates = beanFactory.getBeanNamesForType(overrideMetadata.getBeanType());
130+
if (candidates.length != 1) {
131+
Field f = overrideMetadata.getField();
132+
throw new IllegalStateException("Unable to select a bean definition to override, " +
133+
candidates.length+ " bean definitions found of type " + overrideMetadata.getBeanType() +
134+
" (as required by annotated field '" + f.getDeclaringClass().getSimpleName() +
135+
"." + f.getName() + "')");
136+
}
137+
beanName = candidates[0];
138+
}
128139

129140
BeanDefinition existingBeanDefinition = null;
130141
if (beanFactory.containsBeanDefinition(beanName)) {
@@ -160,9 +171,19 @@ else if (enforceExistingDefinition) {
160171
private void registerWrapBean(ConfigurableListableBeanFactory beanFactory, OverrideMetadata metadata) {
161172
Set<String> existingBeanNames = getExistingBeanNames(beanFactory, metadata.getBeanType());
162173
String beanName = metadata.getBeanName();
163-
if (!existingBeanNames.contains(beanName)) {
164-
throw new IllegalStateException("Unable to override bean '" + beanName + "' by wrapping," +
165-
" no existing bean instance by this name of type " + metadata.getBeanType());
174+
if (beanName == null) {
175+
if (existingBeanNames.size() != 1) {
176+
Field f = metadata.getField();
177+
throw new IllegalStateException("Unable to select a bean to override by wrapping, " +
178+
existingBeanNames.size() + " bean instances found of type " + metadata.getBeanType() +
179+
" (as required by annotated field '" + f.getDeclaringClass().getSimpleName() +
180+
"." + f.getName() + "')");
181+
}
182+
beanName = existingBeanNames.iterator().next();
183+
}
184+
else if (!existingBeanNames.contains(beanName)) {
185+
throw new IllegalStateException("Unable to override bean '" + beanName + "' by wrapping; " +
186+
"there is no existing bean instance with that name of type " + metadata.getBeanType());
166187
}
167188
this.overrideRegistrar.markWrapEarly(metadata, beanName);
168189
this.overrideRegistrar.registerNameForMetadata(metadata, beanName);

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,13 @@ protected OverrideMetadata(Field field, ResolvableType beanType,
5858
}
5959

6060
/**
61-
* Get the bean name to override.
62-
* <p>Defaults to the name of the {@link #getField() field}.
61+
* Get the bean name to override, or {@code null} to look for a single
62+
* matching bean of type {@link #getBeanType()}.
63+
* <p>Defaults to {@code null}.
6364
*/
65+
@Nullable
6466
protected String getBeanName() {
65-
return this.field.getName();
67+
return null;
6668
}
6769

6870
/**

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

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@
2626
import org.springframework.test.context.bean.override.BeanOverride;
2727

2828
/**
29-
* Mark a field to override a bean instance in the {@code BeanFactory}.
29+
* Mark a field to override a bean definition in the {@code BeanFactory}.
30+
*
31+
* <p>By default, the bean to override is inferred from the type of the
32+
* annotated field. This requires that exactly one matching definition is
33+
* present in the application context. To explicitly specify a bean name to
34+
* replace, set the {@link #value()} or {@link #name()} attribute.
3035
*
3136
* <p>The instance is created from a zero-argument static factory method in the
3237
* test class whose return type is compatible with the annotated field. In the
@@ -38,7 +43,7 @@
3843
* that name.</li>
3944
* <li>If a method name is not specified, look for exactly one static method named
4045
* with a suffix equal to {@value #CONVENTION_SUFFIX} and starting with either the
41-
* name of the annotated field or the name of the bean.</li>
46+
* name of the annotated field or the name of the bean (if specified).</li>
4247
* </ul>
4348
*
4449
* <p>Consider the following example.
@@ -62,13 +67,13 @@
6267
* is also replaced in the {@code BeanFactory} so that other injection points
6368
* for that bean use the overridden bean instance.
6469
*
65-
* <p>To make things more explicit, the method name can be set, as shown in the
66-
* following example.
70+
* <p>To make things more explicit, the bean and method names can be set,
71+
* as shown in the following example.
6772
*
6873
* <pre><code>
6974
* class CustomerServiceTests {
7075
*
71-
* &#064;TestBean(methodName = "createTestCustomerRepository")
76+
* &#064;TestBean(name = "repository", methodName = "createTestCustomerRepository")
7277
* private CustomerRepository repository;
7378
*
7479
* // Tests
@@ -78,10 +83,6 @@
7883
* }
7984
* }</code></pre>
8085
*
81-
* <p>By default, the name of the bean to override is inferred from the name of
82-
* the annotated field. To use a different bean name, set the {@link #value()} or
83-
* {@link #name()} attribute.
84-
*
8586
* @author Simon Baslé
8687
* @author Stephane Nicoll
8788
* @author Sam Brannen
@@ -113,8 +114,8 @@
113114

114115
/**
115116
* Name of the bean to override.
116-
* <p>If left unspecified, the name of the bean to override is the name of
117-
* the annotated field. If specified, the field name is ignored.
117+
* <p>If left unspecified, the bean to override is selected according to
118+
* the annotated field's type.
118119
* @see #value()
119120
*/
120121
@AliasFor("value")

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,14 +144,14 @@ public TestBeanOverrideMetadata(Field field, Method overrideMethod, TestBean ove
144144
ResolvableType typeToOverride) {
145145

146146
super(field, typeToOverride, BeanOverrideStrategy.REPLACE_DEFINITION);
147-
this.beanName = StringUtils.hasText(overrideAnnotation.name()) ?
148-
overrideAnnotation.name() : field.getName();
147+
this.beanName = overrideAnnotation.name();
149148
this.overrideMethod = overrideMethod;
150149
}
151150

152151
@Override
152+
@Nullable
153153
protected String getBeanName() {
154-
return this.beanName;
154+
return StringUtils.hasText(this.beanName) ? this.beanName : super.getBeanName();
155155
}
156156

157157
@Override

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,14 @@
2828
import org.springframework.test.context.bean.override.BeanOverride;
2929

3030
/**
31-
* Mark a field to trigger a bean override using a Mockito mock. If no explicit
32-
* {@link #name()} is specified, the annotated field's name is interpreted to
33-
* be the target of the override. In either case, if no existing bean is defined
34-
* a new one will be added to the context.
31+
* Mark a field to trigger a bean override using a Mockito mock.
32+
*
33+
* <p>If no explicit {@link #name()} is specified, a target bean definition is
34+
* selected according to the class of the annotated field, and there must be
35+
* exactly one such candidate definition in the context.
36+
* If a {@link #name()} is specified, either the definition exists in the
37+
* application context and is replaced, or it doesn't and a new one is added to
38+
* the context.
3539
*
3640
* <p>Dependencies that are known to the application context but are not beans
3741
* (such as those
@@ -51,7 +55,8 @@
5155

5256
/**
5357
* The name of the bean to register or replace.
54-
* <p>If not specified, the name of the annotated field will be used.
58+
* <p>If left unspecified, the bean to override is selected according to
59+
* the annotated field's type.
5560
* @return the name of the mocked bean
5661
*/
5762
String name() default "";

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,9 @@ abstract class MockitoMetadata extends OverrideMetadata {
5454

5555

5656
@Override
57+
@Nullable
5758
protected String getBeanName() {
58-
if (StringUtils.hasText(this.name)) {
59-
return this.name;
60-
}
61-
return super.getBeanName();
59+
return StringUtils.hasText(this.name) ? this.name : super.getBeanName();
6260
}
6361

6462
@Override

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,13 @@
2828

2929
/**
3030
* Mark a field to trigger a bean override using a Mockito spy, which will wrap
31-
* the original instance. If no explicit {@link #name()} is specified, the
32-
* annotated field's name is interpreted to be the target of the override.
33-
* In either case, it is required that the target bean is previously registered
34-
* in the context.
31+
* the original instance.
32+
*
33+
* <p>If no explicit {@link #name()} is specified, a target bean is selected
34+
* according to the class of the annotated field, and there must be exactly one
35+
* such candidate bean.
36+
* If a {@link #name()} is specified, it is required that a target bean of that
37+
* name has been previously registered in the application context.
3538
*
3639
* <p>Dependencies that are known to the application context but are not beans
3740
* (such as those
@@ -50,7 +53,8 @@
5053

5154
/**
5255
* The name of the bean to spy.
53-
* <p>If not specified, the name of the annotated field will be used.
56+
* <p>If left unspecified, the bean to override is selected according to
57+
* the annotated field's type.
5458
* @return the name of the spied bean
5559
*/
5660
String name() default "";

0 commit comments

Comments
 (0)