From 374c3b4545a65d0a7f616ff2ff1a7a54516d294d Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 27 Mar 2025 15:29:14 +0100 Subject: [PATCH] Provide complete support for qualifier annotations with Bean Overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prior to this commit, the Test Bean Override feature provided support for overriding beans based on qualifier annotations in several scenarios; however, qualifier annotations got lost if they were declared on the return type of the @⁠Bean method for the bean being overridden and the @⁠BeanOverride (such as @⁠MockitoBean) was based on a supertype of that return type. To address that, this commit sets the @⁠BeanOverride field as the "qualified element" in the RootBeanDefinition to ensure that qualifier annotations are available for subsequent autowiring candidate resolution. Closes gh-34646 --- .../BeanOverrideBeanFactoryPostProcessor.java | 19 ++- ...hCustomQualifierAnnotationByNameTests.java | 108 ++++++++++++++++++ ...hCustomQualifierAnnotationByTypeTests.java | 108 ++++++++++++++++++ ...hCustomQualifierAnnotationByNameTests.java | 106 +++++++++++++++++ ...hCustomQualifierAnnotationByTypeTests.java | 106 +++++++++++++++++ 5 files changed, 446 insertions(+), 1 deletion(-) create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByNameTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByTypeTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByNameTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByTypeTests.java 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 11d282f405..19f2aeeb6e 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 @@ -152,6 +152,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, // an existing bean definition. if (beanFactory.containsBeanDefinition(beanName)) { existingBeanDefinition = beanFactory.getBeanDefinition(beanName); + setQualifiedElement(existingBeanDefinition, handler); } } else { @@ -166,6 +167,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, if (candidates.contains(beanName)) { // 3) We are overriding an existing bean by-name. existingBeanDefinition = beanFactory.getBeanDefinition(beanName); + setQualifiedElement(existingBeanDefinition, handler); } else if (requireExistingBean) { Field field = handler.getField(); @@ -450,10 +452,25 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, private static RootBeanDefinition createPseudoBeanDefinition(BeanOverrideHandler handler) { RootBeanDefinition definition = new RootBeanDefinition(handler.getBeanType().resolve()); definition.setTargetType(handler.getBeanType()); - definition.setQualifiedElement(handler.getField()); + setQualifiedElement(definition, handler); return definition; } + /** + * Set the {@linkplain RootBeanDefinition#setQualifiedElement(java.lang.reflect.AnnotatedElement) + * qualified element} in the supplied {@link BeanDefinition} to the + * {@linkplain BeanOverrideHandler#getField() field} of the supplied + * {@code BeanOverrideHandler}. + *

This is necessary for proper autowiring candidate resolution. + * @since 6.2.6 + */ + private static void setQualifiedElement(BeanDefinition beanDefinition, BeanOverrideHandler handler) { + Field field = handler.getField(); + if (field != null && beanDefinition instanceof RootBeanDefinition rbd) { + rbd.setQualifiedElement(field); + } + } + /** * Validate that the {@link BeanDefinition} for the supplied bean name is suitable * for being replaced by a bean override. diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByNameTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByNameTests.java new file mode 100644 index 0000000000..63e9b6ee07 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByNameTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2025 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.integration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +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.ExampleServiceCaller; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; +import static org.springframework.test.mockito.MockitoAssertions.assertMockName; + +/** + * Tests for {@link MockitoBean @MockitoBean} where the mocked bean is associated + * with a custom {@link Qualifier @Qualifier} annotation and the bean to override + * is selected by name. + * + * @author Sam Brannen + * @since 6.2.6 + * @see gh-34646 + * @see MockitoBeanWithCustomQualifierAnnotationByTypeTests + */ +@ExtendWith(SpringExtension.class) +class MockitoBeanWithCustomQualifierAnnotationByNameTests { + + @MockitoBean(name = "qualifiedService", enforceOverride = true) + @MyQualifier + ExampleService service; + + @Autowired + ExampleServiceCaller caller; + + + @Test + void test(ApplicationContext context) { + assertIsMock(service); + assertMockName(service, "qualifiedService"); + assertThat(service).isNotInstanceOf(QualifiedService.class); + + // Since the 'service' field's type is ExampleService, the QualifiedService + // bean in the @Configuration class effectively gets removed from the context, + // or rather it never gets created because we register an ExampleService as + // a manual singleton in its place. + assertThat(context.getBeanNamesForType(QualifiedService.class)).isEmpty(); + assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1); + assertThat(context.getBeanNamesForType(ExampleServiceCaller.class)).hasSize(1); + + when(service.greeting()).thenReturn("mock!"); + assertThat(caller.sayGreeting()).isEqualTo("I say mock!"); + } + + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + QualifiedService qualifiedService() { + return new QualifiedService(); + } + + @Bean + ExampleServiceCaller myServiceCaller(@MyQualifier ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Qualifier + @Retention(RetentionPolicy.RUNTIME) + @interface MyQualifier { + } + + @MyQualifier + static class QualifiedService implements ExampleService { + + @Override + public String greeting() { + return "Qualified service"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByTypeTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByTypeTests.java new file mode 100644 index 0000000000..acf1fd66f4 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByTypeTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2025 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.integration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +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.ExampleServiceCaller; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; +import static org.springframework.test.mockito.MockitoAssertions.assertMockName; + +/** + * Tests for {@link MockitoBean @MockitoBean} where the mocked bean is associated + * with a custom {@link Qualifier @Qualifier} annotation and the bean to override + * is selected by type. + * + * @author Sam Brannen + * @since 6.2.6 + * @see gh-34646 + * @see MockitoBeanWithCustomQualifierAnnotationByNameTests + */ +@ExtendWith(SpringExtension.class) +class MockitoBeanWithCustomQualifierAnnotationByTypeTests { + + @MockitoBean(enforceOverride = true) + @MyQualifier + ExampleService service; + + @Autowired + ExampleServiceCaller caller; + + + @Test + void test(ApplicationContext context) { + assertIsMock(service); + assertMockName(service, "qualifiedService"); + assertThat(service).isNotInstanceOf(QualifiedService.class); + + // Since the 'service' field's type is ExampleService, the QualifiedService + // bean in the @Configuration class effectively gets removed from the context, + // or rather it never gets created because we register an ExampleService as + // a manual singleton in its place. + assertThat(context.getBeanNamesForType(QualifiedService.class)).isEmpty(); + assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1); + assertThat(context.getBeanNamesForType(ExampleServiceCaller.class)).hasSize(1); + + when(service.greeting()).thenReturn("mock!"); + assertThat(caller.sayGreeting()).isEqualTo("I say mock!"); + } + + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + QualifiedService qualifiedService() { + return new QualifiedService(); + } + + @Bean + ExampleServiceCaller myServiceCaller(@MyQualifier ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Qualifier + @Retention(RetentionPolicy.RUNTIME) + @interface MyQualifier { + } + + @MyQualifier + static class QualifiedService implements ExampleService { + + @Override + public String greeting() { + return "Qualified service"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByNameTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByNameTests.java new file mode 100644 index 0000000000..f3d1fc1c37 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByNameTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2025 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.integration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +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.ExampleServiceCaller; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; +import static org.springframework.test.mockito.MockitoAssertions.assertMockName; + +/** + * Tests for {@link MockitoSpyBean @MockitoSpyBean} where the mocked bean is associated + * with a custom {@link Qualifier @Qualifier} annotation and the bean to override + * is selected by name. + * + * @author Sam Brannen + * @since 6.2.6 + * @see gh-34646 + * @see MockitoSpyBeanWithCustomQualifierAnnotationByTypeTests + */ +@ExtendWith(SpringExtension.class) +class MockitoSpyBeanWithCustomQualifierAnnotationByNameTests { + + @MockitoSpyBean(name = "qualifiedService") + @MyQualifier + ExampleService service; + + @Autowired + ExampleServiceCaller caller; + + + @Test + void test(ApplicationContext context) { + assertIsSpy(service); + assertMockName(service, "qualifiedService"); + assertThat(service).isInstanceOf(QualifiedService.class); + + assertThat(context.getBeanNamesForType(QualifiedService.class)).hasSize(1); + assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1); + assertThat(context.getBeanNamesForType(ExampleServiceCaller.class)).hasSize(1); + + when(service.greeting()).thenReturn("mock!"); + assertThat(caller.sayGreeting()).isEqualTo("I say mock!"); + verify(service).greeting(); + } + + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + QualifiedService qualifiedService() { + return new QualifiedService(); + } + + @Bean + ExampleServiceCaller myServiceCaller(@MyQualifier ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Qualifier + @Retention(RetentionPolicy.RUNTIME) + @interface MyQualifier { + } + + @MyQualifier + static class QualifiedService implements ExampleService { + + @Override + public String greeting() { + return "Qualified service"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByTypeTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByTypeTests.java new file mode 100644 index 0000000000..197eedc5b0 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByTypeTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2025 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.integration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +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.ExampleServiceCaller; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; +import static org.springframework.test.mockito.MockitoAssertions.assertMockName; + +/** + * Tests for {@link MockitoSpyBean @MockitoSpyBean} where the mocked bean is associated + * with a custom {@link Qualifier @Qualifier} annotation and the bean to override + * is selected by name. + * + * @author Sam Brannen + * @since 6.2.6 + * @see gh-34646 + * @see MockitoSpyBeanWithCustomQualifierAnnotationByNameTests + */ +@ExtendWith(SpringExtension.class) +class MockitoSpyBeanWithCustomQualifierAnnotationByTypeTests { + + @MockitoSpyBean + @MyQualifier + ExampleService service; + + @Autowired + ExampleServiceCaller caller; + + + @Test + void test(ApplicationContext context) { + assertIsSpy(service); + assertMockName(service, "qualifiedService"); + assertThat(service).isInstanceOf(QualifiedService.class); + + assertThat(context.getBeanNamesForType(QualifiedService.class)).hasSize(1); + assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1); + assertThat(context.getBeanNamesForType(ExampleServiceCaller.class)).hasSize(1); + + when(service.greeting()).thenReturn("mock!"); + assertThat(caller.sayGreeting()).isEqualTo("I say mock!"); + verify(service).greeting(); + } + + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + QualifiedService qualifiedService() { + return new QualifiedService(); + } + + @Bean + ExampleServiceCaller myServiceCaller(@MyQualifier ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Qualifier + @Retention(RetentionPolicy.RUNTIME) + @interface MyQualifier { + } + + @MyQualifier + static class QualifiedService implements ExampleService { + + @Override + public String greeting() { + return "Qualified service"; + } + } + +}