Skip to content

Commit cd2fbb1

Browse files
committed
Properly resolve @⁠TestBean factory method within class hierarchy
Prior to this commit, the search algorithm used to locate a @⁠TestBean factory method within a test class hierarchy incorrectly found factory methods declared in subclasses or nested test classes "below" the class in which the @⁠TestBean field was declared. This resulted in "duplicate bean override" failures for @⁠TestBean overrides which are clearly not duplicates but rather "overrides of an override". This commit ensures that @⁠TestBean factory method resolution is consistent in type hierarchies as well as in enclosing class hierarchies (for @⁠Nested test classes) by beginning the search for a factory method in the class which declares the @⁠TestBean field. Closes gh-34204
1 parent e874552 commit cd2fbb1

File tree

5 files changed

+58
-69
lines changed

5 files changed

+58
-69
lines changed

framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc

+4-2
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,10 @@ Java::
9292

9393
[TIP]
9494
====
95-
Spring searches for the factory method to invoke in the test class, in the test class
96-
hierarchy, and in the enclosing class hierarchy for a `@Nested` test class.
95+
To locate the factory method to invoke, Spring searches in the class in which the
96+
`@TestBean` field is declared, in one of its superclasses, or in any implemented
97+
interfaces. If the `@TestBean` field is declared in a `@Nested` test class, the enclosing
98+
class hierarchy will also be searched.
9799
98100
Alternatively, a factory method in an external class can be referenced via its
99101
fully-qualified method name following the syntax `<fully-qualified class name>#<method name>`

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

+19-17
Original file line numberDiff line numberDiff line change
@@ -44,24 +44,24 @@
4444
* you can set the {@link #enforceOverride() enforceOverride} attribute to {@code true}
4545
* &mdash; for example, {@code @TestBean(enforceOverride = true)}.
4646
*
47-
* <p>The instance is created from a zero-argument static factory method in the
48-
* test class whose return type is compatible with the annotated field. In the
49-
* case of a nested test class, the enclosing class hierarchy is also searched.
50-
* Similarly, if the test class extends from a base class or implements any
51-
* interfaces, the entire type hierarchy is searched. Alternatively, a factory
52-
* method in an external class can be referenced via its fully-qualified method
53-
* name following the syntax {@code <fully-qualified class name>#<method name>}
54-
* &mdash; for example,
47+
* <p>The instance is created from a zero-argument static factory method whose
48+
* return type is compatible with the annotated field. The factory method can be
49+
* declared directly in the class which declares the {@code @TestBean} field or
50+
* within the type hierarchy above that class, including implemented interfaces.
51+
* If the {@code @TestBean} field is declared in a nested test class, the enclosing
52+
* class hierarchy is also searched. Alternatively, a factory method in an external
53+
* class can be referenced via its fully-qualified method name following the syntax
54+
* {@code <fully-qualified class name>#<method name>} &mdash; for example,
5555
* {@code @TestBean(methodName = "org.example.TestUtils#createCustomerRepository")}.
5656
*
5757
* <p>The factory method is deduced as follows.
5858
*
5959
* <ul>
60-
* <li>If the {@link #methodName()} is specified, look for a static method with
61-
* that name.</li>
62-
* <li>If a method name is not specified, look for exactly one static method
63-
* named with either the name of the annotated field or the name of the bean
64-
* (if specified).</li>
60+
* <li>If the {@link #methodName methodName} is specified, Spring looks for a static
61+
* method with that name.</li>
62+
* <li>If a method name is not specified, Spring looks for exactly one static method
63+
* whose name is either the name of the annotated field or the {@link #name() name}
64+
* of the bean (if specified).</li>
6565
* </ul>
6666
*
6767
* <p>Consider the following example.
@@ -146,15 +146,17 @@
146146
/**
147147
* Name of the static factory method that will be used to instantiate the bean
148148
* to override.
149-
* <p>A search will be performed to find the factory method in the test class,
150-
* in one of its superclasses, or in any implemented interfaces. In the case
151-
* of a nested test class, the enclosing class hierarchy will also be searched.
149+
* <p>A search will be performed to find the factory method in the class in
150+
* which the {@code @TestBean} field is declared, in one of its superclasses,
151+
* or in any implemented interfaces. If the {@code @TestBean} field is declared
152+
* in a nested test class, the enclosing class hierarchy will also be searched.
152153
* <p>Alternatively, a factory method in an external class can be referenced
153154
* via its fully-qualified method name following the syntax
154155
* {@code <fully-qualified class name>#<method name>} &mdash; for example,
155156
* {@code @TestBean(methodName = "org.example.TestUtils#createCustomerRepository")}.
156157
* <p>If left unspecified, the name of the factory method will be detected
157-
* based either on the name of the annotated field or the name of the bean.
158+
* based either on the name of the {@code @TestBean} field or the {@link #name() name}
159+
* of the bean.
158160
*/
159161
String methodName() default "";
160162

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

+3-3
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.
@@ -67,7 +67,7 @@ public TestBeanOverrideHandler createHandler(Annotation overrideAnnotation, Clas
6767
Method factoryMethod;
6868
if (!methodName.isBlank()) {
6969
// If the user specified an explicit method name, search for that.
70-
factoryMethod = findTestBeanFactoryMethod(testClass, field.getType(), methodName);
70+
factoryMethod = findTestBeanFactoryMethod(field.getDeclaringClass(), field.getType(), methodName);
7171
}
7272
else {
7373
// Otherwise, search for candidate factory methods whose names match either
@@ -78,7 +78,7 @@ public TestBeanOverrideHandler createHandler(Annotation overrideAnnotation, Clas
7878
if (beanName != null) {
7979
candidateMethodNames.add(beanName);
8080
}
81-
factoryMethod = findTestBeanFactoryMethod(testClass, field.getType(), candidateMethodNames);
81+
factoryMethod = findTestBeanFactoryMethod(field.getDeclaringClass(), field.getType(), candidateMethodNames);
8282
}
8383

8484
return new TestBeanOverrideHandler(

spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanInheritanceIntegrationTests.java

+20-35
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,16 @@ static Pojo puzzleBean() {
4848
return new FakePojo("puzzle in enclosing class");
4949
}
5050

51-
static Pojo enclosingClassBean() {
51+
static Pojo enclosingClassFactoryMethod() {
5252
return new FakePojo("in enclosing test class");
5353
}
5454

5555
abstract static class AbstractTestCase {
5656

57-
@TestBean
58-
Pojo someBean;
59-
6057
@TestBean("otherBean")
6158
Pojo otherBean;
6259

63-
@TestBean("thirdBean")
60+
@TestBean
6461
Pojo anotherBean;
6562

6663
@TestBean
@@ -70,8 +67,8 @@ static Pojo otherBean() {
7067
return new FakePojo("other in superclass");
7168
}
7269

73-
static Pojo thirdBean() {
74-
return new FakePojo("third in superclass");
70+
static Pojo anotherBean() {
71+
return new FakePojo("another in superclass");
7572
}
7673

7774
static Pojo enigmaBean() {
@@ -93,49 +90,42 @@ class NestedTests extends AbstractTestCase {
9390
@TestBean(methodName = "commonBean")
9491
Pojo pojo;
9592

96-
@TestBean(name = "pojo2", methodName = "enclosingClassBean")
93+
@TestBean(name = "pojo2", methodName = "enclosingClassFactoryMethod")
9794
Pojo pojo2;
9895

99-
@TestBean(methodName = "localEnigmaBean")
96+
@TestBean
10097
Pojo enigmaBean;
10198

10299
@TestBean
103100
Pojo puzzleBean;
104101

105102

103+
// "Overrides" puzzleBean() defined in TestBeanInheritanceIntegrationTests.
106104
static Pojo puzzleBean() {
107105
return new FakePojo("puzzle in nested class");
108106
}
109107

110-
static Pojo localEnigmaBean() {
108+
// "Overrides" enigmaBean() defined in AbstractTestCase.
109+
static Pojo enigmaBean() {
111110
return new FakePojo("enigma in subclass");
112111
}
113112

114-
static Pojo someBean() {
115-
return new FakePojo("someBeanOverride");
116-
}
117-
118-
// "Overrides" otherBean() defined in AbstractTestBeanIntegrationTestCase.
119113
static Pojo otherBean() {
120114
return new FakePojo("other in subclass");
121115
}
122116

123117
@Test
124118
void fieldInSuperclassWithFactoryMethodInSuperclass() {
125-
assertThat(ctx.getBean("thirdBean")).as("applicationContext").hasToString("third in superclass");
126-
assertThat(super.anotherBean.value()).as("injection point").isEqualTo("third in superclass");
119+
assertThat(ctx.getBean("anotherBean")).as("applicationContext").hasToString("another in superclass");
120+
assertThat(super.anotherBean.value()).as("injection point").isEqualTo("another in superclass");
127121
}
128122

129-
@Test
130-
void fieldInSuperclassWithFactoryMethodInSubclass() {
131-
assertThat(ctx.getBean("someBean")).as("applicationContext").hasToString("someBeanOverride");
132-
assertThat(super.someBean.value()).as("injection point").isEqualTo("someBeanOverride");
133-
}
134-
135-
@Test
136-
void fieldInSuperclassWithFactoryMethodInSupeclassAndInSubclass() {
137-
assertThat(ctx.getBean("otherBean")).as("applicationContext").hasToString("other in subclass");
138-
assertThat(super.otherBean.value()).as("injection point").isEqualTo("other in subclass");
123+
@Test // gh-34204
124+
void fieldInSuperclassWithFactoryMethodInSuperclassAndInSubclass() {
125+
// We do not expect "other in subclass", because the @TestBean declaration in
126+
// AbstractTestCase cannot "see" the otherBean() factory method in the subclass.
127+
assertThat(ctx.getBean("otherBean")).as("applicationContext").hasToString("other in superclass");
128+
assertThat(super.otherBean.value()).as("injection point").isEqualTo("other in superclass");
139129
}
140130

141131
@Test
@@ -150,13 +140,13 @@ void fieldInNestedClassWithFactoryMethodInEnclosingClass() {
150140
assertThat(this.pojo2.value()).as("injection point").isEqualTo("in enclosing test class");
151141
}
152142

153-
@Test // gh-34194
143+
@Test // gh-34194, gh-34204
154144
void testBeanInSubclassOverridesTestBeanInSuperclass() {
155145
assertThat(ctx.getBean("enigmaBean")).as("applicationContext").hasToString("enigma in subclass");
156146
assertThat(this.enigmaBean.value()).as("injection point").isEqualTo("enigma in subclass");
157147
}
158148

159-
@Test // gh-34194
149+
@Test // gh-34194, gh-34204
160150
void testBeanInNestedClassOverridesTestBeanInEnclosingClass() {
161151
assertThat(ctx.getBean("puzzleBean")).as("applicationContext").hasToString("puzzle in nested class");
162152
assertThat(this.puzzleBean.value()).as("injection point").isEqualTo("puzzle in nested class");
@@ -166,18 +156,13 @@ void testBeanInNestedClassOverridesTestBeanInEnclosingClass() {
166156
@Configuration(proxyBeanMethods = false)
167157
static class Config {
168158

169-
@Bean
170-
Pojo someBean() {
171-
return new ProdPojo();
172-
}
173-
174159
@Bean
175160
Pojo otherBean() {
176161
return new ProdPojo();
177162
}
178163

179164
@Bean
180-
Pojo thirdBean() {
165+
Pojo anotherBean() {
181166
return new ProdPojo();
182167
}
183168

spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanTests.java

+12-12
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@
2626
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
2727

2828
/**
29-
* Tests for {@link TestBean}.
29+
* Tests for {@link TestBean @TestBean}.
3030
*
3131
* @author Stephane Nicoll
32+
* @author Sam Brannen
3233
*/
3334
public class TestBeanTests {
3435

@@ -109,7 +110,7 @@ void contextCustomizerCannotBeCreatedWithFieldInParentAndMissingOverrideMethod()
109110
.isThrownBy(() -> BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(
110111
FailureOverrideInParentWithoutFactoryMethod.class, context))
111112
.withMessage("No static method found named beanToOverride() in %s with return type %s",
112-
FailureOverrideInParentWithoutFactoryMethod.class.getName(), String.class.getName());
113+
AbstractByNameLookup.class.getName(), String.class.getName());
113114
}
114115

115116
@Test
@@ -149,41 +150,40 @@ static class FailureMissingDefaultOverrideMethod {
149150
@TestBean(name = "beanToOverride")
150151
private String example;
151152

152-
// Expected static String example() { ... }
153-
// or static String beanToOverride() { ... }
153+
// No example() or beanToOverride() method
154154
}
155155

156156
static class FailureMissingExplicitOverrideMethod {
157157

158158
@TestBean(methodName = "createExample")
159159
private String example;
160160

161-
// Expected static String createExample() { ... }
161+
// NO createExample() method
162162
}
163163

164164
abstract static class AbstractByNameLookup {
165165

166-
@TestBean(methodName = "beanToOverride")
167-
protected String beanToOverride;
166+
@TestBean
167+
String beanToOverride;
168+
169+
// No beanToOverride() method
168170
}
169171

170172
static class FailureOverrideInParentWithoutFactoryMethod extends AbstractByNameLookup {
171-
172-
// No beanToOverride() method
173173
}
174174

175175
abstract static class AbstractCompetingMethods {
176176

177-
@TestBean(name = "beanToOverride")
178-
protected String example;
179-
180177
static String example() {
181178
throw new IllegalStateException("Should not be called");
182179
}
183180
}
184181

185182
static class FailureCompetingOverrideMethods extends AbstractCompetingMethods {
186183

184+
@TestBean(name = "beanToOverride")
185+
String example;
186+
187187
static String beanToOverride() {
188188
throw new IllegalStateException("Should not be called");
189189
}

0 commit comments

Comments
 (0)