From a86612a254267cbaebde626ddd6a8bd9db65429b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Tue, 14 May 2024 12:35:22 +0200 Subject: [PATCH] Use by-type semantics in bean overriding if no explicit name is provided This change switches default behavior of `@TestBean`, `@MockitoBean` and `@MockitoSpyBean` to match the bean definition / bean to override by type in the case there is no explicit bean name provided via the annotation. The previous behavior of using the annotated field's name is still an option for implementors, but no longer the default. Closes gh-32761 --- .../annotation-mockitobean.adoc | 13 +- .../annotation-testbean.adoc | 9 +- .../bean-overriding.adoc | 14 +- .../BeanOverrideBeanFactoryPostProcessor.java | 27 ++- .../bean/override/OverrideMetadata.java | 8 +- .../bean/override/convention/TestBean.java | 23 +- .../convention/TestBeanOverrideProcessor.java | 6 +- .../bean/override/mockito/MockitoBean.java | 15 +- .../override/mockito/MockitoMetadata.java | 6 +- .../bean/override/mockito/MockitoSpyBean.java | 14 +- .../bean/override/OverrideMetadataTests.java | 2 +- .../AbstractTestBeanIntegrationTestCase.java | 4 +- .../TestBeanByTypeIntegrationTests.java | 140 +++++++++++ .../TestBeanInheritanceIntegrationTests.java | 4 +- .../convention/TestBeanIntegrationTests.java | 14 +- .../example/TestOverrideMetadata.java | 2 +- .../mockito/MockitoBeanIntegrationTests.java | 8 +- .../MockitoByTypeIntegrationTests.java | 223 ++++++++++++++++++ .../MockitoSpyBeanIntegrationTests.java | 24 +- 19 files changed, 483 insertions(+), 73 deletions(-) create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanByTypeIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoByTypeIntegrationTests.java 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 5549df0ff1..8e4d42deee 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 @@ -6,17 +6,24 @@ the test's `ApplicationContext` with a Mockito mock or spy, respectively. In the case, the original bean definition is not replaced, but instead an early instance of the bean is captured and wrapped by the spy. -By default, the name of the bean to override is derived from the annotated field's name, -but both annotations allow for a specific `name` to be provided. Each annotation also -defines Mockito-specific attributes to fine-tune the mocking details. +Users are encouraged to make bean overriding as explicit and unambiguous as possible, +typically by specifying a bean `name` in the annotation. +If no bean `name` is specified, the annotated field's type is used to search for candidate +definitions to override. +Each annotation also defines Mockito-specific attributes to fine-tune the mocking details. The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE_DEFINITION` xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding]. +It requires that at most one candidate definition exists if a bean name is specified, +or exactly one if no bean name is specified. + The `@MockitoSpyBean` annotation uses the `WRAP_BEAN` xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy], and the original instance is wrapped in a Mockito spy. +It requires that exactly one candidate definition exists. + The following example shows how to configure the bean name via `@MockitoBean` and `@MockitoSpyBean`: 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 d7f68476f7..18bc13d4ff 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 @@ -5,12 +5,17 @@ `ApplicationContext` with an instance provided by a conventionally named static factory method. -By default, the bean name and the associated factory method name are derived from the -annotated field's name, but the annotation allows for specific values to be provided. +By default, the associated factory method name is derived from the annotated field's name, +but the annotation allows for a specific method name to be provided. The `@TestBean` annotation uses the `REPLACE_DEFINITION` xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding]. +Users are encouraged to make bean overriding as explicit and unambiguous as possible, +typically by specifying a bean `name` in the annotation. +If no bean `name` is specified, the annotated field's type is used to search for candidate +definitions to override. In that case it is required that exactly one definition matches. + The following example shows how to fully configure the `@TestBean` annotation, with explicit values equivalent to the defaults: diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc index 92e6f3d975..974753dc97 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc @@ -58,10 +58,14 @@ by the corresponding `BeanOverrideStrategy`: [NOTE] ==== In contrast to Spring's autowiring mechanism (for example, resolution of an `@Autowired` -field), the bean overriding infrastructure in the TestContext framework does not perform -any heuristics to locate a bean. Instead, the name of the bean to override must be -explicitly provided to or computed by the `BeanOverrideProcessor`. +field), the bean overriding infrastructure in the TestContext framework has limited +heuristics it can perform to locate a bean. Either the `BeanOverrideProcessor` can compute +the name of the bean to override, or it can be unambiguously selected given the type of +the annotated field. -Typically, the user provides the bean name via a custom annotation, or the -`BeanOverrideProcessor` determines the bean name based on some convention. +Typically, the user directly provides the bean name in the custom annotation in order to +make things as explicit as possible. Alternatively, the bean is selected by type by the +`BeanOverrideFactoryPostProcessor`. +Some `BeanOverrideProcessor`s could also internally compute a bean name based on a +convention or another advanced method. ==== diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java index a518fcf6c2..daff62de6e 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java @@ -125,6 +125,17 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, RootBeanDefinition beanDefinition = createBeanDefinition(overrideMetadata); String beanName = overrideMetadata.getBeanName(); + if (beanName == null) { + final String[] candidates = beanFactory.getBeanNamesForType(overrideMetadata.getBeanType()); + if (candidates.length != 1) { + Field f = overrideMetadata.getField(); + throw new IllegalStateException("Unable to select a bean definition to override, " + + candidates.length+ " bean definitions found of type " + overrideMetadata.getBeanType() + + " (as required by annotated field '" + f.getDeclaringClass().getSimpleName() + + "." + f.getName() + "')"); + } + beanName = candidates[0]; + } BeanDefinition existingBeanDefinition = null; if (beanFactory.containsBeanDefinition(beanName)) { @@ -160,9 +171,19 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, private void registerWrapBean(ConfigurableListableBeanFactory beanFactory, OverrideMetadata metadata) { Set existingBeanNames = getExistingBeanNames(beanFactory, metadata.getBeanType()); String beanName = metadata.getBeanName(); - if (!existingBeanNames.contains(beanName)) { - throw new IllegalStateException("Unable to override bean '" + beanName + "' by wrapping," + - " no existing bean instance by this name of type " + metadata.getBeanType()); + if (beanName == null) { + if (existingBeanNames.size() != 1) { + Field f = metadata.getField(); + throw new IllegalStateException("Unable to select a bean to override by wrapping, " + + existingBeanNames.size() + " bean instances found of type " + metadata.getBeanType() + + " (as required by annotated field '" + f.getDeclaringClass().getSimpleName() + + "." + f.getName() + "')"); + } + beanName = existingBeanNames.iterator().next(); + } + else if (!existingBeanNames.contains(beanName)) { + throw new IllegalStateException("Unable to override bean '" + beanName + "' by wrapping; " + + "there is no existing bean instance with that name of type " + metadata.getBeanType()); } this.overrideRegistrar.markWrapEarly(metadata, beanName); this.overrideRegistrar.registerNameForMetadata(metadata, beanName); diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/OverrideMetadata.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/OverrideMetadata.java index 74d0c47a6f..c139c18af4 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/OverrideMetadata.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/OverrideMetadata.java @@ -58,11 +58,13 @@ public abstract class OverrideMetadata { } /** - * Get the bean name to override. - *

Defaults to the name of the {@link #getField() field}. + * Get the bean name to override, or {@code null} to look for a single + * matching bean of type {@link #getBeanType()}. + *

Defaults to {@code null}. */ + @Nullable protected String getBeanName() { - return this.field.getName(); + return null; } /** 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 e3fba8d0ad..45967dff66 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 @@ -26,7 +26,12 @@ import org.springframework.core.annotation.AliasFor; import org.springframework.test.context.bean.override.BeanOverride; /** - * Mark a field to override a bean instance in the {@code BeanFactory}. + * Mark a field to override a bean definition in the {@code BeanFactory}. + * + *

By default, the bean to override is inferred from the type of the + * annotated field. This requires that exactly one matching definition is + * present in the application context. To explicitly specify a bean name to + * replace, set the {@link #value()} or {@link #name()} attribute. * *

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 @@ -38,7 +43,7 @@ import org.springframework.test.context.bean.override.BeanOverride; * that name. *

  • If a method name is not specified, look for exactly one static method named * with a suffix equal to {@value #CONVENTION_SUFFIX} and starting with either the - * name of the annotated field or the name of the bean.
  • + * name of the annotated field or the name of the bean (if specified). * * *

    Consider the following example. @@ -62,13 +67,13 @@ import org.springframework.test.context.bean.override.BeanOverride; * is also replaced in the {@code BeanFactory} so that other injection points * for that bean use the overridden bean instance. * - *

    To make things more explicit, the method name can be set, as shown in the - * following example. + *

    To make things more explicit, the bean and method names can be set, + * as shown in the following example. * *

    
      * class CustomerServiceTests {
      *
    - *     @TestBean(methodName = "createTestCustomerRepository")
    + *     @TestBean(name = "repository", methodName = "createTestCustomerRepository")
      *     private CustomerRepository repository;
      *
      *     // Tests
    @@ -78,10 +83,6 @@ import org.springframework.test.context.bean.override.BeanOverride;
      *     }
      * }
    * - *

    By default, the name of the bean to override is inferred from the name of - * the annotated field. To use a different bean name, set the {@link #value()} or - * {@link #name()} attribute. - * * @author Simon Baslé * @author Stephane Nicoll * @author Sam Brannen @@ -113,8 +114,8 @@ public @interface TestBean { /** * Name of the bean to override. - *

    If left unspecified, the name of the bean to override is the name of - * the annotated field. If specified, the field name is ignored. + *

    If left unspecified, the bean to override is selected according to + * the annotated field's type. * @see #value() */ @AliasFor("value") 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 b8b156ba28..8ce8f56ac5 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 @@ -144,14 +144,14 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor { ResolvableType typeToOverride) { super(field, typeToOverride, BeanOverrideStrategy.REPLACE_DEFINITION); - this.beanName = StringUtils.hasText(overrideAnnotation.name()) ? - overrideAnnotation.name() : field.getName(); + this.beanName = overrideAnnotation.name(); this.overrideMethod = overrideMethod; } @Override + @Nullable protected String getBeanName() { - return this.beanName; + return StringUtils.hasText(this.beanName) ? this.beanName : super.getBeanName(); } @Override 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 536f612cab..2bfc2c9445 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 @@ -28,10 +28,14 @@ import org.mockito.MockSettings; import org.springframework.test.context.bean.override.BeanOverride; /** - * Mark a field to trigger a bean override using a Mockito mock. If no explicit - * {@link #name()} is specified, the annotated field's name is interpreted to - * be the target of the override. In either case, if no existing bean is defined - * a new one will be added to the context. + * Mark a field to trigger a bean override using a Mockito mock. + * + *

    If no explicit {@link #name()} is specified, a target bean definition is + * selected according to the class of the annotated field, and there must be + * exactly one such candidate definition in the context. + * 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. * *

    Dependencies that are known to the application context but are not beans * (such as those @@ -51,7 +55,8 @@ public @interface MockitoBean { /** * The name of the bean to register or replace. - *

    If not specified, the name of the annotated field will be used. + *

    If left unspecified, the bean to override is selected according to + * the annotated field's type. * @return the name of the mocked bean */ String name() default ""; diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoMetadata.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoMetadata.java index e92d1cd219..aa69db7855 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoMetadata.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoMetadata.java @@ -54,11 +54,9 @@ abstract class MockitoMetadata extends OverrideMetadata { @Override + @Nullable protected String getBeanName() { - if (StringUtils.hasText(this.name)) { - return this.name; - } - return super.getBeanName(); + return StringUtils.hasText(this.name) ? this.name : super.getBeanName(); } @Override diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java index 774d9f70e5..9ca3a045e7 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java @@ -28,10 +28,13 @@ import org.springframework.test.context.bean.override.BeanOverride; /** * Mark a field to trigger a bean override using a Mockito spy, which will wrap - * the original instance. If no explicit {@link #name()} is specified, the - * annotated field's name is interpreted to be the target of the override. - * In either case, it is required that the target bean is previously registered - * in the context. + * the original instance. + * + *

    If no explicit {@link #name()} is specified, a target bean is selected + * according to the class of the annotated field, and there must be exactly one + * such candidate bean. + * If a {@link #name()} is specified, it is required that a target bean of that + * name has been previously registered in the application context. * *

    Dependencies that are known to the application context but are not beans * (such as those @@ -50,7 +53,8 @@ public @interface MockitoSpyBean { /** * The name of the bean to spy. - *

    If not specified, the name of the annotated field will be used. + *

    If left unspecified, the bean to override is selected according to + * the annotated field's type. * @return the name of the spied bean */ String name() default ""; diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java index 24d834df9c..c2d3e51074 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java @@ -38,7 +38,7 @@ class OverrideMetadataTests { @Test void implicitConfigurations() throws Exception { OverrideMetadata metadata = exampleOverride(); - assertThat(metadata.getBeanName()).as("expectedBeanName").isEqualTo(metadata.getField().getName()); + assertThat(metadata.getBeanName()).as("expectedBeanName").isNull(); } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/AbstractTestBeanIntegrationTestCase.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/AbstractTestBeanIntegrationTestCase.java index 11d5a423c6..3e6e4402c2 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/AbstractTestBeanIntegrationTestCase.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/AbstractTestBeanIntegrationTestCase.java @@ -23,10 +23,10 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; @SpringJUnitConfig class AbstractTestBeanIntegrationTestCase { - @TestBean + @TestBean(name = "someBean") Pojo someBean; - @TestBean + @TestBean(name = "otherBean") Pojo otherBean; @TestBean(name = "thirdBean") diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanByTypeIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanByTypeIntegrationTests.java new file mode 100644 index 0000000000..ec0ca0c379 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanByTypeIntegrationTests.java @@ -0,0 +1,140 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.convention; + +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineExecutionResults; +import org.junit.platform.testkit.engine.EngineTestKit; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.InstanceOfAssertFactories.THROWABLE; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +@SpringJUnitConfig(TestBeanByTypeIntegrationTests.ConfigByType.class) +public class TestBeanByTypeIntegrationTests { + + @TestBean + ExampleService anyNameForService; + + static ExampleService anyNameForServiceTestOverride() { + return new RealExampleService("Mocked greeting"); + } + + @Test + void overrideIsFoundByType(ApplicationContext ctx) { + assertThat(this.anyNameForService) + .isSameAs(ctx.getBean("example")) + .isSameAs(ctx.getBean(ExampleService.class)); + + assertThat(this.anyNameForService.greeting()).isEqualTo("Mocked greeting"); + } + + @Test + void zeroCandidates() { + Class caseClass = CaseNone.class; + EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")// + .selectors(selectClass(caseClass))// + .execute(); + + assertThat(results.allEvents().failed().stream()).hasSize(1).first() + .satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class) + .getThrowable()).get(THROWABLE) + .cause() + .isInstanceOf(IllegalStateException.class) + .hasMessage("Unable to select a bean definition to override, 0 bean definitions " + + "found of type %s (as required by annotated field '%s.example')", + ExampleService.class.getName(), caseClass.getSimpleName())); + } + + @Test + void tooManyCandidates() { + Class caseClass = CaseTooMany.class; + EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")// + .selectors(selectClass(caseClass))// + .execute(); + + assertThat(results.allEvents().failed().stream()).hasSize(1).first() + .satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class) + .getThrowable()).get(THROWABLE) + .cause() + .isInstanceOf(IllegalStateException.class) + .hasMessage("Unable to select a bean definition to override, 2 bean definitions " + + "found of type %s (as required by annotated field '%s.example')", + ExampleService.class.getName(), caseClass.getSimpleName())); + } + + @Configuration + static class ConfigByType { + @Bean("example") + ExampleService bean1() { + return new RealExampleService("Production hello"); + } + } + + @SpringJUnitConfig(FailingNone.class) + static class CaseNone { + @TestBean + ExampleService example; + + @Test + void test() {} + + static ExampleService exampleTestOverride() { + fail("unexpected override"); + return null; + } + } + + @Configuration + static class FailingNone { + } + + @SpringJUnitConfig(FailingTooMany.class) + static class CaseTooMany { + @TestBean + ExampleService example; + + @Test + void test() {} + + static ExampleService exampleTestOverride() { + fail("unexpected override"); + return null; + } + } + + @Configuration + static class FailingTooMany { + @Bean + ExampleService bean1() { + return new RealExampleService("1 Hello"); + } + @Bean + ExampleService bean2() { + return new RealExampleService("2 Hello"); + } + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanInheritanceIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanInheritanceIntegrationTests.java index 4ca465da74..4879026eae 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanInheritanceIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanInheritanceIntegrationTests.java @@ -39,10 +39,10 @@ public class TestBeanInheritanceIntegrationTests { @DisplayName("Concrete inherited test with correct @TestBean setup") class ConcreteTestBeanIntegrationTests extends AbstractTestBeanIntegrationTestCase { - @TestBean(methodName = "commonBeanOverride") + @TestBean(name = "pojo", methodName = "commonBeanOverride") Pojo pojo; - @TestBean(methodName = "nestedBeanOverride") + @TestBean(name = "pojo2", methodName = "nestedBeanOverride") Pojo pojo2; static Pojo someBeanTestOverride() { diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanIntegrationTests.java index babb08d332..7d2782ebd5 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanIntegrationTests.java @@ -36,10 +36,10 @@ import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass @SpringJUnitConfig public class TestBeanIntegrationTests { - @TestBean + @TestBean(name = "field") String field; - @TestBean + @TestBean(name = "nestedField") String nestedField; @TestBean(name = "field") @@ -48,10 +48,10 @@ public class TestBeanIntegrationTests { @TestBean(name = "nestedField") String renamed2; - @TestBean(methodName = "fieldTestOverride") + @TestBean(name = "methodRenamed1", methodName = "fieldTestOverride") String methodRenamed1; - @TestBean(methodName = "nestedFieldTestOverride") + @TestBean(name = "methodRenamed2", methodName = "nestedFieldTestOverride") String methodRenamed2; static String fieldTestOverride() { @@ -69,7 +69,7 @@ public class TestBeanIntegrationTests { } @Test - void fieldWithBeanNameHasOverride(ApplicationContext ctx) { + void renamedFieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride"); assertThat(this.renamed1).as("injection point").isEqualTo("fieldOverride"); } @@ -153,7 +153,7 @@ public class TestBeanIntegrationTests { } @Test - void fieldWithBeanNameHasOverride(ApplicationContext ctx) { + void renamedFieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride"); assertThat(TestBeanIntegrationTests.this.renamed2).isEqualTo("nestedFieldOverride"); } @@ -209,7 +209,7 @@ public class TestBeanIntegrationTests { @SpringJUnitConfig static class Failing1 { - @TestBean + @TestBean(name = "noOriginalBean") String noOriginalBean; @Test diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestOverrideMetadata.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestOverrideMetadata.java index a89398f15f..0532561ff1 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestOverrideMetadata.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestOverrideMetadata.java @@ -99,7 +99,7 @@ class TestOverrideMetadata extends OverrideMetadata { if (StringUtils.hasText(this.beanName)) { return this.beanName; } - return super.getBeanName(); + return getField().getName(); } String getAnnotationMethodName() { diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanIntegrationTests.java index be351838dc..7667040133 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanIntegrationTests.java @@ -33,10 +33,10 @@ import static org.assertj.core.api.Assertions.assertThat; @SpringJUnitConfig public class MockitoBeanIntegrationTests { - @MockitoBean + @MockitoBean(name = "field") ExampleService field; - @MockitoBean + @MockitoBean(name = "nestedField") ExampleService nestedField; @MockitoBean(name = "field") @@ -63,7 +63,7 @@ public class MockitoBeanIntegrationTests { } @Test - void fieldWithBeanNameHasOverride(ApplicationContext ctx) { + void renamedFieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("field")) .isInstanceOf(ExampleService.class) .satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock()) @@ -98,7 +98,7 @@ public class MockitoBeanIntegrationTests { } @Test - void fieldWithBeanNameHasOverride(ApplicationContext ctx) { + void renamedFieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("nestedField")) .isInstanceOf(ExampleService.class) .satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock()) diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoByTypeIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoByTypeIntegrationTests.java new file mode 100644 index 0000000000..47b0667504 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoByTypeIntegrationTests.java @@ -0,0 +1,223 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineExecutionResults; +import org.junit.platform.testkit.engine.EngineTestKit; +import org.mockito.Mockito; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.THROWABLE; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +public class MockitoByTypeIntegrationTests { + + @Nested + @SpringJUnitConfig(ConfigByType.class) + class Mock { + + @MockitoBean + ExampleService anyNameForService; + + @Test + void overrideIsFoundByType(ApplicationContext ctx) { + assertThat(this.anyNameForService) + .satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock()) + .as("isMock").isTrue()) + .isSameAs(ctx.getBean("example")) + .isSameAs(ctx.getBean(ExampleService.class)); + + when(this.anyNameForService.greeting()).thenReturn("Mocked greeting"); + + assertThat(this.anyNameForService.greeting()).isEqualTo("Mocked greeting"); + verify(this.anyNameForService, times(1)).greeting(); + verifyNoMoreInteractions(this.anyNameForService); + } + + + @Test + void zeroCandidates() { + Class caseClass = CaseNone.class; + EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")// + .selectors(selectClass(caseClass))// + .execute(); + + assertThat(results.allEvents().failed().stream()).hasSize(1).first() + .satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class) + .getThrowable()).get(THROWABLE) + .cause() + .isInstanceOf(IllegalStateException.class) + .hasMessage("Unable to select a bean definition to override, 0 bean definitions " + + "found of type %s (as required by annotated field '%s.example')", + ExampleService.class.getName(), caseClass.getSimpleName())); + } + + @Test + void tooManyCandidates() { + Class caseClass = CaseTooMany.class; + EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")// + .selectors(selectClass(caseClass))// + .execute(); + + assertThat(results.allEvents().failed().stream()).hasSize(1).first() + .satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class) + .getThrowable()).get(THROWABLE) + .cause() + .isInstanceOf(IllegalStateException.class) + .hasMessage("Unable to select a bean definition to override, 2 bean definitions " + + "found of type %s (as required by annotated field '%s.example')", + ExampleService.class.getName(), caseClass.getSimpleName())); + } + + @SpringJUnitConfig(FailingNone.class) + static class CaseNone { + @MockitoBean + ExampleService example; + + @Test + void test() { + assertThat(example).isNotNull(); + } + } + + @SpringJUnitConfig(FailingTooMany.class) + static class CaseTooMany { + @MockitoBean + ExampleService example; + + @Test + void test() { + assertThat(example).isNotNull(); + } + } + } + + @Nested + @SpringJUnitConfig(ConfigByType.class) + class Spy { + + @MockitoSpyBean + ExampleService anyNameForService; + + @Test + void overrideIsFoundByType(ApplicationContext ctx) { + assertThat(this.anyNameForService) + .satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy()) + .as("isSpy").isTrue()) + .isSameAs(ctx.getBean("example")) + .isSameAs(ctx.getBean(ExampleService.class)); + + assertThat(this.anyNameForService.greeting()).isEqualTo("Production hello"); + verify(this.anyNameForService, times(1)).greeting(); + verifyNoMoreInteractions(this.anyNameForService); + } + + @Test + void zeroCandidates() { + Class caseClass = CaseNone.class; + EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")// + .selectors(selectClass(caseClass))// + .execute(); + + assertThat(results.allEvents().failed().stream()).hasSize(1).first() + .satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class) + .getThrowable()).get(THROWABLE) + .cause() + .isInstanceOf(IllegalStateException.class) + .hasMessage("Unable to select a bean definition to override, 0 bean definitions " + + "found of type %s (as required by annotated field '%s.example')", + ExampleService.class.getName(), caseClass.getSimpleName())); + } + + @Test + void tooManyCandidates() { + Class caseClass = CaseTooMany.class; + EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")// + .selectors(selectClass(caseClass))// + .execute(); + + assertThat(results.allEvents().failed().stream()).hasSize(1).first() + .satisfies(e -> assertThat(e.getRequiredPayload(TestExecutionResult.class) + .getThrowable()).get(THROWABLE) + .cause() + .isInstanceOf(IllegalStateException.class) + .hasMessage("Unable to select a bean definition to override, 2 bean definitions " + + "found of type %s (as required by annotated field '%s.example')", + ExampleService.class.getName(), caseClass.getSimpleName())); + } + + @SpringJUnitConfig(FailingNone.class) + static class CaseNone { + @MockitoBean + ExampleService example; + + @Test + void test() { + assertThat(example).isNotNull(); + } + } + + @SpringJUnitConfig(FailingTooMany.class) + static class CaseTooMany { + @MockitoBean + ExampleService example; + + @Test + void test() { + assertThat(example).isNotNull(); + } + } + } + + @Configuration + static class ConfigByType { + @Bean("example") + ExampleService bean1() { + return new RealExampleService("Production hello"); + } + } + + @Configuration + static class FailingNone { + } + + @Configuration + static class FailingTooMany { + @Bean + ExampleService bean1() { + return new RealExampleService("1 Hello"); + } + @Bean + ExampleService bean2() { + return new RealExampleService("2 Hello"); + } + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanIntegrationTests.java index 9cb01c3ffa..24c0076d03 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanIntegrationTests.java @@ -35,10 +35,10 @@ import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass @SpringJUnitConfig(MockitoBeanIntegrationTests.Config.class) public class MockitoSpyBeanIntegrationTests { - @MockitoSpyBean + @MockitoSpyBean(name = "field") ExampleService field; - @MockitoSpyBean + @MockitoSpyBean(name = "nestedField") ExampleService nestedField; @MockitoSpyBean(name = "field") @@ -57,11 +57,10 @@ public class MockitoSpyBeanIntegrationTests { assertThat(this.field.greeting()).as("spied greeting") .isEqualTo("Hello Field"); - } @Test - void fieldWithBeanNameHasOverride(ApplicationContext ctx) { + void renamedFieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("field")) .isInstanceOf(ExampleService.class) .satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy()) @@ -73,7 +72,7 @@ public class MockitoSpyBeanIntegrationTests { } @Test - void failWhenBeanNotPresentFieldName() { + void failWhenBeanNotPresentByType() { EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")// .selectors(selectClass(Failure1.class))// .execute(); @@ -83,13 +82,14 @@ public class MockitoSpyBeanIntegrationTests { .getThrowable()).get(THROWABLE) .cause() .isInstanceOf(IllegalStateException.class) - .hasMessage("Unable to override bean 'notPresent' by wrapping," + - " no existing bean instance by this name of type %s", - ExampleService.class.getName())); + .hasMessage("Unable to select a bean to override by wrapping, " + + "0 bean instances found of type %s " + + "(as required by annotated field '%s.notPresent')", + ExampleService.class.getName(), Failure1.class.getSimpleName())); } @Test - void failWhenBeanNotPresentExplicitName() { + void failWhenBeanNotPresentByExplicitName() { EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")// .selectors(selectClass(Failure2.class))// .execute(); @@ -99,8 +99,8 @@ public class MockitoSpyBeanIntegrationTests { .getThrowable()).get(THROWABLE) .cause() .isInstanceOf(IllegalStateException.class) - .hasMessage("Unable to override bean 'notPresentAtAll' by wrapping," + - " no existing bean instance by this name of type %s", + .hasMessage("Unable to override bean 'notPresentAtAll' by wrapping; " + + "there is no existing bean instance with that name of type %s", ExampleService.class.getName())); } @@ -122,7 +122,7 @@ public class MockitoSpyBeanIntegrationTests { } @Test - void fieldWithBeanNameHasOverride(ApplicationContext ctx) { + void renamedFieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("nestedField")) .isInstanceOf(ExampleService.class) .satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy())