Introduce enforceOverride flag in @⁠TestBean and @⁠MockitoBean

Prior to this commit, @⁠MockitoBean could be used to either create or
replace a bean definition, but @⁠TestBean could only be used to replace
a bean definition.

However, Bean Override implementations should require the presence of
an existing bean definition by default (i.e. literally "override" by
default), while giving the user the option to have a new bean
definition created if desired.

To address that, this commit introduces a new `enforceOverride`
attribute in @⁠TestBean and @⁠MockitoBean that defaults to true but
allows the user to decide if it's OK to create a bean for a nonexistent
bean definition.

Closes gh-33613
This commit is contained in:
Sam Brannen
2024-09-30 14:24:14 +02:00
parent 30dc86810e
commit 1c87e4795d
12 changed files with 103 additions and 31 deletions

View File

@@ -37,7 +37,10 @@ import org.springframework.test.context.bean.override.BeanOverride;
* used to help disambiguate. In the absence of a {@code @Qualifier} annotation,
* the name of the annotated field will be used as a qualifier. Alternatively,
* you can explicitly specify a bean name to replace by setting the
* {@link #value()} or {@link #name()} attribute.
* {@link #value() value} or {@link #name() name} attribute. If you would like
* for a new bean definition to be created when a corresponding bean definition
* does not exist, set the {@link #enforceOverride() enforceOverride} attribute
* to {@code false}.
*
* <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
@@ -143,4 +146,16 @@ public @interface TestBean {
*/
String methodName() default "";
/**
* Whether to require the existence of a bean definition for the bean being
* overridden.
* <p>Defaults to {@code true} which means that an exception will be thrown
* if a corresponding bean definition does not exist.
* <p>Set to {@code false} to create a new bean definition when a corresponding
* bean definition does not exist.
* @see org.springframework.test.context.bean.override.BeanOverrideStrategy#REPLACE_DEFINITION
* @see org.springframework.test.context.bean.override.BeanOverrideStrategy#REPLACE_OR_CREATE_DEFINITION
*/
boolean enforceOverride() default true;
}

View File

@@ -33,6 +33,7 @@ import org.springframework.util.ReflectionUtils;
*
* @author Simon Baslé
* @author Stephane Nicoll
* @author Sam Brannen
* @since 6.2
*/
final class TestBeanOverrideMetadata extends OverrideMetadata {
@@ -41,9 +42,9 @@ final class TestBeanOverrideMetadata extends OverrideMetadata {
TestBeanOverrideMetadata(Field field, ResolvableType beanType, @Nullable String beanName,
Method overrideMethod) {
BeanOverrideStrategy strategy, Method overrideMethod) {
super(field, beanType, beanName, BeanOverrideStrategy.REPLACE_DEFINITION);
super(field, beanType, beanName, strategy);
this.overrideMethod = overrideMethod;
}

View File

@@ -31,11 +31,14 @@ import org.springframework.core.MethodIntrospector;
import org.springframework.core.ResolvableType;
import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.test.context.bean.override.BeanOverrideProcessor;
import org.springframework.test.context.bean.override.BeanOverrideStrategy;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.MethodFilter;
import org.springframework.util.StringUtils;
import static org.springframework.test.context.bean.override.BeanOverrideStrategy.REPLACE_DEFINITION;
import static org.springframework.test.context.bean.override.BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION;
/**
* {@link BeanOverrideProcessor} implementation for {@link TestBean @TestBean}
@@ -52,30 +55,34 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor {
@Override
public TestBeanOverrideMetadata createMetadata(Annotation overrideAnnotation, Class<?> testClass, Field field) {
if (!(overrideAnnotation instanceof TestBean testBeanAnnotation)) {
if (!(overrideAnnotation instanceof TestBean testBean)) {
throw new IllegalStateException("Invalid annotation passed to %s: expected @TestBean on field %s.%s"
.formatted(getClass().getSimpleName(), field.getDeclaringClass().getName(), field.getName()));
}
String beanName = (!testBean.name().isBlank() ? testBean.name() : null);
String methodName = testBean.methodName();
BeanOverrideStrategy strategy = (testBean.enforceOverride() ? REPLACE_DEFINITION : REPLACE_OR_CREATE_DEFINITION);
Method overrideMethod;
String methodName = testBeanAnnotation.methodName();
if (!methodName.isBlank()) {
// If the user specified an explicit method name, search for that.
overrideMethod = findTestBeanFactoryMethod(testClass, field.getType(), methodName);
}
else {
// Otherwise, search for candidate factory methods the field name
// or explicit bean name (if any).
// Otherwise, search for candidate factory methods whose names match either
// the field name or the explicit bean name (if any).
List<String> candidateMethodNames = new ArrayList<>();
candidateMethodNames.add(field.getName());
String beanName = testBeanAnnotation.name();
if (StringUtils.hasText(beanName)) {
if (beanName != null) {
candidateMethodNames.add(beanName);
}
overrideMethod = findTestBeanFactoryMethod(testClass, field.getType(), candidateMethodNames);
}
String beanName = (StringUtils.hasText(testBeanAnnotation.name()) ? testBeanAnnotation.name() : null);
return new TestBeanOverrideMetadata(field, ResolvableType.forField(field, testClass), beanName, overrideMethod);
return new TestBeanOverrideMetadata(
field, ResolvableType.forField(field, testClass), beanName, strategy, overrideMethod);
}
/**

View File

@@ -33,13 +33,14 @@ import org.springframework.test.context.bean.override.BeanOverride;
* {@link org.springframework.context.ApplicationContext ApplicationContext}
* using a Mockito mock.
*
* <p>If no explicit {@link #name()} is specified, a target bean definition is
* selected according to the type of the annotated field, and there must be
* exactly one such candidate definition in the context. A {@code @Qualifier}
* annotation can be used to help disambiguate.
* 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>If no explicit {@link #name() name} is specified, a target bean definition
* is selected according to the type of the annotated field, and there must be
* exactly one such candidate definition in the context. Otherwise, a {@code @Qualifier}
* annotation can be used to help disambiguate between multiple candidates. If a
* {@link #name() name} is specified, by default a corresponding bean definition
* must exist in the application context. If you would like for a new bean definition
* to be created when a corresponding bean definition does not exist, set the
* {@link #enforceOverride() enforceOverride} attribute to {@code false}.
*
* <p>Dependencies that are known to the application context but are not beans
* (such as those
@@ -51,6 +52,7 @@ import org.springframework.test.context.bean.override.BeanOverride;
* Any attempt to override a non-singleton bean will result in an exception.
*
* @author Simon Baslé
* @author Sam Brannen
* @since 6.2
* @see org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean
* @see org.springframework.test.context.bean.override.convention.TestBean @TestBean
@@ -100,4 +102,16 @@ public @interface MockitoBean {
*/
MockReset reset() default MockReset.AFTER;
/**
* Whether to require the existence of a bean definition for the bean being
* overridden.
* <p>Defaults to {@code true} which means that an exception will be thrown
* if a corresponding bean definition does not exist.
* <p>Set to {@code false} to create a new bean definition when a corresponding
* bean definition does not exist.
* @see org.springframework.test.context.bean.override.BeanOverrideStrategy#REPLACE_DEFINITION
* @see org.springframework.test.context.bean.override.BeanOverrideStrategy#REPLACE_OR_CREATE_DEFINITION
*/
boolean enforceOverride() default true;
}

View File

@@ -37,6 +37,9 @@ import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import static org.springframework.test.context.bean.override.BeanOverrideStrategy.REPLACE_DEFINITION;
import static org.springframework.test.context.bean.override.BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION;
/**
* {@link OverrideMetadata} implementation for Mockito {@code mock} support.
*
@@ -54,15 +57,17 @@ class MockitoBeanOverrideMetadata extends AbstractMockitoOverrideMetadata {
private final boolean serializable;
MockitoBeanOverrideMetadata(Field field, ResolvableType typeToMock, MockitoBean annotation) {
this(field, typeToMock, (StringUtils.hasText(annotation.name()) ? annotation.name() : null),
annotation.reset(), annotation.extraInterfaces(), annotation.answers(), annotation.serializable());
MockitoBeanOverrideMetadata(Field field, ResolvableType typeToMock, MockitoBean mockitoBean) {
this(field, typeToMock, (!mockitoBean.name().isBlank() ? mockitoBean.name() : null),
(mockitoBean.enforceOverride() ? REPLACE_DEFINITION : REPLACE_OR_CREATE_DEFINITION),
mockitoBean.reset(), mockitoBean.extraInterfaces(), mockitoBean.answers(), mockitoBean.serializable());
}
private MockitoBeanOverrideMetadata(Field field, ResolvableType typeToMock, @Nullable String beanName, MockReset reset,
Class<?>[] extraInterfaces, @Nullable Answers answers, boolean serializable) {
private MockitoBeanOverrideMetadata(Field field, ResolvableType typeToMock, @Nullable String beanName,
BeanOverrideStrategy strategy, MockReset reset, Class<?>[] extraInterfaces, @Nullable Answers answers,
boolean serializable) {
super(field, typeToMock, beanName, BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION, reset, false);
super(field, typeToMock, beanName, strategy, reset, false);
Assert.notNull(typeToMock, "'typeToMock' must not be null");
this.extraInterfaces = asClassSet(extraInterfaces);
this.answers = (answers != null ? answers : Answers.RETURNS_DEFAULTS);