Skip to content

Commit 374c3b4

Browse files
committed
Provide complete support for qualifier annotations with Bean Overrides
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
1 parent d7e470d commit 374c3b4

File tree

5 files changed

+446
-1
lines changed

5 files changed

+446
-1
lines changed

spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java

+18-1
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ private void replaceOrCreateBean(ConfigurableListableBeanFactory beanFactory, Be
152152
// an existing bean definition.
153153
if (beanFactory.containsBeanDefinition(beanName)) {
154154
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
155+
setQualifiedElement(existingBeanDefinition, handler);
155156
}
156157
}
157158
else {
@@ -166,6 +167,7 @@ private void replaceOrCreateBean(ConfigurableListableBeanFactory beanFactory, Be
166167
if (candidates.contains(beanName)) {
167168
// 3) We are overriding an existing bean by-name.
168169
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
170+
setQualifiedElement(existingBeanDefinition, handler);
169171
}
170172
else if (requireExistingBean) {
171173
Field field = handler.getField();
@@ -450,10 +452,25 @@ private static String determinePrimaryCandidate(ConfigurableListableBeanFactory
450452
private static RootBeanDefinition createPseudoBeanDefinition(BeanOverrideHandler handler) {
451453
RootBeanDefinition definition = new RootBeanDefinition(handler.getBeanType().resolve());
452454
definition.setTargetType(handler.getBeanType());
453-
definition.setQualifiedElement(handler.getField());
455+
setQualifiedElement(definition, handler);
454456
return definition;
455457
}
456458

459+
/**
460+
* Set the {@linkplain RootBeanDefinition#setQualifiedElement(java.lang.reflect.AnnotatedElement)
461+
* qualified element} in the supplied {@link BeanDefinition} to the
462+
* {@linkplain BeanOverrideHandler#getField() field} of the supplied
463+
* {@code BeanOverrideHandler}.
464+
* <p>This is necessary for proper autowiring candidate resolution.
465+
* @since 6.2.6
466+
*/
467+
private static void setQualifiedElement(BeanDefinition beanDefinition, BeanOverrideHandler handler) {
468+
Field field = handler.getField();
469+
if (field != null && beanDefinition instanceof RootBeanDefinition rbd) {
470+
rbd.setQualifiedElement(field);
471+
}
472+
}
473+
457474
/**
458475
* Validate that the {@link BeanDefinition} for the supplied bean name is suitable
459476
* for being replaced by a bean override.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.bean.override.mockito.integration;
18+
19+
import java.lang.annotation.Retention;
20+
import java.lang.annotation.RetentionPolicy;
21+
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.api.extension.ExtendWith;
24+
25+
import org.springframework.beans.factory.annotation.Autowired;
26+
import org.springframework.beans.factory.annotation.Qualifier;
27+
import org.springframework.context.ApplicationContext;
28+
import org.springframework.context.annotation.Bean;
29+
import org.springframework.context.annotation.Configuration;
30+
import org.springframework.test.context.bean.override.example.ExampleService;
31+
import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
32+
import org.springframework.test.context.bean.override.mockito.MockitoBean;
33+
import org.springframework.test.context.junit.jupiter.SpringExtension;
34+
35+
import static org.assertj.core.api.Assertions.assertThat;
36+
import static org.mockito.Mockito.when;
37+
import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
38+
import static org.springframework.test.mockito.MockitoAssertions.assertMockName;
39+
40+
/**
41+
* Tests for {@link MockitoBean @MockitoBean} where the mocked bean is associated
42+
* with a custom {@link Qualifier @Qualifier} annotation and the bean to override
43+
* is selected by name.
44+
*
45+
* @author Sam Brannen
46+
* @since 6.2.6
47+
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34646">gh-34646</a>
48+
* @see MockitoBeanWithCustomQualifierAnnotationByTypeTests
49+
*/
50+
@ExtendWith(SpringExtension.class)
51+
class MockitoBeanWithCustomQualifierAnnotationByNameTests {
52+
53+
@MockitoBean(name = "qualifiedService", enforceOverride = true)
54+
@MyQualifier
55+
ExampleService service;
56+
57+
@Autowired
58+
ExampleServiceCaller caller;
59+
60+
61+
@Test
62+
void test(ApplicationContext context) {
63+
assertIsMock(service);
64+
assertMockName(service, "qualifiedService");
65+
assertThat(service).isNotInstanceOf(QualifiedService.class);
66+
67+
// Since the 'service' field's type is ExampleService, the QualifiedService
68+
// bean in the @Configuration class effectively gets removed from the context,
69+
// or rather it never gets created because we register an ExampleService as
70+
// a manual singleton in its place.
71+
assertThat(context.getBeanNamesForType(QualifiedService.class)).isEmpty();
72+
assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1);
73+
assertThat(context.getBeanNamesForType(ExampleServiceCaller.class)).hasSize(1);
74+
75+
when(service.greeting()).thenReturn("mock!");
76+
assertThat(caller.sayGreeting()).isEqualTo("I say mock!");
77+
}
78+
79+
80+
@Configuration(proxyBeanMethods = false)
81+
static class Config {
82+
83+
@Bean
84+
QualifiedService qualifiedService() {
85+
return new QualifiedService();
86+
}
87+
88+
@Bean
89+
ExampleServiceCaller myServiceCaller(@MyQualifier ExampleService service) {
90+
return new ExampleServiceCaller(service);
91+
}
92+
}
93+
94+
@Qualifier
95+
@Retention(RetentionPolicy.RUNTIME)
96+
@interface MyQualifier {
97+
}
98+
99+
@MyQualifier
100+
static class QualifiedService implements ExampleService {
101+
102+
@Override
103+
public String greeting() {
104+
return "Qualified service";
105+
}
106+
}
107+
108+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.bean.override.mockito.integration;
18+
19+
import java.lang.annotation.Retention;
20+
import java.lang.annotation.RetentionPolicy;
21+
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.api.extension.ExtendWith;
24+
25+
import org.springframework.beans.factory.annotation.Autowired;
26+
import org.springframework.beans.factory.annotation.Qualifier;
27+
import org.springframework.context.ApplicationContext;
28+
import org.springframework.context.annotation.Bean;
29+
import org.springframework.context.annotation.Configuration;
30+
import org.springframework.test.context.bean.override.example.ExampleService;
31+
import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
32+
import org.springframework.test.context.bean.override.mockito.MockitoBean;
33+
import org.springframework.test.context.junit.jupiter.SpringExtension;
34+
35+
import static org.assertj.core.api.Assertions.assertThat;
36+
import static org.mockito.Mockito.when;
37+
import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
38+
import static org.springframework.test.mockito.MockitoAssertions.assertMockName;
39+
40+
/**
41+
* Tests for {@link MockitoBean @MockitoBean} where the mocked bean is associated
42+
* with a custom {@link Qualifier @Qualifier} annotation and the bean to override
43+
* is selected by type.
44+
*
45+
* @author Sam Brannen
46+
* @since 6.2.6
47+
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34646">gh-34646</a>
48+
* @see MockitoBeanWithCustomQualifierAnnotationByNameTests
49+
*/
50+
@ExtendWith(SpringExtension.class)
51+
class MockitoBeanWithCustomQualifierAnnotationByTypeTests {
52+
53+
@MockitoBean(enforceOverride = true)
54+
@MyQualifier
55+
ExampleService service;
56+
57+
@Autowired
58+
ExampleServiceCaller caller;
59+
60+
61+
@Test
62+
void test(ApplicationContext context) {
63+
assertIsMock(service);
64+
assertMockName(service, "qualifiedService");
65+
assertThat(service).isNotInstanceOf(QualifiedService.class);
66+
67+
// Since the 'service' field's type is ExampleService, the QualifiedService
68+
// bean in the @Configuration class effectively gets removed from the context,
69+
// or rather it never gets created because we register an ExampleService as
70+
// a manual singleton in its place.
71+
assertThat(context.getBeanNamesForType(QualifiedService.class)).isEmpty();
72+
assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1);
73+
assertThat(context.getBeanNamesForType(ExampleServiceCaller.class)).hasSize(1);
74+
75+
when(service.greeting()).thenReturn("mock!");
76+
assertThat(caller.sayGreeting()).isEqualTo("I say mock!");
77+
}
78+
79+
80+
@Configuration(proxyBeanMethods = false)
81+
static class Config {
82+
83+
@Bean
84+
QualifiedService qualifiedService() {
85+
return new QualifiedService();
86+
}
87+
88+
@Bean
89+
ExampleServiceCaller myServiceCaller(@MyQualifier ExampleService service) {
90+
return new ExampleServiceCaller(service);
91+
}
92+
}
93+
94+
@Qualifier
95+
@Retention(RetentionPolicy.RUNTIME)
96+
@interface MyQualifier {
97+
}
98+
99+
@MyQualifier
100+
static class QualifiedService implements ExampleService {
101+
102+
@Override
103+
public String greeting() {
104+
return "Qualified service";
105+
}
106+
}
107+
108+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.bean.override.mockito.integration;
18+
19+
import java.lang.annotation.Retention;
20+
import java.lang.annotation.RetentionPolicy;
21+
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.api.extension.ExtendWith;
24+
25+
import org.springframework.beans.factory.annotation.Autowired;
26+
import org.springframework.beans.factory.annotation.Qualifier;
27+
import org.springframework.context.ApplicationContext;
28+
import org.springframework.context.annotation.Bean;
29+
import org.springframework.context.annotation.Configuration;
30+
import org.springframework.test.context.bean.override.example.ExampleService;
31+
import org.springframework.test.context.bean.override.example.ExampleServiceCaller;
32+
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
33+
import org.springframework.test.context.junit.jupiter.SpringExtension;
34+
35+
import static org.assertj.core.api.Assertions.assertThat;
36+
import static org.mockito.Mockito.verify;
37+
import static org.mockito.Mockito.when;
38+
import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy;
39+
import static org.springframework.test.mockito.MockitoAssertions.assertMockName;
40+
41+
/**
42+
* Tests for {@link MockitoSpyBean @MockitoSpyBean} where the mocked bean is associated
43+
* with a custom {@link Qualifier @Qualifier} annotation and the bean to override
44+
* is selected by name.
45+
*
46+
* @author Sam Brannen
47+
* @since 6.2.6
48+
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34646">gh-34646</a>
49+
* @see MockitoSpyBeanWithCustomQualifierAnnotationByTypeTests
50+
*/
51+
@ExtendWith(SpringExtension.class)
52+
class MockitoSpyBeanWithCustomQualifierAnnotationByNameTests {
53+
54+
@MockitoSpyBean(name = "qualifiedService")
55+
@MyQualifier
56+
ExampleService service;
57+
58+
@Autowired
59+
ExampleServiceCaller caller;
60+
61+
62+
@Test
63+
void test(ApplicationContext context) {
64+
assertIsSpy(service);
65+
assertMockName(service, "qualifiedService");
66+
assertThat(service).isInstanceOf(QualifiedService.class);
67+
68+
assertThat(context.getBeanNamesForType(QualifiedService.class)).hasSize(1);
69+
assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1);
70+
assertThat(context.getBeanNamesForType(ExampleServiceCaller.class)).hasSize(1);
71+
72+
when(service.greeting()).thenReturn("mock!");
73+
assertThat(caller.sayGreeting()).isEqualTo("I say mock!");
74+
verify(service).greeting();
75+
}
76+
77+
78+
@Configuration(proxyBeanMethods = false)
79+
static class Config {
80+
81+
@Bean
82+
QualifiedService qualifiedService() {
83+
return new QualifiedService();
84+
}
85+
86+
@Bean
87+
ExampleServiceCaller myServiceCaller(@MyQualifier ExampleService service) {
88+
return new ExampleServiceCaller(service);
89+
}
90+
}
91+
92+
@Qualifier
93+
@Retention(RetentionPolicy.RUNTIME)
94+
@interface MyQualifier {
95+
}
96+
97+
@MyQualifier
98+
static class QualifiedService implements ExampleService {
99+
100+
@Override
101+
public String greeting() {
102+
return "Qualified service";
103+
}
104+
}
105+
106+
}

0 commit comments

Comments
 (0)