Skip to content

Commit

Permalink
Introduce enforceOverride flag in @⁠TestBean and @⁠MockitoBean
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sbrannen committed Sep 30, 2024
1 parent 30dc868 commit 1c87e47
Show file tree
Hide file tree
Showing 12 changed files with 103 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@
* 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
Expand Down Expand Up @@ -143,4 +146,16 @@
*/
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;

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
*
* @author Simon Baslé
* @author Stephane Nicoll
* @author Sam Brannen
* @since 6.2
*/
final class TestBeanOverrideMetadata extends OverrideMetadata {
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,14 @@
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}
Expand All @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@
* {@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
Expand All @@ -51,6 +52,7 @@
* 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
Expand Down Expand Up @@ -100,4 +102,16 @@
*/
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;

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
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.
*
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
@SpringJUnitConfig
public class TestBeanForByTypeLookupIntegrationTests {

@TestBean(enforceOverride = false)
MessageService messageService;

@TestBean
ExampleService anyNameForService;

Expand All @@ -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");
}
Expand All @@ -63,6 +71,12 @@ static StringBuilder someString2() {
}


@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)
Expand Down Expand Up @@ -114,4 +128,10 @@ StringBuilder beanString3() {
}
}

@FunctionalInterface
interface MessageService {

String getMessage();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

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;
Expand Down Expand Up @@ -124,7 +125,8 @@ private Method sampleMethod(String noArgMethodName) {
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@
@SpringJUnitConfig
public class MockitoBeanForByTypeLookupIntegrationTests {

@MockitoBean
@MockitoBean(enforceOverride = false)
AnotherService serviceIsNotABean;

@MockitoBean
@MockitoBean(enforceOverride = false)
ExampleService anyNameForService;

@MockitoBean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ static class ExplicitStrictness extends BaseCase {
@DirtiesContext
static class ImplicitStrictnessWithMockitoBean extends BaseCase {

@MockitoBean
@MockitoBean(enforceOverride = false)
@SuppressWarnings("unused")
DateTimeFormatter ignoredMock;
}
Expand Down

0 comments on commit 1c87e47

Please sign in to comment.