diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc index 9f294606f2e..f142a2ded82 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc @@ -92,8 +92,10 @@ Java:: [TIP] ==== -Spring searches for the factory method to invoke in the test class, in the test class -hierarchy, and in the enclosing class hierarchy for a `@Nested` test class. +To locate the factory method to invoke, Spring searches in the class in which the +`@TestBean` field is declared, in one of its superclasses, or in any implemented +interfaces. If the `@TestBean` field is declared in a `@Nested` test class, the enclosing +class hierarchy will also be searched. Alternatively, a factory method in an external class can be referenced via its fully-qualified method name following the syntax `#` diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java index 3b870f1dcb9..f6a63ed4574 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java @@ -44,24 +44,24 @@ import org.springframework.test.context.bean.override.BeanOverride; * you can set the {@link #enforceOverride() enforceOverride} attribute to {@code true} * — for example, {@code @TestBean(enforceOverride = true)}. * - *

The instance is created from a zero-argument static factory method in the - * test class whose return type is compatible with the annotated field. In the - * case of a nested test class, the enclosing class hierarchy is also searched. - * Similarly, if the test class extends from a base class or implements any - * interfaces, the entire type hierarchy is searched. Alternatively, a factory - * method in an external class can be referenced via its fully-qualified method - * name following the syntax {@code #} - * — for example, + *

The instance is created from a zero-argument static factory method whose + * return type is compatible with the annotated field. The factory method can be + * declared directly in the class which declares the {@code @TestBean} field or + * within the type hierarchy above that class, including implemented interfaces. + * If the {@code @TestBean} field is declared in a nested test class, the enclosing + * class hierarchy is also searched. Alternatively, a factory method in an external + * class can be referenced via its fully-qualified method name following the syntax + * {@code #} — for example, * {@code @TestBean(methodName = "org.example.TestUtils#createCustomerRepository")}. * *

The factory method is deduced as follows. * *

* *

Consider the following example. @@ -146,15 +146,17 @@ public @interface TestBean { /** * Name of the static factory method that will be used to instantiate the bean * to override. - *

A search will be performed to find the factory method in the test class, - * in one of its superclasses, or in any implemented interfaces. In the case - * of a nested test class, the enclosing class hierarchy will also be searched. + *

A search will be performed to find the factory method in the class in + * which the {@code @TestBean} field is declared, in one of its superclasses, + * or in any implemented interfaces. If the {@code @TestBean} field is declared + * in a nested test class, the enclosing class hierarchy will also be searched. *

Alternatively, a factory method in an external class can be referenced * via its fully-qualified method name following the syntax * {@code #} — for example, * {@code @TestBean(methodName = "org.example.TestUtils#createCustomerRepository")}. *

If left unspecified, the name of the factory method will be detected - * based either on the name of the annotated field or the name of the bean. + * based either on the name of the {@code @TestBean} field or the {@link #name() name} + * of the bean. */ String methodName() default ""; diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java index 778cedf3cd6..a47d491b845 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * 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. @@ -67,7 +67,7 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor { Method factoryMethod; if (!methodName.isBlank()) { // If the user specified an explicit method name, search for that. - factoryMethod = findTestBeanFactoryMethod(testClass, field.getType(), methodName); + factoryMethod = findTestBeanFactoryMethod(field.getDeclaringClass(), field.getType(), methodName); } else { // Otherwise, search for candidate factory methods whose names match either @@ -78,7 +78,7 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor { if (beanName != null) { candidateMethodNames.add(beanName); } - factoryMethod = findTestBeanFactoryMethod(testClass, field.getType(), candidateMethodNames); + factoryMethod = findTestBeanFactoryMethod(field.getDeclaringClass(), field.getType(), candidateMethodNames); } return new TestBeanOverrideHandler( diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanInheritanceIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanInheritanceIntegrationTests.java index 9e3890c005b..ade77e9dc83 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanInheritanceIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanInheritanceIntegrationTests.java @@ -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(); } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanTests.java index f860700524a..d771af6333e 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanTests.java @@ -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"); }