Skip to content

Commit 1c87e47

Browse files
committed
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
1 parent 30dc868 commit 1c87e47

File tree

12 files changed

+103
-31
lines changed

12 files changed

+103
-31
lines changed

framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ to override. If multiple candidates match, `@Qualifier` can be provided to narro
1111
candidate to override. Alternatively, a candidate whose bean definition name matches the
1212
name of the field will match.
1313

14+
When using `@MockitoBean`, if you would like for a new bean definition to be created when
15+
a corresponding bean definition does not exist, set the `enforceOverride` attribute to
16+
`false` – for example, `@MockitoBean(enforceOverride = false)`.
17+
1418
To use a by-name override rather than a by-type override, specify the `name` attribute
1519
of the annotation.
1620

framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ to override. If multiple candidates match, `@Qualifier` can be provided to narro
1515
candidate to override. Alternatively, a candidate whose bean definition name matches the
1616
name of the field will match.
1717

18+
If you would like for a new bean definition to be created when a corresponding bean
19+
definition does not exist, set the `enforceOverride` attribute to `false` – for example,
20+
`@TestBean(enforceOverride = false)`.
21+
1822
To use a by-name override rather than a by-type override, specify the `name` attribute
1923
of the annotation.
2024

spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java

+16-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@
3737
* used to help disambiguate. In the absence of a {@code @Qualifier} annotation,
3838
* the name of the annotated field will be used as a qualifier. Alternatively,
3939
* you can explicitly specify a bean name to replace by setting the
40-
* {@link #value()} or {@link #name()} attribute.
40+
* {@link #value() value} or {@link #name() name} attribute. If you would like
41+
* for a new bean definition to be created when a corresponding bean definition
42+
* does not exist, set the {@link #enforceOverride() enforceOverride} attribute
43+
* to {@code false}.
4144
*
4245
* <p>The instance is created from a zero-argument static factory method in the
4346
* test class whose return type is compatible with the annotated field. In the
@@ -143,4 +146,16 @@
143146
*/
144147
String methodName() default "";
145148

149+
/**
150+
* Whether to require the existence of a bean definition for the bean being
151+
* overridden.
152+
* <p>Defaults to {@code true} which means that an exception will be thrown
153+
* if a corresponding bean definition does not exist.
154+
* <p>Set to {@code false} to create a new bean definition when a corresponding
155+
* bean definition does not exist.
156+
* @see org.springframework.test.context.bean.override.BeanOverrideStrategy#REPLACE_DEFINITION
157+
* @see org.springframework.test.context.bean.override.BeanOverrideStrategy#REPLACE_OR_CREATE_DEFINITION
158+
*/
159+
boolean enforceOverride() default true;
160+
146161
}

spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideMetadata.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
*
3434
* @author Simon Baslé
3535
* @author Stephane Nicoll
36+
* @author Sam Brannen
3637
* @since 6.2
3738
*/
3839
final class TestBeanOverrideMetadata extends OverrideMetadata {
@@ -41,9 +42,9 @@ final class TestBeanOverrideMetadata extends OverrideMetadata {
4142

4243

4344
TestBeanOverrideMetadata(Field field, ResolvableType beanType, @Nullable String beanName,
44-
Method overrideMethod) {
45+
BeanOverrideStrategy strategy, Method overrideMethod) {
4546

46-
super(field, beanType, beanName, BeanOverrideStrategy.REPLACE_DEFINITION);
47+
super(field, beanType, beanName, strategy);
4748
this.overrideMethod = overrideMethod;
4849
}
4950

spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java

+16-9
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,14 @@
3131
import org.springframework.core.ResolvableType;
3232
import org.springframework.test.context.TestContextAnnotationUtils;
3333
import org.springframework.test.context.bean.override.BeanOverrideProcessor;
34+
import org.springframework.test.context.bean.override.BeanOverrideStrategy;
3435
import org.springframework.util.Assert;
3536
import org.springframework.util.ClassUtils;
3637
import org.springframework.util.ReflectionUtils;
3738
import org.springframework.util.ReflectionUtils.MethodFilter;
38-
import org.springframework.util.StringUtils;
39+
40+
import static org.springframework.test.context.bean.override.BeanOverrideStrategy.REPLACE_DEFINITION;
41+
import static org.springframework.test.context.bean.override.BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION;
3942

4043
/**
4144
* {@link BeanOverrideProcessor} implementation for {@link TestBean @TestBean}
@@ -52,30 +55,34 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor {
5255

5356
@Override
5457
public TestBeanOverrideMetadata createMetadata(Annotation overrideAnnotation, Class<?> testClass, Field field) {
55-
if (!(overrideAnnotation instanceof TestBean testBeanAnnotation)) {
58+
if (!(overrideAnnotation instanceof TestBean testBean)) {
5659
throw new IllegalStateException("Invalid annotation passed to %s: expected @TestBean on field %s.%s"
5760
.formatted(getClass().getSimpleName(), field.getDeclaringClass().getName(), field.getName()));
5861
}
62+
63+
String beanName = (!testBean.name().isBlank() ? testBean.name() : null);
64+
String methodName = testBean.methodName();
65+
BeanOverrideStrategy strategy = (testBean.enforceOverride() ? REPLACE_DEFINITION : REPLACE_OR_CREATE_DEFINITION);
66+
5967
Method overrideMethod;
60-
String methodName = testBeanAnnotation.methodName();
6168
if (!methodName.isBlank()) {
6269
// If the user specified an explicit method name, search for that.
6370
overrideMethod = findTestBeanFactoryMethod(testClass, field.getType(), methodName);
6471
}
6572
else {
66-
// Otherwise, search for candidate factory methods the field name
67-
// or explicit bean name (if any).
73+
// Otherwise, search for candidate factory methods whose names match either
74+
// the field name or the explicit bean name (if any).
6875
List<String> candidateMethodNames = new ArrayList<>();
6976
candidateMethodNames.add(field.getName());
7077

71-
String beanName = testBeanAnnotation.name();
72-
if (StringUtils.hasText(beanName)) {
78+
if (beanName != null) {
7379
candidateMethodNames.add(beanName);
7480
}
7581
overrideMethod = findTestBeanFactoryMethod(testClass, field.getType(), candidateMethodNames);
7682
}
77-
String beanName = (StringUtils.hasText(testBeanAnnotation.name()) ? testBeanAnnotation.name() : null);
78-
return new TestBeanOverrideMetadata(field, ResolvableType.forField(field, testClass), beanName, overrideMethod);
83+
84+
return new TestBeanOverrideMetadata(
85+
field, ResolvableType.forField(field, testClass), beanName, strategy, overrideMethod);
7986
}
8087

8188
/**

spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java

+21-7
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,14 @@
3333
* {@link org.springframework.context.ApplicationContext ApplicationContext}
3434
* using a Mockito mock.
3535
*
36-
* <p>If no explicit {@link #name()} is specified, a target bean definition is
37-
* selected according to the type of the annotated field, and there must be
38-
* exactly one such candidate definition in the context. A {@code @Qualifier}
39-
* annotation can be used to help disambiguate.
40-
* If a {@link #name()} is specified, either the definition exists in the
41-
* application context and is replaced, or it doesn't and a new one is added to
42-
* the context.
36+
* <p>If no explicit {@link #name() name} is specified, a target bean definition
37+
* is selected according to the type of the annotated field, and there must be
38+
* exactly one such candidate definition in the context. Otherwise, a {@code @Qualifier}
39+
* annotation can be used to help disambiguate between multiple candidates. If a
40+
* {@link #name() name} is specified, by default a corresponding bean definition
41+
* must exist in the application context. If you would like for a new bean definition
42+
* to be created when a corresponding bean definition does not exist, set the
43+
* {@link #enforceOverride() enforceOverride} attribute to {@code false}.
4344
*
4445
* <p>Dependencies that are known to the application context but are not beans
4546
* (such as those
@@ -51,6 +52,7 @@
5152
* Any attempt to override a non-singleton bean will result in an exception.
5253
*
5354
* @author Simon Baslé
55+
* @author Sam Brannen
5456
* @since 6.2
5557
* @see org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean
5658
* @see org.springframework.test.context.bean.override.convention.TestBean @TestBean
@@ -100,4 +102,16 @@
100102
*/
101103
MockReset reset() default MockReset.AFTER;
102104

105+
/**
106+
* Whether to require the existence of a bean definition for the bean being
107+
* overridden.
108+
* <p>Defaults to {@code true} which means that an exception will be thrown
109+
* if a corresponding bean definition does not exist.
110+
* <p>Set to {@code false} to create a new bean definition when a corresponding
111+
* bean definition does not exist.
112+
* @see org.springframework.test.context.bean.override.BeanOverrideStrategy#REPLACE_DEFINITION
113+
* @see org.springframework.test.context.bean.override.BeanOverrideStrategy#REPLACE_OR_CREATE_DEFINITION
114+
*/
115+
boolean enforceOverride() default true;
116+
103117
}

spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideMetadata.java

+11-6
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
import org.springframework.util.ClassUtils;
3838
import org.springframework.util.StringUtils;
3939

40+
import static org.springframework.test.context.bean.override.BeanOverrideStrategy.REPLACE_DEFINITION;
41+
import static org.springframework.test.context.bean.override.BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION;
42+
4043
/**
4144
* {@link OverrideMetadata} implementation for Mockito {@code mock} support.
4245
*
@@ -54,15 +57,17 @@ class MockitoBeanOverrideMetadata extends AbstractMockitoOverrideMetadata {
5457
private final boolean serializable;
5558

5659

57-
MockitoBeanOverrideMetadata(Field field, ResolvableType typeToMock, MockitoBean annotation) {
58-
this(field, typeToMock, (StringUtils.hasText(annotation.name()) ? annotation.name() : null),
59-
annotation.reset(), annotation.extraInterfaces(), annotation.answers(), annotation.serializable());
60+
MockitoBeanOverrideMetadata(Field field, ResolvableType typeToMock, MockitoBean mockitoBean) {
61+
this(field, typeToMock, (!mockitoBean.name().isBlank() ? mockitoBean.name() : null),
62+
(mockitoBean.enforceOverride() ? REPLACE_DEFINITION : REPLACE_OR_CREATE_DEFINITION),
63+
mockitoBean.reset(), mockitoBean.extraInterfaces(), mockitoBean.answers(), mockitoBean.serializable());
6064
}
6165

62-
private MockitoBeanOverrideMetadata(Field field, ResolvableType typeToMock, @Nullable String beanName, MockReset reset,
63-
Class<?>[] extraInterfaces, @Nullable Answers answers, boolean serializable) {
66+
private MockitoBeanOverrideMetadata(Field field, ResolvableType typeToMock, @Nullable String beanName,
67+
BeanOverrideStrategy strategy, MockReset reset, Class<?>[] extraInterfaces, @Nullable Answers answers,
68+
boolean serializable) {
6469

65-
super(field, typeToMock, beanName, BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION, reset, false);
70+
super(field, typeToMock, beanName, strategy, reset, false);
6671
Assert.notNull(typeToMock, "'typeToMock' must not be null");
6772
this.extraInterfaces = asClassSet(extraInterfaces);
6873
this.answers = (answers != null ? answers : Answers.RETURNS_DEFAULTS);

spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanForByTypeLookupIntegrationTests.java

+20
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
@SpringJUnitConfig
4040
public class TestBeanForByTypeLookupIntegrationTests {
4141

42+
@TestBean(enforceOverride = false)
43+
MessageService messageService;
44+
4245
@TestBean
4346
ExampleService anyNameForService;
4447

@@ -50,6 +53,11 @@ public class TestBeanForByTypeLookupIntegrationTests {
5053
@CustomQualifier
5154
StringBuilder anyNameForStringBuilder2;
5255

56+
57+
static MessageService messageService() {
58+
return () -> "mocked nonexistent bean definition";
59+
}
60+
5361
static ExampleService anyNameForService() {
5462
return new RealExampleService("Mocked greeting");
5563
}
@@ -63,6 +71,12 @@ static StringBuilder someString2() {
6371
}
6472

6573

74+
@Test
75+
void overrideIsFoundByTypeForNonexistentBeanDefinition(ApplicationContext ctx) {
76+
assertThat(this.messageService).isSameAs(ctx.getBean(MessageService.class));
77+
assertThat(this.messageService.getMessage()).isEqualTo("mocked nonexistent bean definition");
78+
}
79+
6680
@Test
6781
void overrideIsFoundByType(ApplicationContext ctx) {
6882
assertThat(this.anyNameForService)
@@ -114,4 +128,10 @@ StringBuilder beanString3() {
114128
}
115129
}
116130

131+
@FunctionalInterface
132+
interface MessageService {
133+
134+
String getMessage();
135+
}
136+
117137
}

spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideMetadataTests.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import org.springframework.beans.factory.annotation.Qualifier;
2626
import org.springframework.core.ResolvableType;
27+
import org.springframework.test.context.bean.override.BeanOverrideStrategy;
2728
import org.springframework.test.context.bean.override.OverrideMetadata;
2829
import org.springframework.util.ReflectionUtils;
2930
import org.springframework.util.StringUtils;
@@ -124,7 +125,8 @@ private Method sampleMethod(String noArgMethodName) {
124125
private TestBeanOverrideMetadata createMetadata(Field field, Method overrideMethod) {
125126
TestBean annotation = field.getAnnotation(TestBean.class);
126127
String beanName = (StringUtils.hasText(annotation.name()) ? annotation.name() : null);
127-
return new TestBeanOverrideMetadata(field, ResolvableType.forClass(field.getType()), beanName, overrideMethod);
128+
return new TestBeanOverrideMetadata(
129+
field, ResolvableType.forClass(field.getType()), beanName, BeanOverrideStrategy.REPLACE_DEFINITION, overrideMethod);
128130
}
129131

130132
static class SampleOneOverride {

spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByNameLookupIntegrationTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ public class MockitoBeanForByNameLookupIntegrationTests {
4848
@MockitoBean(name = "nestedField")
4949
ExampleService renamed2;
5050

51-
@MockitoBean(name = "nonExistingBean")
51+
@MockitoBean(name = "nonExistingBean", enforceOverride = false)
5252
ExampleService nonExisting1;
5353

54-
@MockitoBean(name = "nestedNonExistingBean")
54+
@MockitoBean(name = "nestedNonExistingBean", enforceOverride = false)
5555
ExampleService nonExisting2;
5656

5757

spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByTypeLookupIntegrationTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@
4848
@SpringJUnitConfig
4949
public class MockitoBeanForByTypeLookupIntegrationTests {
5050

51-
@MockitoBean
51+
@MockitoBean(enforceOverride = false)
5252
AnotherService serviceIsNotABean;
5353

54-
@MockitoBean
54+
@MockitoBean(enforceOverride = false)
5555
ExampleService anyNameForService;
5656

5757
@MockitoBean

spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanSettingsStrictIntegrationTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ static class ExplicitStrictness extends BaseCase {
9393
@DirtiesContext
9494
static class ImplicitStrictnessWithMockitoBean extends BaseCase {
9595

96-
@MockitoBean
96+
@MockitoBean(enforceOverride = false)
9797
@SuppressWarnings("unused")
9898
DateTimeFormatter ignoredMock;
9999
}

0 commit comments

Comments
 (0)