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:
parent
30dc86810e
commit
1c87e4795d
|
@ -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
|
candidate to override. Alternatively, a candidate whose bean definition name matches the
|
||||||
name of the field will match.
|
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
|
To use a by-name override rather than a by-type override, specify the `name` attribute
|
||||||
of the annotation.
|
of the annotation.
|
||||||
|
|
||||||
|
|
|
@ -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
|
candidate to override. Alternatively, a candidate whose bean definition name matches the
|
||||||
name of the field will match.
|
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
|
To use a by-name override rather than a by-type override, specify the `name` attribute
|
||||||
of the annotation.
|
of the annotation.
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,10 @@ import org.springframework.test.context.bean.override.BeanOverride;
|
||||||
* used to help disambiguate. In the absence of a {@code @Qualifier} annotation,
|
* 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,
|
* 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
|
* 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
|
* <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
|
* test class whose return type is compatible with the annotated field. In the
|
||||||
|
@ -143,4 +146,16 @@ public @interface TestBean {
|
||||||
*/
|
*/
|
||||||
String methodName() default "";
|
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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ import org.springframework.util.ReflectionUtils;
|
||||||
*
|
*
|
||||||
* @author Simon Baslé
|
* @author Simon Baslé
|
||||||
* @author Stephane Nicoll
|
* @author Stephane Nicoll
|
||||||
|
* @author Sam Brannen
|
||||||
* @since 6.2
|
* @since 6.2
|
||||||
*/
|
*/
|
||||||
final class TestBeanOverrideMetadata extends OverrideMetadata {
|
final class TestBeanOverrideMetadata extends OverrideMetadata {
|
||||||
|
@ -41,9 +42,9 @@ final class TestBeanOverrideMetadata extends OverrideMetadata {
|
||||||
|
|
||||||
|
|
||||||
TestBeanOverrideMetadata(Field field, ResolvableType beanType, @Nullable String beanName,
|
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;
|
this.overrideMethod = overrideMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,11 +31,14 @@ import org.springframework.core.MethodIntrospector;
|
||||||
import org.springframework.core.ResolvableType;
|
import org.springframework.core.ResolvableType;
|
||||||
import org.springframework.test.context.TestContextAnnotationUtils;
|
import org.springframework.test.context.TestContextAnnotationUtils;
|
||||||
import org.springframework.test.context.bean.override.BeanOverrideProcessor;
|
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.Assert;
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
import org.springframework.util.ReflectionUtils;
|
import org.springframework.util.ReflectionUtils;
|
||||||
import org.springframework.util.ReflectionUtils.MethodFilter;
|
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}
|
* {@link BeanOverrideProcessor} implementation for {@link TestBean @TestBean}
|
||||||
|
@ -52,30 +55,34 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TestBeanOverrideMetadata createMetadata(Annotation overrideAnnotation, Class<?> testClass, Field field) {
|
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"
|
throw new IllegalStateException("Invalid annotation passed to %s: expected @TestBean on field %s.%s"
|
||||||
.formatted(getClass().getSimpleName(), field.getDeclaringClass().getName(), field.getName()));
|
.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;
|
Method overrideMethod;
|
||||||
String methodName = testBeanAnnotation.methodName();
|
|
||||||
if (!methodName.isBlank()) {
|
if (!methodName.isBlank()) {
|
||||||
// If the user specified an explicit method name, search for that.
|
// If the user specified an explicit method name, search for that.
|
||||||
overrideMethod = findTestBeanFactoryMethod(testClass, field.getType(), methodName);
|
overrideMethod = findTestBeanFactoryMethod(testClass, field.getType(), methodName);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Otherwise, search for candidate factory methods the field name
|
// Otherwise, search for candidate factory methods whose names match either
|
||||||
// or explicit bean name (if any).
|
// the field name or the explicit bean name (if any).
|
||||||
List<String> candidateMethodNames = new ArrayList<>();
|
List<String> candidateMethodNames = new ArrayList<>();
|
||||||
candidateMethodNames.add(field.getName());
|
candidateMethodNames.add(field.getName());
|
||||||
|
|
||||||
String beanName = testBeanAnnotation.name();
|
if (beanName != null) {
|
||||||
if (StringUtils.hasText(beanName)) {
|
|
||||||
candidateMethodNames.add(beanName);
|
candidateMethodNames.add(beanName);
|
||||||
}
|
}
|
||||||
overrideMethod = findTestBeanFactoryMethod(testClass, field.getType(), candidateMethodNames);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -33,13 +33,14 @@ import org.springframework.test.context.bean.override.BeanOverride;
|
||||||
* {@link org.springframework.context.ApplicationContext ApplicationContext}
|
* {@link org.springframework.context.ApplicationContext ApplicationContext}
|
||||||
* using a Mockito mock.
|
* using a Mockito mock.
|
||||||
*
|
*
|
||||||
* <p>If no explicit {@link #name()} is specified, a target bean definition is
|
* <p>If no explicit {@link #name() name} is specified, a target bean definition
|
||||||
* selected according to the type of the annotated field, and there must be
|
* 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}
|
* exactly one such candidate definition in the context. Otherwise, a {@code @Qualifier}
|
||||||
* annotation can be used to help disambiguate.
|
* annotation can be used to help disambiguate between multiple candidates. If a
|
||||||
* If a {@link #name()} is specified, either the definition exists in the
|
* {@link #name() name} is specified, by default a corresponding bean definition
|
||||||
* application context and is replaced, or it doesn't and a new one is added to
|
* must exist in the application context. If you would like for a new bean definition
|
||||||
* the context.
|
* 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
|
* <p>Dependencies that are known to the application context but are not beans
|
||||||
* (such as those
|
* (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.
|
* Any attempt to override a non-singleton bean will result in an exception.
|
||||||
*
|
*
|
||||||
* @author Simon Baslé
|
* @author Simon Baslé
|
||||||
|
* @author Sam Brannen
|
||||||
* @since 6.2
|
* @since 6.2
|
||||||
* @see org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean
|
* @see org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean
|
||||||
* @see org.springframework.test.context.bean.override.convention.TestBean @TestBean
|
* @see org.springframework.test.context.bean.override.convention.TestBean @TestBean
|
||||||
|
@ -100,4 +102,16 @@ public @interface MockitoBean {
|
||||||
*/
|
*/
|
||||||
MockReset reset() default MockReset.AFTER;
|
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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,9 @@ import org.springframework.util.Assert;
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
import org.springframework.util.StringUtils;
|
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.
|
* {@link OverrideMetadata} implementation for Mockito {@code mock} support.
|
||||||
*
|
*
|
||||||
|
@ -54,15 +57,17 @@ class MockitoBeanOverrideMetadata extends AbstractMockitoOverrideMetadata {
|
||||||
private final boolean serializable;
|
private final boolean serializable;
|
||||||
|
|
||||||
|
|
||||||
MockitoBeanOverrideMetadata(Field field, ResolvableType typeToMock, MockitoBean annotation) {
|
MockitoBeanOverrideMetadata(Field field, ResolvableType typeToMock, MockitoBean mockitoBean) {
|
||||||
this(field, typeToMock, (StringUtils.hasText(annotation.name()) ? annotation.name() : null),
|
this(field, typeToMock, (!mockitoBean.name().isBlank() ? mockitoBean.name() : null),
|
||||||
annotation.reset(), annotation.extraInterfaces(), annotation.answers(), annotation.serializable());
|
(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,
|
private MockitoBeanOverrideMetadata(Field field, ResolvableType typeToMock, @Nullable String beanName,
|
||||||
Class<?>[] extraInterfaces, @Nullable Answers answers, boolean serializable) {
|
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");
|
Assert.notNull(typeToMock, "'typeToMock' must not be null");
|
||||||
this.extraInterfaces = asClassSet(extraInterfaces);
|
this.extraInterfaces = asClassSet(extraInterfaces);
|
||||||
this.answers = (answers != null ? answers : Answers.RETURNS_DEFAULTS);
|
this.answers = (answers != null ? answers : Answers.RETURNS_DEFAULTS);
|
||||||
|
|
|
@ -39,6 +39,9 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||||
@SpringJUnitConfig
|
@SpringJUnitConfig
|
||||||
public class TestBeanForByTypeLookupIntegrationTests {
|
public class TestBeanForByTypeLookupIntegrationTests {
|
||||||
|
|
||||||
|
@TestBean(enforceOverride = false)
|
||||||
|
MessageService messageService;
|
||||||
|
|
||||||
@TestBean
|
@TestBean
|
||||||
ExampleService anyNameForService;
|
ExampleService anyNameForService;
|
||||||
|
|
||||||
|
@ -50,6 +53,11 @@ public class TestBeanForByTypeLookupIntegrationTests {
|
||||||
@CustomQualifier
|
@CustomQualifier
|
||||||
StringBuilder anyNameForStringBuilder2;
|
StringBuilder anyNameForStringBuilder2;
|
||||||
|
|
||||||
|
|
||||||
|
static MessageService messageService() {
|
||||||
|
return () -> "mocked nonexistent bean definition";
|
||||||
|
}
|
||||||
|
|
||||||
static ExampleService anyNameForService() {
|
static ExampleService anyNameForService() {
|
||||||
return new RealExampleService("Mocked greeting");
|
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
|
@Test
|
||||||
void overrideIsFoundByType(ApplicationContext ctx) {
|
void overrideIsFoundByType(ApplicationContext ctx) {
|
||||||
assertThat(this.anyNameForService)
|
assertThat(this.anyNameForService)
|
||||||
|
@ -114,4 +128,10 @@ public class TestBeanForByTypeLookupIntegrationTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface MessageService {
|
||||||
|
|
||||||
|
String getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.core.ResolvableType;
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.test.context.bean.override.BeanOverrideStrategy;
|
||||||
import org.springframework.test.context.bean.override.OverrideMetadata;
|
import org.springframework.test.context.bean.override.OverrideMetadata;
|
||||||
import org.springframework.util.ReflectionUtils;
|
import org.springframework.util.ReflectionUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
@ -124,7 +125,8 @@ class TestBeanOverrideMetadataTests {
|
||||||
private TestBeanOverrideMetadata createMetadata(Field field, Method overrideMethod) {
|
private TestBeanOverrideMetadata createMetadata(Field field, Method overrideMethod) {
|
||||||
TestBean annotation = field.getAnnotation(TestBean.class);
|
TestBean annotation = field.getAnnotation(TestBean.class);
|
||||||
String beanName = (StringUtils.hasText(annotation.name()) ? annotation.name() : null);
|
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 {
|
static class SampleOneOverride {
|
||||||
|
|
|
@ -48,10 +48,10 @@ public class MockitoBeanForByNameLookupIntegrationTests {
|
||||||
@MockitoBean(name = "nestedField")
|
@MockitoBean(name = "nestedField")
|
||||||
ExampleService renamed2;
|
ExampleService renamed2;
|
||||||
|
|
||||||
@MockitoBean(name = "nonExistingBean")
|
@MockitoBean(name = "nonExistingBean", enforceOverride = false)
|
||||||
ExampleService nonExisting1;
|
ExampleService nonExisting1;
|
||||||
|
|
||||||
@MockitoBean(name = "nestedNonExistingBean")
|
@MockitoBean(name = "nestedNonExistingBean", enforceOverride = false)
|
||||||
ExampleService nonExisting2;
|
ExampleService nonExisting2;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -48,10 +48,10 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
@SpringJUnitConfig
|
@SpringJUnitConfig
|
||||||
public class MockitoBeanForByTypeLookupIntegrationTests {
|
public class MockitoBeanForByTypeLookupIntegrationTests {
|
||||||
|
|
||||||
@MockitoBean
|
@MockitoBean(enforceOverride = false)
|
||||||
AnotherService serviceIsNotABean;
|
AnotherService serviceIsNotABean;
|
||||||
|
|
||||||
@MockitoBean
|
@MockitoBean(enforceOverride = false)
|
||||||
ExampleService anyNameForService;
|
ExampleService anyNameForService;
|
||||||
|
|
||||||
@MockitoBean
|
@MockitoBean
|
||||||
|
|
|
@ -93,7 +93,7 @@ class MockitoBeanSettingsStrictIntegrationTests {
|
||||||
@DirtiesContext
|
@DirtiesContext
|
||||||
static class ImplicitStrictnessWithMockitoBean extends BaseCase {
|
static class ImplicitStrictnessWithMockitoBean extends BaseCase {
|
||||||
|
|
||||||
@MockitoBean
|
@MockitoBean(enforceOverride = false)
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
DateTimeFormatter ignoredMock;
|
DateTimeFormatter ignoredMock;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue