From 1c87e4795df252a08d790b4898ba4d1ade43343f Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:24:14 +0200 Subject: [PATCH] =?UTF-8?q?Introduce=20enforceOverride=20flag=20in=20@?= =?UTF-8?q?=E2=81=A0TestBean=20and=20@=E2=81=A0MockitoBean?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../annotation-mockitobean.adoc | 4 +++ .../annotation-testbean.adoc | 4 +++ .../bean/override/convention/TestBean.java | 17 ++++++++++- .../convention/TestBeanOverrideMetadata.java | 5 ++-- .../convention/TestBeanOverrideProcessor.java | 25 +++++++++++------ .../bean/override/mockito/MockitoBean.java | 28 ++++++++++++++----- .../mockito/MockitoBeanOverrideMetadata.java | 17 +++++++---- ...stBeanForByTypeLookupIntegrationTests.java | 20 +++++++++++++ .../TestBeanOverrideMetadataTests.java | 4 ++- ...toBeanForByNameLookupIntegrationTests.java | 4 +-- ...toBeanForByTypeLookupIntegrationTests.java | 4 +-- ...itoBeanSettingsStrictIntegrationTests.java | 2 +- 12 files changed, 103 insertions(+), 31 deletions(-) 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 62b5b89ecb83..483ca3b2d0ea 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 fc4c2c63d7e3..a384ecbcb651 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 c141b53d207c..051199b32ca0 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 @@ * 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 @@ */ 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 1204ef3fffbd..ef095f5ba79f 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 @@ * * @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 b3fdabc3a0a6..cd17f52d1314 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.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 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); } /** diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java index 90a2e0a452fa..68b27b6bc636 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java @@ -33,13 +33,14 @@ * {@link org.springframework.context.ApplicationContext ApplicationContext} * using a Mockito mock. * - *

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 @@ * 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 @@ */ 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 765c114d89a1..8de7f9e51f79 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.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 b35372460c1e..465f54c554b5 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 @@ @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 @@ 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) @@ -114,4 +128,10 @@ StringBuilder beanString3() { } } + @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 e7df2a12db5a..fb5bbb0f9179 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.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 @@ 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 { 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 d2d225309627..0b0e4b5eed5a 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 0836b4571806..3cf66858118a 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 @@ @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 a54fbf3756be..583c7d4314fb 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 @@ static class ExplicitStrictness extends BaseCase { @DirtiesContext static class ImplicitStrictnessWithMockitoBean extends BaseCase { - @MockitoBean + @MockitoBean(enforceOverride = false) @SuppressWarnings("unused") DateTimeFormatter ignoredMock; }