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

@ -11,6 +11,10 @@ to override. If multiple candidates match, `@Qualifier` can be provided to narro
candidate to override. Alternatively, a candidate whose bean definition name matches the
name of the field will match.
When using `@MockitoBean`, if you would like for a new bean definition to be created when
a corresponding bean definition does not exist, set the `enforceOverride` attribute to
`false` for example, `@MockitoBean(enforceOverride = false)`.
To use a by-name override rather than a by-type override, specify the `name` attribute
of the annotation.

View File

@ -15,6 +15,10 @@ to override. If multiple candidates match, `@Qualifier` can be provided to narro
candidate to override. Alternatively, a candidate whose bean definition name matches the
name of the field will match.
If you would like for a new bean definition to be created when a corresponding bean
definition does not exist, set the `enforceOverride` attribute to `false` for example,
`@TestBean(enforceOverride = false)`.
To use a by-name override rather than a by-type override, specify the `name` attribute
of the annotation.

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);

View File

@ -39,6 +39,9 @@ import static org.assertj.core.api.Assertions.assertThat;
@SpringJUnitConfig
public class TestBeanForByTypeLookupIntegrationTests {
@TestBean(enforceOverride = false)
MessageService messageService;
@TestBean
ExampleService anyNameForService;
@ -50,6 +53,11 @@ public class TestBeanForByTypeLookupIntegrationTests {
@CustomQualifier
StringBuilder anyNameForStringBuilder2;
static MessageService messageService() {
return () -> "mocked nonexistent bean definition";
}
static ExampleService anyNameForService() {
return new RealExampleService("Mocked greeting");
}
@ -63,6 +71,12 @@ public class TestBeanForByTypeLookupIntegrationTests {
}
@Test
void overrideIsFoundByTypeForNonexistentBeanDefinition(ApplicationContext ctx) {
assertThat(this.messageService).isSameAs(ctx.getBean(MessageService.class));
assertThat(this.messageService.getMessage()).isEqualTo("mocked nonexistent bean definition");
}
@Test
void overrideIsFoundByType(ApplicationContext ctx) {
assertThat(this.anyNameForService)
@ -114,4 +128,10 @@ public class TestBeanForByTypeLookupIntegrationTests {
}
}
@FunctionalInterface
interface MessageService {
String getMessage();
}
}

View File

@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.ResolvableType;
import org.springframework.test.context.bean.override.BeanOverrideStrategy;
import org.springframework.test.context.bean.override.OverrideMetadata;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
@ -124,7 +125,8 @@ class TestBeanOverrideMetadataTests {
private TestBeanOverrideMetadata createMetadata(Field field, Method overrideMethod) {
TestBean annotation = field.getAnnotation(TestBean.class);
String beanName = (StringUtils.hasText(annotation.name()) ? annotation.name() : null);
return new TestBeanOverrideMetadata(field, ResolvableType.forClass(field.getType()), beanName, overrideMethod);
return new TestBeanOverrideMetadata(
field, ResolvableType.forClass(field.getType()), beanName, BeanOverrideStrategy.REPLACE_DEFINITION, overrideMethod);
}
static class SampleOneOverride {

View File

@ -48,10 +48,10 @@ public class MockitoBeanForByNameLookupIntegrationTests {
@MockitoBean(name = "nestedField")
ExampleService renamed2;
@MockitoBean(name = "nonExistingBean")
@MockitoBean(name = "nonExistingBean", enforceOverride = false)
ExampleService nonExisting1;
@MockitoBean(name = "nestedNonExistingBean")
@MockitoBean(name = "nestedNonExistingBean", enforceOverride = false)
ExampleService nonExisting2;

View File

@ -48,10 +48,10 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
@SpringJUnitConfig
public class MockitoBeanForByTypeLookupIntegrationTests {
@MockitoBean
@MockitoBean(enforceOverride = false)
AnotherService serviceIsNotABean;
@MockitoBean
@MockitoBean(enforceOverride = false)
ExampleService anyNameForService;
@MockitoBean

View File

@ -93,7 +93,7 @@ class MockitoBeanSettingsStrictIntegrationTests {
@DirtiesContext
static class ImplicitStrictnessWithMockitoBean extends BaseCase {
@MockitoBean
@MockitoBean(enforceOverride = false)
@SuppressWarnings("unused")
DateTimeFormatter ignoredMock;
}