Skip to content

Commit ef4f1f0

Browse files
committed
Ensure @⁠BeanOverride in subclass takes precedence over superclass
Prior to this commit, a @⁠BeanOverride (such as @⁠TestBean) for a specific target bean which was declared in a superclass always took precedence over a bean override for the same target bean in a subclass, thereby rendering the bean override configuration in the subclass useless. In other words, there was no way for a test class to override a bean override declared in a superclass. To address that, this commit switches from direct use of ReflectionUtils.doWithFields() to a custom search algorithm that traverses the class hierarchy using tail recursion for processing @⁠BeanOverride fields (delegating now to ReflectionUtils.doWithLocalFields() in order to continue to benefit from the caching of declared fields in ReflectionUtils). Closes gh-34194
1 parent 51b8974 commit ef4f1f0

File tree

2 files changed

+103
-36
lines changed

2 files changed

+103
-36
lines changed

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

+19-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -103,10 +103,27 @@ protected BeanOverrideHandler(Field field, ResolvableType beanType, @Nullable St
103103
*/
104104
public static List<BeanOverrideHandler> forTestClass(Class<?> testClass) {
105105
List<BeanOverrideHandler> handlers = new LinkedList<>();
106-
ReflectionUtils.doWithFields(testClass, field -> processField(field, testClass, handlers));
106+
findHandlers(testClass, testClass, handlers);
107107
return handlers;
108108
}
109109

110+
/**
111+
* Find handlers using tail recursion to ensure that "locally declared"
112+
* bean overrides take precedence over inherited bean overrides.
113+
* @since 6.2.2
114+
*/
115+
private static void findHandlers(Class<?> clazz, Class<?> testClass, List<BeanOverrideHandler> handlers) {
116+
if (clazz == null || clazz == Object.class) {
117+
return;
118+
}
119+
120+
// 1) Search type hierarchy.
121+
findHandlers(clazz.getSuperclass(), testClass, handlers);
122+
123+
// 2) Process fields in current class.
124+
ReflectionUtils.doWithLocalFields(clazz, field -> processField(field, testClass, handlers));
125+
}
126+
110127
private static void processField(Field field, Class<?> testClass, List<BeanOverrideHandler> handlers) {
111128
AtomicBoolean overrideAnnotationFound = new AtomicBoolean();
112129
MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> {
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,21 @@
3838
* @author Sam Brannen
3939
* @since 6.2
4040
*/
41-
public class TestBeanForInheritanceIntegrationTests {
41+
@SpringJUnitConfig
42+
public class TestBeanInheritanceIntegrationTests {
43+
44+
@TestBean
45+
Pojo puzzleBean;
46+
47+
static Pojo puzzleBean() {
48+
return new FakePojo("puzzle in enclosing class");
49+
}
4250

4351
static Pojo enclosingClassBean() {
4452
return new FakePojo("in enclosing test class");
4553
}
4654

47-
@SpringJUnitConfig
48-
abstract static class AbstractTestBeanIntegrationTestCase {
55+
abstract static class AbstractTestCase {
4956

5057
@TestBean
5158
Pojo someBean;
@@ -56,6 +63,9 @@ abstract static class AbstractTestBeanIntegrationTestCase {
5663
@TestBean("thirdBean")
5764
Pojo anotherBean;
5865

66+
@TestBean
67+
Pojo enigmaBean;
68+
5969
static Pojo otherBean() {
6070
return new FakePojo("other in superclass");
6171
}
@@ -64,44 +74,18 @@ static Pojo thirdBean() {
6474
return new FakePojo("third in superclass");
6575
}
6676

67-
static Pojo commonBean() {
68-
return new FakePojo("common in superclass");
77+
static Pojo enigmaBean() {
78+
return new FakePojo("enigma in superclass");
6979
}
7080

71-
@Configuration(proxyBeanMethods = false)
72-
static class Config {
73-
74-
@Bean
75-
Pojo someBean() {
76-
return new ProdPojo();
77-
}
78-
79-
@Bean
80-
Pojo otherBean() {
81-
return new ProdPojo();
82-
}
83-
84-
@Bean
85-
Pojo thirdBean() {
86-
return new ProdPojo();
87-
}
88-
89-
@Bean
90-
Pojo pojo() {
91-
return new ProdPojo();
92-
}
93-
94-
@Bean
95-
Pojo pojo2() {
96-
return new ProdPojo();
97-
}
81+
static Pojo commonBean() {
82+
return new FakePojo("common in superclass");
9883
}
99-
10084
}
10185

10286
@Nested
10387
@DisplayName("Nested, concrete inherited tests with correct @TestBean setup")
104-
class NestedConcreteTestBeanIntegrationTests extends AbstractTestBeanIntegrationTestCase {
88+
class NestedTests extends AbstractTestCase {
10589

10690
@Autowired
10791
ApplicationContext ctx;
@@ -112,6 +96,21 @@ class NestedConcreteTestBeanIntegrationTests extends AbstractTestBeanIntegration
11296
@TestBean(name = "pojo2", methodName = "enclosingClassBean")
11397
Pojo pojo2;
11498

99+
@TestBean(methodName = "localEnigmaBean")
100+
Pojo enigmaBean;
101+
102+
@TestBean
103+
Pojo puzzleBean;
104+
105+
106+
static Pojo puzzleBean() {
107+
return new FakePojo("puzzle in nested class");
108+
}
109+
110+
static Pojo localEnigmaBean() {
111+
return new FakePojo("enigma in subclass");
112+
}
113+
115114
static Pojo someBean() {
116115
return new FakePojo("someBeanOverride");
117116
}
@@ -150,6 +149,57 @@ void fieldInNestedClassWithFactoryMethodInEnclosingClass() {
150149
assertThat(ctx.getBean("pojo2")).as("applicationContext").hasToString("in enclosing test class");
151150
assertThat(this.pojo2.value()).as("injection point").isEqualTo("in enclosing test class");
152151
}
152+
153+
@Test // gh-34194
154+
void testBeanInSubclassOverridesTestBeanInSuperclass() {
155+
assertThat(ctx.getBean("enigmaBean")).as("applicationContext").hasToString("enigma in subclass");
156+
assertThat(this.enigmaBean.value()).as("injection point").isEqualTo("enigma in subclass");
157+
}
158+
159+
@Test // gh-34194
160+
void testBeanInNestedClassOverridesTestBeanInEnclosingClass() {
161+
assertThat(ctx.getBean("puzzleBean")).as("applicationContext").hasToString("puzzle in nested class");
162+
assertThat(this.puzzleBean.value()).as("injection point").isEqualTo("puzzle in nested class");
163+
}
164+
}
165+
166+
@Configuration(proxyBeanMethods = false)
167+
static class Config {
168+
169+
@Bean
170+
Pojo someBean() {
171+
return new ProdPojo();
172+
}
173+
174+
@Bean
175+
Pojo otherBean() {
176+
return new ProdPojo();
177+
}
178+
179+
@Bean
180+
Pojo thirdBean() {
181+
return new ProdPojo();
182+
}
183+
184+
@Bean
185+
Pojo enigmaBean() {
186+
return new ProdPojo();
187+
}
188+
189+
@Bean
190+
Pojo puzzleBean() {
191+
return new ProdPojo();
192+
}
193+
194+
@Bean
195+
Pojo pojo() {
196+
return new ProdPojo();
197+
}
198+
199+
@Bean
200+
Pojo pojo2() {
201+
return new ProdPojo();
202+
}
153203
}
154204

155205
interface Pojo {

0 commit comments

Comments
 (0)