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())