diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc index 62b5b89ecb8..483ca3b2d0e 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc @@ -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. diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc index fc4c2c63d7e..a384ecbcb65 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc @@ -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. diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java index c141b53d207..051199b32ca 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java @@ -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}. * *
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. + *
Defaults to {@code true} which means that an exception will be thrown + * if a corresponding bean definition does not exist. + *
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;
+
 }
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideMetadata.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideMetadata.java
index 1204ef3fffb..ef095f5ba79 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideMetadata.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideMetadata.java
@@ -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;
 	}
 
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java
index b3fdabc3a0a..cd17f52d131 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java
@@ -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 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.
+ *  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}.
  *
  *  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.
+	 *  Defaults to {@code true} which means that an exception will be thrown
+	 * if a corresponding bean definition does not exist.
+	 *  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;
+
 }
diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideMetadata.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideMetadata.java
index 765c114d89a..8de7f9e51f7 100644
--- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideMetadata.java
+++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideMetadata.java
@@ -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);
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanForByTypeLookupIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanForByTypeLookupIntegrationTests.java
index b35372460c1..465f54c554b 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanForByTypeLookupIntegrationTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanForByTypeLookupIntegrationTests.java
@@ -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();
+	}
+
 }
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideMetadataTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideMetadataTests.java
index e7df2a12db5..fb5bbb0f917 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideMetadataTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideMetadataTests.java
@@ -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 {
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByNameLookupIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByNameLookupIntegrationTests.java
index d2d22530962..0b0e4b5eed5 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByNameLookupIntegrationTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByNameLookupIntegrationTests.java
@@ -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;
 
 
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByTypeLookupIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByTypeLookupIntegrationTests.java
index 0836b457180..3cf66858118 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByTypeLookupIntegrationTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByTypeLookupIntegrationTests.java
@@ -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
diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanSettingsStrictIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanSettingsStrictIntegrationTests.java
index a54fbf3756b..583c7d4314f 100644
--- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanSettingsStrictIntegrationTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanSettingsStrictIntegrationTests.java
@@ -93,7 +93,7 @@ class MockitoBeanSettingsStrictIntegrationTests {
 	@DirtiesContext
 	static class ImplicitStrictnessWithMockitoBean extends BaseCase {
 
-		@MockitoBean
+		@MockitoBean(enforceOverride = false)
 		@SuppressWarnings("unused")
 		DateTimeFormatter ignoredMock;
 	}