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
This commit is contained in:
Sam Brannen
2025-01-07 17:20:32 +02:00
parent e8745522fc
commit cd2fbb1ec5
5 changed files with 59 additions and 70 deletions

View File

@@ -48,19 +48,16 @@ public class TestBeanInheritanceIntegrationTests {
return new FakePojo("puzzle in enclosing class");
}
static Pojo enclosingClassBean() {
static Pojo enclosingClassFactoryMethod() {
return new FakePojo("in enclosing test class");
}
abstract static class AbstractTestCase {
@TestBean
Pojo someBean;
@TestBean("otherBean")
Pojo otherBean;
@TestBean("thirdBean")
@TestBean
Pojo anotherBean;
@TestBean
@@ -70,8 +67,8 @@ public class TestBeanInheritanceIntegrationTests {
return new FakePojo("other in superclass");
}
static Pojo thirdBean() {
return new FakePojo("third in superclass");
static Pojo anotherBean() {
return new FakePojo("another in superclass");
}
static Pojo enigmaBean() {
@@ -93,49 +90,42 @@ public class TestBeanInheritanceIntegrationTests {
@TestBean(methodName = "commonBean")
Pojo pojo;
@TestBean(name = "pojo2", methodName = "enclosingClassBean")
@TestBean(name = "pojo2", methodName = "enclosingClassFactoryMethod")
Pojo pojo2;
@TestBean(methodName = "localEnigmaBean")
@TestBean
Pojo enigmaBean;
@TestBean
Pojo puzzleBean;
// "Overrides" puzzleBean() defined in TestBeanInheritanceIntegrationTests.
static Pojo puzzleBean() {
return new FakePojo("puzzle in nested class");
}
static Pojo localEnigmaBean() {
// "Overrides" enigmaBean() defined in AbstractTestCase.
static Pojo enigmaBean() {
return new FakePojo("enigma in subclass");
}
static Pojo someBean() {
return new FakePojo("someBeanOverride");
}
// "Overrides" otherBean() defined in AbstractTestBeanIntegrationTestCase.
static Pojo otherBean() {
return new FakePojo("other in subclass");
}
@Test
void fieldInSuperclassWithFactoryMethodInSuperclass() {
assertThat(ctx.getBean("thirdBean")).as("applicationContext").hasToString("third in superclass");
assertThat(super.anotherBean.value()).as("injection point").isEqualTo("third in superclass");
assertThat(ctx.getBean("anotherBean")).as("applicationContext").hasToString("another in superclass");
assertThat(super.anotherBean.value()).as("injection point").isEqualTo("another in superclass");
}
@Test
void fieldInSuperclassWithFactoryMethodInSubclass() {
assertThat(ctx.getBean("someBean")).as("applicationContext").hasToString("someBeanOverride");
assertThat(super.someBean.value()).as("injection point").isEqualTo("someBeanOverride");
}
@Test
void fieldInSuperclassWithFactoryMethodInSupeclassAndInSubclass() {
assertThat(ctx.getBean("otherBean")).as("applicationContext").hasToString("other in subclass");
assertThat(super.otherBean.value()).as("injection point").isEqualTo("other in subclass");
@Test // gh-34204
void fieldInSuperclassWithFactoryMethodInSuperclassAndInSubclass() {
// We do not expect "other in subclass", because the @TestBean declaration in
// AbstractTestCase cannot "see" the otherBean() factory method in the subclass.
assertThat(ctx.getBean("otherBean")).as("applicationContext").hasToString("other in superclass");
assertThat(super.otherBean.value()).as("injection point").isEqualTo("other in superclass");
}
@Test
@@ -150,13 +140,13 @@ public class TestBeanInheritanceIntegrationTests {
assertThat(this.pojo2.value()).as("injection point").isEqualTo("in enclosing test class");
}
@Test // gh-34194
@Test // gh-34194, gh-34204
void testBeanInSubclassOverridesTestBeanInSuperclass() {
assertThat(ctx.getBean("enigmaBean")).as("applicationContext").hasToString("enigma in subclass");
assertThat(this.enigmaBean.value()).as("injection point").isEqualTo("enigma in subclass");
}
@Test // gh-34194
@Test // gh-34194, gh-34204
void testBeanInNestedClassOverridesTestBeanInEnclosingClass() {
assertThat(ctx.getBean("puzzleBean")).as("applicationContext").hasToString("puzzle in nested class");
assertThat(this.puzzleBean.value()).as("injection point").isEqualTo("puzzle in nested class");
@@ -166,18 +156,13 @@ public class TestBeanInheritanceIntegrationTests {
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean
Pojo someBean() {
return new ProdPojo();
}
@Bean
Pojo otherBean() {
return new ProdPojo();
}
@Bean
Pojo thirdBean() {
Pojo anotherBean() {
return new ProdPojo();
}

View File

@@ -26,9 +26,10 @@ import org.springframework.test.context.bean.override.BeanOverrideContextCustomi
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link TestBean}.
* Tests for {@link TestBean @TestBean}.
*
* @author Stephane Nicoll
* @author Sam Brannen
*/
public class TestBeanTests {
@@ -109,7 +110,7 @@ public class TestBeanTests {
.isThrownBy(() -> BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(
FailureOverrideInParentWithoutFactoryMethod.class, context))
.withMessage("No static method found named beanToOverride() in %s with return type %s",
FailureOverrideInParentWithoutFactoryMethod.class.getName(), String.class.getName());
AbstractByNameLookup.class.getName(), String.class.getName());
}
@Test
@@ -149,8 +150,7 @@ public class TestBeanTests {
@TestBean(name = "beanToOverride")
private String example;
// Expected static String example() { ... }
// or static String beanToOverride() { ... }
// No example() or beanToOverride() method
}
static class FailureMissingExplicitOverrideMethod {
@@ -158,24 +158,21 @@ public class TestBeanTests {
@TestBean(methodName = "createExample")
private String example;
// Expected static String createExample() { ... }
// NO createExample() method
}
abstract static class AbstractByNameLookup {
@TestBean(methodName = "beanToOverride")
protected String beanToOverride;
}
static class FailureOverrideInParentWithoutFactoryMethod extends AbstractByNameLookup {
@TestBean
String beanToOverride;
// No beanToOverride() method
}
abstract static class AbstractCompetingMethods {
static class FailureOverrideInParentWithoutFactoryMethod extends AbstractByNameLookup {
}
@TestBean(name = "beanToOverride")
protected String example;
abstract static class AbstractCompetingMethods {
static String example() {
throw new IllegalStateException("Should not be called");
@@ -184,6 +181,9 @@ public class TestBeanTests {
static class FailureCompetingOverrideMethods extends AbstractCompetingMethods {
@TestBean(name = "beanToOverride")
String example;
static String beanToOverride() {
throw new IllegalStateException("Should not be called");
}