From 28f62abda44c607997c5a32ba3fbd857a092ab4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 10 Jun 2024 18:07:36 +0200 Subject: [PATCH] Use the field name as a fallback qualifier for Bean Overriding This commit harmonizes how a candidate bean definition is determined for overriding using `@TestBean`, `@MockitoBean`, and `@MockitoSpyBean`. Previously, a qualifier was necessary even if the name of the annotated field matches the name of a candidate. After this commit, such candidate will be picked up transparently, the same it is done for regular autowiring. This commit also reviews the documentation of the feature as considering the field means that its name is taken into account to compute a cache key if by-type lookup is requested. Closes gh-32939 --- .../annotation-mockitobean.adoc | 100 +++++++++++++-- .../annotation-testbean.adoc | 31 +++-- .../BeanOverrideBeanFactoryPostProcessor.java | 7 + .../bean/override/OverrideMetadata.java | 18 ++- ...OverrideBeanFactoryPostProcessorTests.java | 121 +++++++++++++----- .../bean/override/OverrideMetadataTests.java | 25 +++- .../TestBeanOverrideMetadataTests.java | 22 +++- ...itoBeanContextCustomizerEqualityTests.java | 84 ++++++++++-- .../MockitoBeanOverrideMetadataTests.java | 24 +++- .../MockitoSpyBeanOverrideMetadataTests.java | 20 ++- 10 files changed, 357 insertions(+), 95 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 d6de89bd344e..85442b628d06 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 @@ -7,17 +7,27 @@ case, the original bean definition is not replaced, but instead an early instanc bean is captured and wrapped by the spy. By default, the annotated field's type is used to search for candidate definitions to -override, but note that `@Qualifier` annotations are also taken into account for the -purpose of matching. Users can also make things entirely explicit by specifying a bean -`name` in the annotation. +override. If multiple candidates match, the usual `@Qualifier` can be provided to +narrow the candidate to override. Alternatively, a candidate whose bean definition name +matches the name of the field will match. + +To use a by-name override rather than a by-type override, specify the `name` attribute +of the annotation. + +[WARNING] +==== +The qualifiers, including the name of the field are used to determine if a separate +`ApplicationContext` needs to be created. If you are using this feature to mock or +spy the same bean in several tests, make sure to name the field consistently to avoid +creating unnecessary contexts. +==== 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 matching candidate definition exists if a bean name -is specified, or exactly one if no bean name is specified. +If no definition matches, then a definition is created on-the-fly. The `@MockitoSpyBean` annotation uses the `WRAP_BEAN` xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy], @@ -25,8 +35,7 @@ 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`: +The following example shows how to use the default behavior of the `@MockitoBean` annotation: [tabs] ====== @@ -35,17 +44,80 @@ Java:: [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- class OverrideBeanTests { + @MockitoBean // <1> + private CustomService customService; + + // test case body... + } +---- +<1> Replace the bean with type `CustomService` with a Mockito `mock`. +====== + +In the example above, we are creating a mock for `CustomService`. If more that +one bean with such type exist, the bean named `customService` is considered. Otherwise, +the test will fail and you will need to provide a qualifier of some sort to identify which +of the `CustomService` beans you want to override. If no such bean exists, a bean +definition will be created with an auto-generated bean name. - @MockitoBean(name = "service1") // <1> - private CustomService mockService; +The following example uses a by-name lookup, rather than a by-type lookup: - @MockitoSpyBean(name = "service2") // <2> - private CustomService spyService; // <3> +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + class OverrideBeanTests { + @MockitoBean(name = "service") // <1> + private CustomService customService; // test case body... + + } +---- +<1> Replace the bean named `service` with a Mockito `mock`. +====== + +If no bean definition named `service` exists, one is created. + +The following example shows how to use the default behavior of the `@MockitoSpyBean` annotation: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + class OverrideBeanTests { + @MockitoSpyBean // <1> + private CustomService customService; + + // test case body... + } +---- +<1> Wrap the bean with type `CustomService` with a Mockito `spy`. +====== + +In the example above, we are wrapping the bean with type `CustomService`. If more that +one bean with such type exist, the bean named `customService` is considered. Otherwise, +the test will fail and you will need to provide a qualifier of some sort to identify which +of the `CustomService` beans you want to spy. + +The following example uses a by-name lookup, rather than a by-type lookup: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + class OverrideBeanTests { + @MockitoSpyBean(name = "service") // <1> + private CustomService customService; + + // test case body... + } ---- -<1> Mark `mockService` as a Mockito mock override of bean `service1` in this test class. -<2> Mark `spyService` as a Mockito spy override of bean `service2` in this test class. -<3> The fields will be injected with the Mockito mock and spy, respectively. +<1> Wrap the bean named `service` with a Mockito `spy`. ====== 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 beaca680f752..7e68e0dec034 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 @@ -11,11 +11,21 @@ with the type of the bean to override is expected. To make things more explicit, you'd rather use a different name, the annotation allows for a specific method name to be provided. +By default, the annotated field's type is used to search for candidate definitions to +override. If multiple candidates match, the usual `@Qualifier` can be provided to +narrow the candidate to override. Alternatively, a candidate whose bean definition name +matches the name of the field will match. -By default, 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, but note that `@Qualifier` -annotations are also taken into account for the purpose of matching. -Users can also make things entirely explicit by specifying a bean `name` in the annotation. +To use a by-name override rather than a by-type override, specify the `name` attribute +of the annotation. + +[WARNING] +==== +The qualifiers, including the name of the field are used to determine if a separate +`ApplicationContext` needs to be created. If you are using this feature to override +the same bean in several tests, make sure to name the field consistently to avoid +creating unnecessary contexts. +==== The following example shows how to use the default behavior of the `@TestBean` annotation: @@ -27,11 +37,11 @@ Java:: ---- class OverrideBeanTests { @TestBean // <1> - private CustomService service; + private CustomService customService; // test case body... - private static CustomService service() { // <2> + private static CustomService customService() { // <2> return new MyFakeCustomService(); } } @@ -40,8 +50,13 @@ Java:: <2> The result of this static method will be used as the instance and injected into the field. ====== +In the example above, we are overriding the bean with type `CustomService`. If more that +one bean with such type exist, the bean named `customService` is considered. Otherwise, +the test will fail and you will need to provide a qualifier of some sort to identify which +of the `CustomService` beans you want to override. + -The following example shows how to fully configure the `@TestBean` annotation: +The following example uses a by-name lookup, rather than a by-type lookup: [tabs] ====== @@ -51,7 +66,7 @@ Java:: ---- class OverrideBeanTests { @TestBean(name = "service", methodName = "createCustomService") // <1> - private CustomService service; + private CustomService customService; // test case body... 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 1801081eda74..d98fc3bb86e4 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 @@ -259,6 +259,13 @@ private Set getExistingBeanNamesByType(ConfigurableListableBeanFactory b else { beans.removeIf(ScopedProxyUtils::isScopedTarget); } + // In case of multiple matches, last resort fallback on the field's name + if (beans.size() > 1) { + String fieldName = metadata.getField().getName(); + if (beans.contains(fieldName)) { + return Set.of(fieldName); + } + } return beans; } 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 c7a323e354a0..6ff6f47e92f3 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 @@ -167,16 +167,24 @@ public boolean equals(Object obj) { return false; } OverrideMetadata that = (OverrideMetadata) obj; - return Objects.equals(this.beanType.getType(), that.beanType.getType()) && - Objects.equals(this.beanName, that.beanName) && - Objects.equals(this.strategy, that.strategy) && + if (!Objects.equals(this.beanType.getType(), that.beanType.getType()) || + !Objects.equals(this.beanName, that.beanName) || + !Objects.equals(this.strategy, that.strategy)) { + return false; + } + if (this.beanName != null) { + return true; + } + // by type lookup + return Objects.equals(this.field.getName(), that.field.getName()) && Arrays.equals(this.field.getAnnotations(), that.field.getAnnotations()); } @Override public int hashCode() { - return Objects.hash(this.beanType.getType(), this.beanName, this.strategy, - Arrays.hashCode(this.field.getAnnotations())); + int hash = Objects.hash(this.beanType.getType(), this.beanName, this.strategy); + return (this.beanName != null ? hash : hash + + Objects.hash(this.field.getName(), Arrays.hashCode(this.field.getAnnotations()))); } @Override diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java index 139c5b4a2a24..dd1dc22c480b 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java @@ -55,18 +55,16 @@ class BeanOverrideBeanFactoryPostProcessorTests { @Test - void canReplaceExistingBeanDefinitions() { - AnnotationConfigApplicationContext context = createContext(CaseByNameAndByType.class); + void replaceBeanByNameWithMatchingBeanDefinition() { + AnnotationConfigApplicationContext context = createContext(CaseByName.class); context.registerBean("descriptionBean", String.class, () -> "Original"); - context.registerBean("someInteger", Integer.class, () -> 1); context.refresh(); assertThat(context.getBean("descriptionBean")).isEqualTo("overridden"); - assertThat(context.getBean("someInteger")).isSameAs(42); } @Test - void cannotReplaceIfNoBeanNameMatching() { + void replaceBeanByNameWithoutMatchingBeanDefinitionFails() { AnnotationConfigApplicationContext context = createContext(CaseByName.class); assertThatIllegalStateException() @@ -76,7 +74,52 @@ void cannotReplaceIfNoBeanNameMatching() { } @Test - void cannotReplaceIfNoBeanTypeMatching() { + void replaceBeanByNameWithMatchingBeanDefinitionAndWrongTypeFails() { + AnnotationConfigApplicationContext context = createContext(CaseByName.class); + context.registerBean("descriptionBean", Integer.class, () -> -1); + + assertThatIllegalStateException() + .isThrownBy(context::refresh) + .withMessage("Unable to override bean 'descriptionBean': there is no bean definition " + + "to replace with that name of type java.lang.String"); + } + + @Test + void replaceBeanByNameCanOverrideBeanProducedByFactoryBeanWithClassObjectTypeAttribute() { + AnnotationConfigApplicationContext context = prepareContextWithFactoryBean(CharSequence.class); + context.refresh(); + + assertThat(context.getBean("beanToBeOverridden")).isEqualTo("overridden"); + } + + @Test + void replaceBeanByNameCanOverrideBeanProducedByFactoryBeanWithResolvableTypeObjectTypeAttribute() { + AnnotationConfigApplicationContext context = prepareContextWithFactoryBean(ResolvableType.forClass(CharSequence.class)); + context.refresh(); + + assertThat(context.getBean("beanToBeOverridden")).isEqualTo("overridden"); + } + + private AnnotationConfigApplicationContext prepareContextWithFactoryBean(Object objectTypeAttribute) { + AnnotationConfigApplicationContext context = createContext(CaseOverrideBeanProducedByFactoryBean.class); + context.registerBean("testFactoryBean", TestFactoryBean.class, TestFactoryBean::new); + RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(TestFactoryBean.class); + factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, objectTypeAttribute); + context.registerBeanDefinition("beanToBeOverridden", factoryBeanDefinition); + return context; + } + + @Test + void replaceBeanByTypeWithSingleMatchingBean() { + AnnotationConfigApplicationContext context = createContext(CaseByType.class); + context.registerBean("someInteger", Integer.class, () -> 1); + context.refresh(); + + assertThat(context.getBean("someInteger")).isEqualTo(42); + } + + @Test + void replaceBeanByTypeWithoutMatchingBeanFails() { AnnotationConfigApplicationContext context = createContext(CaseByType.class); assertThatIllegalStateException() @@ -86,51 +129,62 @@ void cannotReplaceIfNoBeanTypeMatching() { } @Test - void canReplaceExistingBeanDefinitionsWithCreateReplaceStrategy() { - AnnotationConfigApplicationContext context = createContext(CaseByNameAndByTypeWithReplaceOrCreateStrategy.class); - context.register(CaseByNameAndByTypeWithReplaceOrCreateStrategy.class); - context.registerBean("descriptionBean", String.class, () -> "Original"); + void replaceBeanByTypeWithMultipleMatchesAndNoQualifierFails() { + AnnotationConfigApplicationContext context = createContext(CaseByType.class); context.registerBean("someInteger", Integer.class, () -> 1); + context.registerBean("anotherInteger", Integer.class, () -> 2); + + assertThatIllegalStateException() + .isThrownBy(context::refresh) + .withMessage("Unable to select a bean definition to override: found 2 bean definitions " + + "of type java.lang.Integer (as required by annotated field 'CaseByType.counter'): " + + "[someInteger, anotherInteger]"); + } + + @Test + void replaceBeanByTypeWithMultipleMatchesAndFieldNameQualifierMatches() { + AnnotationConfigApplicationContext context = createContext(CaseByType.class); + context.registerBean("counter", Integer.class, () -> 1); + context.registerBean("someInteger", Integer.class, () -> 2); context.refresh(); - assertThat(context.getBean("descriptionBean")).isEqualTo("overridden"); - assertThat(context.getBean("someInteger")).isEqualTo(42); + assertThat(context.getBean("counter")).isSameAs(42); } @Test - void canCreateIfOriginalMissingWithCreateReplaceStrategy() { - AnnotationConfigApplicationContext context = createContext(CaseByNameAndByTypeWithReplaceOrCreateStrategy.class); + void createOrReplaceBeanByNameWithMatchingBeanDefinition() { + AnnotationConfigApplicationContext context = createContext(CaseByNameWithReplaceOrCreateStrategy.class); + context.registerBean("descriptionBean", String.class, () -> "Original"); context.refresh(); - String byTypeGeneratedBeanName = "java.lang.Integer#0"; - assertThat(context.getBeanDefinitionNames()).contains("descriptionBean", byTypeGeneratedBeanName); assertThat(context.getBean("descriptionBean")).isEqualTo("overridden"); - assertThat(context.getBean(byTypeGeneratedBeanName)).isEqualTo(42); } @Test - void canOverrideBeanProducedByFactoryBeanWithClassObjectTypeAttribute() { - AnnotationConfigApplicationContext context = prepareContextWithFactoryBean(CharSequence.class); + void createOrReplaceBeanByNameWithoutMatchingDefinitionCreatesBeanDefinition() { + AnnotationConfigApplicationContext context = createContext(CaseByNameWithReplaceOrCreateStrategy.class); context.refresh(); - assertThat(context.getBean("beanToBeOverridden")).isEqualTo("overridden"); + assertThat(context.getBean("descriptionBean")).isEqualTo("overridden"); } @Test - void canOverrideBeanProducedByFactoryBeanWithResolvableTypeObjectTypeAttribute() { - AnnotationConfigApplicationContext context = prepareContextWithFactoryBean(ResolvableType.forClass(CharSequence.class)); + void createOrReplaceBeanByTypeWithMatchingBean() { + AnnotationConfigApplicationContext context = createContext(CaseByTypeWithReplaceOrCreateStrategy.class); + context.registerBean("someBean", String.class, () -> "Original"); context.refresh(); - assertThat(context.getBean("beanToBeOverridden")).isEqualTo("overridden"); + assertThat(context.getBean("someBean")).isEqualTo("overridden"); } - private AnnotationConfigApplicationContext prepareContextWithFactoryBean(Object objectTypeAttribute) { - AnnotationConfigApplicationContext context = createContext(CaseOverrideBeanProducedByFactoryBean.class); - context.registerBean("testFactoryBean", TestFactoryBean.class, TestFactoryBean::new); - RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(TestFactoryBean.class); - factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, objectTypeAttribute); - context.registerBeanDefinition("beanToBeOverridden", factoryBeanDefinition); - return context; + @Test + void createOrReplaceBeanByTypeWithoutMatchingDefinitionCreatesBeanDefinition() { + AnnotationConfigApplicationContext context = createContext(CaseByTypeWithReplaceOrCreateStrategy.class); + context.refresh(); + + String generatedBeanName = "java.lang.String#0"; + assertThat(context.getBeanDefinitionNames()).contains(generatedBeanName); + assertThat(context.getBean(generatedBeanName)).isEqualTo("overridden"); } @Test @@ -216,14 +270,11 @@ static class CaseByType { } - static class CaseByNameAndByType { + static class CaseByNameWithReplaceOrCreateStrategy { - @DummyBean(beanName = "descriptionBean") + @DummyBean(beanName = "descriptionBean", strategy = BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION) private String description; - @DummyBean - private Integer counter; - } static class CaseByTypeWithReplaceOrCreateStrategy { 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 1cf31c70c3c1..8f84af1ab1cf 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 @@ -68,9 +68,9 @@ void forTestClassWithMultipleFieldsSameMetadata() { List overrideMetadata = OverrideMetadata.forTestClass(MultipleAnnotationsDuplicate.class); assertThat(overrideMetadata).hasSize(2) .anySatisfy(hasTestBeanMetadata( - field(MultipleAnnotationsDuplicate.class, "message1"), String.class, null)) + field(MultipleAnnotationsDuplicate.class, "message1"), String.class, "messageBean")) .anySatisfy(hasTestBeanMetadata( - field(MultipleAnnotationsDuplicate.class, "message2"), String.class, null)); + field(MultipleAnnotationsDuplicate.class, "message2"), String.class, "messageBean")); assertThat(new HashSet<>(overrideMetadata)).hasSize(1); } @@ -127,6 +127,14 @@ void isEqualToWithSameMetadataButDifferentFields() { assertThat(metadata).hasSameHashCodeAs(metadata2); } + @Test + void isEqualToWithByNameLookupAndDifferentFieldNames() { + OverrideMetadata metadata = createMetadata(field(ConfigA.class, "noQualifier"), "beanToOverride"); + OverrideMetadata metadata2 = createMetadata(field(ConfigB.class, "example"), "beanToOverride"); + assertThat(metadata).isEqualTo(metadata2); + assertThat(metadata).hasSameHashCodeAs(metadata2); + } + @Test void isEqualToWithSameMetadataAndSameQualifierValues() { OverrideMetadata metadata = createMetadata(field(ConfigA.class, "directQualifier")); @@ -149,6 +157,13 @@ void isNotEqualToWithSameMetadataAndDifferentQualifiers() { assertThat(metadata).isNotEqualTo(metadata2); } + @Test + void isNotEqualToWithByTypeLookupAndDifferentFieldNames() { + OverrideMetadata metadata = createMetadata(field(ConfigA.class, "noQualifier")); + OverrideMetadata metadata2 = createMetadata(field(ConfigB.class, "example")); + assertThat(metadata).isNotEqualTo(metadata2); + } + private OverrideMetadata createMetadata(Field field) { return createMetadata(field, null); } @@ -195,10 +210,10 @@ static class MultipleAnnotations { static class MultipleAnnotationsDuplicate { - @DummyBean + @DummyBean(beanName = "messageBean") String message1; - @DummyBean + @DummyBean(beanName = "messageBean") String message2; } @@ -233,6 +248,8 @@ public static class ConfigB { private ExampleService noQualifier; + private ExampleService example; + @Qualifier("test") private ExampleService directQualifier; 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 64b4277bb1a8..e7df2a12db5a 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 @@ -74,13 +74,20 @@ void isEqualToWithSameMetadata() { } @Test - void isEqualToWithSameMetadataButDifferentField() { - TestBeanOverrideMetadata metadata1 = createMetadata(sampleField("message"), sampleMethod("message")); - TestBeanOverrideMetadata metadata2 = createMetadata(sampleField("message2"), sampleMethod("message")); + void isEqualToWithSameMetadataByNameLookupAndDifferentField() { + TestBeanOverrideMetadata metadata1 = createMetadata(sampleField("message3"), sampleMethod("message")); + TestBeanOverrideMetadata metadata2 = createMetadata(sampleField("message4"), sampleMethod("message")); assertThat(metadata1).isEqualTo(metadata2); assertThat(metadata1).hasSameHashCodeAs(metadata2); } + @Test + void isNotEqualToWithSameMetadataByTypeLookupAndDifferentField() { + TestBeanOverrideMetadata metadata1 = createMetadata(sampleField("message"), sampleMethod("message")); + TestBeanOverrideMetadata metadata2 = createMetadata(sampleField("message2"), sampleMethod("message")); + assertThat(metadata1).isNotEqualTo(metadata2); + } + @Test void isNotEqualToWithSameMetadataButDifferentBeanName() { TestBeanOverrideMetadata metadata1 = createMetadata(sampleField("message"), sampleMethod("message")); @@ -98,7 +105,7 @@ void isNotEqualToWithSameMetadataButDifferentMethod() { @Test void isNotEqualToWithSameMetadataButDifferentAnnotations() { TestBeanOverrideMetadata metadata1 = createMetadata(sampleField("message"), sampleMethod("message")); - TestBeanOverrideMetadata metadata2 = createMetadata(sampleField("message4"), sampleMethod("message")); + TestBeanOverrideMetadata metadata2 = createMetadata(sampleField("message5"), sampleMethod("message")); assertThat(metadata1).isNotEqualTo(metadata2); } @@ -116,7 +123,7 @@ private Method sampleMethod(String noArgMethodName) { private TestBeanOverrideMetadata createMetadata(Field field, Method overrideMethod) { TestBean annotation = field.getAnnotation(TestBean.class); - String beanName = (StringUtils.hasText(annotation.value()) ? annotation.value() : null); + String beanName = (StringUtils.hasText(annotation.name()) ? annotation.name() : null); return new TestBeanOverrideMetadata(field, ResolvableType.forClass(field.getType()), beanName, overrideMethod); } @@ -162,9 +169,12 @@ static class Sample { @TestBean(name = "anotherBean") private String message3; + @TestBean(name = "anotherBean") + private String message4; + @Qualifier("anotherBean") @TestBean - private String message4; + private String message5; static String message() { return "OK"; diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanContextCustomizerEqualityTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanContextCustomizerEqualityTests.java index 15851036a823..8ea5de3c786a 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanContextCustomizerEqualityTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanContextCustomizerEqualityTests.java @@ -34,28 +34,48 @@ class MockitoBeanContextCustomizerEqualityTests { @Test - void contextCustomizerWithSameMockInDifferentClassIsEqual() { - assertThat(createContextCustomizer(Case1.class)).isEqualTo(createContextCustomizer(Case2.class)); + void contextCustomizerWithSameMockByNameInDifferentClassIsEqual() { + assertThat(createContextCustomizer(Case1ByName.class)).isEqualTo(createContextCustomizer(Case2ByName.class)); } @Test - void contextCustomizerWithSameSpyInDifferentClassIsEqual() { - assertThat(createContextCustomizer(Case4.class)).isEqualTo(createContextCustomizer(Case5.class)); + void contextCustomizerWithSameMockByTypeInDifferentClassIsEqual() { + assertThat(createContextCustomizer(Case1ByType.class)).isEqualTo(createContextCustomizer(Case2ByTypeSameFieldName.class)); + } + + @Test + void contextCustomizerWithSameMockByTypeAndDifferentFieldNamesAreNotEqual() { + assertThat(createContextCustomizer(Case1ByType.class)).isNotEqualTo(createContextCustomizer(Case2ByType.class)); + } + + @Test + void contextCustomizerWithSameSpyByNameInDifferentClassIsEqual() { + assertThat(createContextCustomizer(Case4ByName.class)).isEqualTo(createContextCustomizer(Case5ByName.class)); + } + + @Test + void contextCustomizerWithSameSpyByTypeInDifferentClassIsEqual() { + assertThat(createContextCustomizer(Case4ByType.class)).isEqualTo(createContextCustomizer(Case5ByTypeSameFieldName.class)); + } + + @Test + void contextCustomizerWithSameSpyByTypeAndDifferentFieldNamesAreNotEqual() { + assertThat(createContextCustomizer(Case4ByType.class)).isNotEqualTo(createContextCustomizer(Case5ByType.class)); } @Test void contextCustomizerWithSimilarMockButDifferentAnswersIsNotEqual() { - assertThat(createContextCustomizer(Case1.class)).isNotEqualTo(createContextCustomizer(Case3.class)); + assertThat(createContextCustomizer(Case1ByType.class)).isNotEqualTo(createContextCustomizer(Case3.class)); } @Test void contextCustomizerWithSimilarSpyButDifferentProxyTargetClassFlagIsNotEqual() { - assertThat(createContextCustomizer(Case5.class)).isNotEqualTo(createContextCustomizer(Case6.class)); + assertThat(createContextCustomizer(Case5ByType.class)).isNotEqualTo(createContextCustomizer(Case6.class)); } @Test void contextCustomizerWithMockAndSpyAreNotEqual() { - assertThat(createContextCustomizer(Case1.class)).isNotEqualTo(createContextCustomizer(Case4.class)); + assertThat(createContextCustomizer(Case1ByType.class)).isNotEqualTo(createContextCustomizer(Case4ByType.class)); } private ContextCustomizer createContextCustomizer(Class testClass) { @@ -64,20 +84,41 @@ private ContextCustomizer createContextCustomizer(Class testClass) { return customizer; } - static class Case1 { + static class Case1ByName { + + @MockitoBean(name = "serviceBean") + private String exampleService; + + } + + static class Case1ByType { @MockitoBean private String exampleService; } - static class Case2 { + static class Case2ByName { + + @MockitoBean(name = "serviceBean") + private String serviceToMock; + + } + + static class Case2ByType { @MockitoBean private String serviceToMock; } + static class Case2ByTypeSameFieldName { + + @MockitoBean + private String exampleService; + + } + static class Case3 { @MockitoBean(answers = Answers.RETURNS_MOCKS) @@ -85,20 +126,41 @@ static class Case3 { } - static class Case4 { + static class Case4ByName { + + @MockitoSpyBean(name = "serviceBean") + private String exampleService; + + } + + static class Case4ByType { @MockitoSpyBean private String exampleService; } - static class Case5 { + static class Case5ByName { + + @MockitoSpyBean(name = "serviceBean") + private String serviceToMock; + + } + + static class Case5ByType { @MockitoSpyBean private String serviceToMock; } + static class Case5ByTypeSameFieldName { + + @MockitoSpyBean + private String exampleService; + + } + static class Case6 { @MockitoSpyBean(proxyTargetAware = false) diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideMetadataTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideMetadataTests.java index 61b1fd850312..99aad2960db5 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideMetadataTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideMetadataTests.java @@ -64,9 +64,16 @@ void isEqualToWithSameMetadata() { } @Test - void isEqualToWithSameMetadataButDifferentField() { + void isNotEqualEqualToByTypeLookupWithSameMetadataButDifferentField() { MockitoBeanOverrideMetadata metadata = createMetadata(sampleField("service")); MockitoBeanOverrideMetadata metadata2 = createMetadata(sampleField("service2")); + assertThat(metadata).isNotEqualTo(metadata2); + } + + @Test + void isEqualEqualToByNameLookupWithSameMetadataButDifferentField() { + MockitoBeanOverrideMetadata metadata = createMetadata(sampleField("service3")); + MockitoBeanOverrideMetadata metadata2 = createMetadata(sampleField("service4")); assertThat(metadata).isEqualTo(metadata2); assertThat(metadata).hasSameHashCodeAs(metadata2); } @@ -81,21 +88,21 @@ void isNotEqualToWithSameMetadataButDifferentBeanName() { @Test void isNotEqualToWithSameMetadataButDifferentExtraInterfaces() { MockitoBeanOverrideMetadata metadata = createMetadata(sampleField("service")); - MockitoBeanOverrideMetadata metadata2 = createMetadata(sampleField("service4")); + MockitoBeanOverrideMetadata metadata2 = createMetadata(sampleField("service5")); assertThat(metadata).isNotEqualTo(metadata2); } @Test void isNotEqualToWithSameMetadataButDifferentAnswers() { MockitoBeanOverrideMetadata metadata = createMetadata(sampleField("service")); - MockitoBeanOverrideMetadata metadata2 = createMetadata(sampleField("service5")); + MockitoBeanOverrideMetadata metadata2 = createMetadata(sampleField("service6")); assertThat(metadata).isNotEqualTo(metadata2); } @Test void isNotEqualToWithSameMetadataButDifferentSerializableFlag() { MockitoBeanOverrideMetadata metadata = createMetadata(sampleField("service")); - MockitoBeanOverrideMetadata metadata2 = createMetadata(sampleField("service6")); + MockitoBeanOverrideMetadata metadata2 = createMetadata(sampleField("service7")); assertThat(metadata).isNotEqualTo(metadata2); } @@ -137,15 +144,18 @@ static class Sample { @MockitoBean(name = "beanToMock") private String service3; - @MockitoBean(extraInterfaces = Externalizable.class) + @MockitoBean(name = "beanToMock") private String service4; - @MockitoBean(answers = Answers.RETURNS_MOCKS) + @MockitoBean(extraInterfaces = Externalizable.class) private String service5; - @MockitoBean(serializable = true) + @MockitoBean(answers = Answers.RETURNS_MOCKS) private String service6; + @MockitoBean(serializable = true) + private String service7; + } } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideMetadataTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideMetadataTests.java index c02e4ebfa937..1872aede3396 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideMetadataTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideMetadataTests.java @@ -62,9 +62,16 @@ void isEqualToWithSameMetadata() { } @Test - void isEqualToWithSameMetadataButDifferentField() { + void isNotEqualToByTypeLookupWithSameMetadataButDifferentField() { MockitoSpyBeanOverrideMetadata metadata = createMetadata(sampleField("service")); MockitoSpyBeanOverrideMetadata metadata2 = createMetadata(sampleField("service2")); + assertThat(metadata).isNotEqualTo(metadata2); + } + + @Test + void isEqualToByNameLookupWithSameMetadataButDifferentField() { + MockitoSpyBeanOverrideMetadata metadata = createMetadata(sampleField("service3")); + MockitoSpyBeanOverrideMetadata metadata2 = createMetadata(sampleField("service4")); assertThat(metadata).isEqualTo(metadata2); assertThat(metadata).hasSameHashCodeAs(metadata2); } @@ -79,14 +86,14 @@ void isNotEqualToWithSameMetadataButDifferentBeanName() { @Test void isNotEqualToWithSameMetadataButDifferentReset() { MockitoSpyBeanOverrideMetadata metadata = createMetadata(sampleField("service")); - MockitoSpyBeanOverrideMetadata metadata2 = createMetadata(sampleField("service4")); + MockitoSpyBeanOverrideMetadata metadata2 = createMetadata(sampleField("service5")); assertThat(metadata).isNotEqualTo(metadata2); } @Test void isNotEqualToWithSameMetadataButDifferentProxyTargetAwareFlag() { MockitoSpyBeanOverrideMetadata metadata = createMetadata(sampleField("service")); - MockitoSpyBeanOverrideMetadata metadata2 = createMetadata(sampleField("service5")); + MockitoSpyBeanOverrideMetadata metadata2 = createMetadata(sampleField("service6")); assertThat(metadata).isNotEqualTo(metadata2); } @@ -128,12 +135,15 @@ static class Sample { @MockitoSpyBean(name = "beanToMock") private String service3; - @MockitoSpyBean(reset = MockReset.BEFORE) + @MockitoSpyBean(name = "beanToMock") private String service4; - @MockitoSpyBean(proxyTargetAware = false) + @MockitoSpyBean(reset = MockReset.BEFORE) private String service5; + @MockitoSpyBean(proxyTargetAware = false) + private String service6; + } }