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
This commit is contained in:
Simon Baslé
2024-05-14 12:35:22 +02:00
parent fc07946e60
commit a86612a254
19 changed files with 483 additions and 73 deletions

View File

@@ -125,6 +125,17 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
RootBeanDefinition beanDefinition = createBeanDefinition(overrideMetadata);
String beanName = overrideMetadata.getBeanName();
if (beanName == null) {
final String[] candidates = beanFactory.getBeanNamesForType(overrideMetadata.getBeanType());
if (candidates.length != 1) {
Field f = overrideMetadata.getField();
throw new IllegalStateException("Unable to select a bean definition to override, " +
candidates.length+ " bean definitions found of type " + overrideMetadata.getBeanType() +
" (as required by annotated field '" + f.getDeclaringClass().getSimpleName() +
"." + f.getName() + "')");
}
beanName = candidates[0];
}
BeanDefinition existingBeanDefinition = null;
if (beanFactory.containsBeanDefinition(beanName)) {
@@ -160,9 +171,19 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
private void registerWrapBean(ConfigurableListableBeanFactory beanFactory, OverrideMetadata metadata) {
Set<String> existingBeanNames = getExistingBeanNames(beanFactory, metadata.getBeanType());
String beanName = metadata.getBeanName();
if (!existingBeanNames.contains(beanName)) {
throw new IllegalStateException("Unable to override bean '" + beanName + "' by wrapping," +
" no existing bean instance by this name of type " + metadata.getBeanType());
if (beanName == null) {
if (existingBeanNames.size() != 1) {
Field f = metadata.getField();
throw new IllegalStateException("Unable to select a bean to override by wrapping, " +
existingBeanNames.size() + " bean instances found of type " + metadata.getBeanType() +
" (as required by annotated field '" + f.getDeclaringClass().getSimpleName() +
"." + f.getName() + "')");
}
beanName = existingBeanNames.iterator().next();
}
else if (!existingBeanNames.contains(beanName)) {
throw new IllegalStateException("Unable to override bean '" + beanName + "' by wrapping; " +
"there is no existing bean instance with that name of type " + metadata.getBeanType());
}
this.overrideRegistrar.markWrapEarly(metadata, beanName);
this.overrideRegistrar.registerNameForMetadata(metadata, beanName);

View File

@@ -58,11 +58,13 @@ public abstract class OverrideMetadata {
}
/**
* Get the bean name to override.
* <p>Defaults to the name of the {@link #getField() field}.
* Get the bean name to override, or {@code null} to look for a single
* matching bean of type {@link #getBeanType()}.
* <p>Defaults to {@code null}.
*/
@Nullable
protected String getBeanName() {
return this.field.getName();
return null;
}
/**

View File

@@ -26,7 +26,12 @@ import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.bean.override.BeanOverride;
/**
* Mark a field to override a bean instance in the {@code BeanFactory}.
* Mark a field to override a bean definition in the {@code BeanFactory}.
*
* <p>By default, the bean to override is inferred from the type of the
* annotated field. This requires that exactly one matching definition is
* present in the application context. To explicitly specify a bean name to
* replace, set the {@link #value()} or {@link #name()} attribute.
*
* <p>The instance is created from a zero-argument static factory method in the
* test class whose return type is compatible with the annotated field. In the
@@ -38,7 +43,7 @@ import org.springframework.test.context.bean.override.BeanOverride;
* that name.</li>
* <li>If a method name is not specified, look for exactly one static method named
* with a suffix equal to {@value #CONVENTION_SUFFIX} and starting with either the
* name of the annotated field or the name of the bean.</li>
* name of the annotated field or the name of the bean (if specified).</li>
* </ul>
*
* <p>Consider the following example.
@@ -62,13 +67,13 @@ import org.springframework.test.context.bean.override.BeanOverride;
* is also replaced in the {@code BeanFactory} so that other injection points
* for that bean use the overridden bean instance.
*
* <p>To make things more explicit, the method name can be set, as shown in the
* following example.
* <p>To make things more explicit, the bean and method names can be set,
* as shown in the following example.
*
* <pre><code>
* class CustomerServiceTests {
*
* &#064;TestBean(methodName = "createTestCustomerRepository")
* &#064;TestBean(name = "repository", methodName = "createTestCustomerRepository")
* private CustomerRepository repository;
*
* // Tests
@@ -78,10 +83,6 @@ import org.springframework.test.context.bean.override.BeanOverride;
* }
* }</code></pre>
*
* <p>By default, the name of the bean to override is inferred from the name of
* the annotated field. To use a different bean name, set the {@link #value()} or
* {@link #name()} attribute.
*
* @author Simon Baslé
* @author Stephane Nicoll
* @author Sam Brannen
@@ -113,8 +114,8 @@ public @interface TestBean {
/**
* Name of the bean to override.
* <p>If left unspecified, the name of the bean to override is the name of
* the annotated field. If specified, the field name is ignored.
* <p>If left unspecified, the bean to override is selected according to
* the annotated field's type.
* @see #value()
*/
@AliasFor("value")

View File

@@ -144,14 +144,14 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor {
ResolvableType typeToOverride) {
super(field, typeToOverride, BeanOverrideStrategy.REPLACE_DEFINITION);
this.beanName = StringUtils.hasText(overrideAnnotation.name()) ?
overrideAnnotation.name() : field.getName();
this.beanName = overrideAnnotation.name();
this.overrideMethod = overrideMethod;
}
@Override
@Nullable
protected String getBeanName() {
return this.beanName;
return StringUtils.hasText(this.beanName) ? this.beanName : super.getBeanName();
}
@Override

View File

@@ -28,10 +28,14 @@ import org.mockito.MockSettings;
import org.springframework.test.context.bean.override.BeanOverride;
/**
* Mark a field to trigger a bean override using a Mockito mock. If no explicit
* {@link #name()} is specified, the annotated field's name is interpreted to
* be the target of the override. In either case, if no existing bean is defined
* a new one will be added to the context.
* Mark a field to trigger a bean override using a Mockito mock.
*
* <p>If no explicit {@link #name()} is specified, a target bean definition is
* selected according to the class of the annotated field, and there must be
* exactly one such candidate definition in the context.
* If a {@link #name()} is specified, either the definition exists in the
* application context and is replaced, or it doesn't and a new one is added to
* the context.
*
* <p>Dependencies that are known to the application context but are not beans
* (such as those
@@ -51,7 +55,8 @@ public @interface MockitoBean {
/**
* The name of the bean to register or replace.
* <p>If not specified, the name of the annotated field will be used.
* <p>If left unspecified, the bean to override is selected according to
* the annotated field's type.
* @return the name of the mocked bean
*/
String name() default "";

View File

@@ -54,11 +54,9 @@ abstract class MockitoMetadata extends OverrideMetadata {
@Override
@Nullable
protected String getBeanName() {
if (StringUtils.hasText(this.name)) {
return this.name;
}
return super.getBeanName();
return StringUtils.hasText(this.name) ? this.name : super.getBeanName();
}
@Override

View File

@@ -28,10 +28,13 @@ import org.springframework.test.context.bean.override.BeanOverride;
/**
* Mark a field to trigger a bean override using a Mockito spy, which will wrap
* the original instance. If no explicit {@link #name()} is specified, the
* annotated field's name is interpreted to be the target of the override.
* In either case, it is required that the target bean is previously registered
* in the context.
* the original instance.
*
* <p>If no explicit {@link #name()} is specified, a target bean is selected
* according to the class of the annotated field, and there must be exactly one
* such candidate bean.
* If a {@link #name()} is specified, it is required that a target bean of that
* name has been previously registered in the application context.
*
* <p>Dependencies that are known to the application context but are not beans
* (such as those
@@ -50,7 +53,8 @@ public @interface MockitoSpyBean {
/**
* The name of the bean to spy.
* <p>If not specified, the name of the annotated field will be used.
* <p>If left unspecified, the bean to override is selected according to
* the annotated field's type.
* @return the name of the spied bean
*/
String name() default "";