Merge branch '6.2.x'

This commit is contained in:
Sam Brannen 2025-01-07 17:32:42 +02:00
commit e8788c7e3e
5 changed files with 59 additions and 70 deletions

View File

@ -92,8 +92,10 @@ Java::
[TIP] [TIP]
==== ====
Spring searches for the factory method to invoke in the test class, in the test class To locate the factory method to invoke, Spring searches in the class in which the
hierarchy, and in the enclosing class hierarchy for a `@Nested` test class. `@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 Alternatively, a factory method in an external class can be referenced via its
fully-qualified method name following the syntax `<fully-qualified class name>#<method name>` fully-qualified method name following the syntax `<fully-qualified class name>#<method name>`

View File

@ -44,24 +44,24 @@ import org.springframework.test.context.bean.override.BeanOverride;
* you can set the {@link #enforceOverride() enforceOverride} attribute to {@code true} * you can set the {@link #enforceOverride() enforceOverride} attribute to {@code true}
* &mdash; for example, {@code @TestBean(enforceOverride = true)}. * &mdash; for example, {@code @TestBean(enforceOverride = true)}.
* *
* <p>The instance is created from a zero-argument static factory method in the * <p>The instance is created from a zero-argument static factory method whose
* test class whose return type is compatible with the annotated field. In the * return type is compatible with the annotated field. The factory method can be
* case of a nested test class, the enclosing class hierarchy is also searched. * declared directly in the class which declares the {@code @TestBean} field or
* Similarly, if the test class extends from a base class or implements any * within the type hierarchy above that class, including implemented interfaces.
* interfaces, the entire type hierarchy is searched. Alternatively, a factory * If the {@code @TestBean} field is declared in a nested test class, the enclosing
* method in an external class can be referenced via its fully-qualified method * class hierarchy is also searched. Alternatively, a factory method in an external
* name following the syntax {@code <fully-qualified class name>#<method name>} * class can be referenced via its fully-qualified method name following the syntax
* &mdash; for example, * {@code <fully-qualified class name>#<method name>} &mdash; for example,
* {@code @TestBean(methodName = "org.example.TestUtils#createCustomerRepository")}. * {@code @TestBean(methodName = "org.example.TestUtils#createCustomerRepository")}.
* *
* <p>The factory method is deduced as follows. * <p>The factory method is deduced as follows.
* *
* <ul> * <ul>
* <li>If the {@link #methodName()} is specified, look for a static method with * <li>If the {@link #methodName methodName} is specified, Spring looks for a static
* that name.</li> * method with that name.</li>
* <li>If a method name is not specified, look for exactly one static method * <li>If a method name is not specified, Spring looks for exactly one static method
* named with either the name of the annotated field or the name of the bean * whose name is either the name of the annotated field or the {@link #name() name}
* (if specified).</li> * of the bean (if specified).</li>
* </ul> * </ul>
* *
* <p>Consider the following example. * <p>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 * Name of the static factory method that will be used to instantiate the bean
* to override. * to override.
* <p>A search will be performed to find the factory method in the test class, * <p>A search will be performed to find the factory method in the class in
* in one of its superclasses, or in any implemented interfaces. In the case * which the {@code @TestBean} field is declared, in one of its superclasses,
* of a nested test class, the enclosing class hierarchy will also be searched. * 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.
* <p>Alternatively, a factory method in an external class can be referenced * <p>Alternatively, a factory method in an external class can be referenced
* via its fully-qualified method name following the syntax * via its fully-qualified method name following the syntax
* {@code <fully-qualified class name>#<method name>} &mdash; for example, * {@code <fully-qualified class name>#<method name>} &mdash; for example,
* {@code @TestBean(methodName = "org.example.TestUtils#createCustomerRepository")}. * {@code @TestBean(methodName = "org.example.TestUtils#createCustomerRepository")}.
* <p>If left unspecified, the name of the factory method will be detected * <p>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 ""; String methodName() default "";

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -67,7 +67,7 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor {
Method factoryMethod; Method factoryMethod;
if (!methodName.isBlank()) { if (!methodName.isBlank()) {
// If the user specified an explicit method name, search for that. // 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 { else {
// Otherwise, search for candidate factory methods whose names match either // Otherwise, search for candidate factory methods whose names match either
@ -78,7 +78,7 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor {
if (beanName != null) { if (beanName != null) {
candidateMethodNames.add(beanName); candidateMethodNames.add(beanName);
} }
factoryMethod = findTestBeanFactoryMethod(testClass, field.getType(), candidateMethodNames); factoryMethod = findTestBeanFactoryMethod(field.getDeclaringClass(), field.getType(), candidateMethodNames);
} }
return new TestBeanOverrideHandler( return new TestBeanOverrideHandler(

View File

@ -48,19 +48,16 @@ public class TestBeanInheritanceIntegrationTests {
return new FakePojo("puzzle in enclosing class"); return new FakePojo("puzzle in enclosing class");
} }
static Pojo enclosingClassBean() { static Pojo enclosingClassFactoryMethod() {
return new FakePojo("in enclosing test class"); return new FakePojo("in enclosing test class");
} }
abstract static class AbstractTestCase { abstract static class AbstractTestCase {
@TestBean
Pojo someBean;
@TestBean("otherBean") @TestBean("otherBean")
Pojo otherBean; Pojo otherBean;
@TestBean("thirdBean") @TestBean
Pojo anotherBean; Pojo anotherBean;
@TestBean @TestBean
@ -70,8 +67,8 @@ public class TestBeanInheritanceIntegrationTests {
return new FakePojo("other in superclass"); return new FakePojo("other in superclass");
} }
static Pojo thirdBean() { static Pojo anotherBean() {
return new FakePojo("third in superclass"); return new FakePojo("another in superclass");
} }
static Pojo enigmaBean() { static Pojo enigmaBean() {
@ -93,49 +90,42 @@ public class TestBeanInheritanceIntegrationTests {
@TestBean(methodName = "commonBean") @TestBean(methodName = "commonBean")
Pojo pojo; Pojo pojo;
@TestBean(name = "pojo2", methodName = "enclosingClassBean") @TestBean(name = "pojo2", methodName = "enclosingClassFactoryMethod")
Pojo pojo2; Pojo pojo2;
@TestBean(methodName = "localEnigmaBean") @TestBean
Pojo enigmaBean; Pojo enigmaBean;
@TestBean @TestBean
Pojo puzzleBean; Pojo puzzleBean;
// "Overrides" puzzleBean() defined in TestBeanInheritanceIntegrationTests.
static Pojo puzzleBean() { static Pojo puzzleBean() {
return new FakePojo("puzzle in nested class"); return new FakePojo("puzzle in nested class");
} }
static Pojo localEnigmaBean() { // "Overrides" enigmaBean() defined in AbstractTestCase.
static Pojo enigmaBean() {
return new FakePojo("enigma in subclass"); return new FakePojo("enigma in subclass");
} }
static Pojo someBean() {
return new FakePojo("someBeanOverride");
}
// "Overrides" otherBean() defined in AbstractTestBeanIntegrationTestCase.
static Pojo otherBean() { static Pojo otherBean() {
return new FakePojo("other in subclass"); return new FakePojo("other in subclass");
} }
@Test @Test
void fieldInSuperclassWithFactoryMethodInSuperclass() { void fieldInSuperclassWithFactoryMethodInSuperclass() {
assertThat(ctx.getBean("thirdBean")).as("applicationContext").hasToString("third in superclass"); assertThat(ctx.getBean("anotherBean")).as("applicationContext").hasToString("another in superclass");
assertThat(super.anotherBean.value()).as("injection point").isEqualTo("third in superclass"); assertThat(super.anotherBean.value()).as("injection point").isEqualTo("another in superclass");
} }
@Test @Test // gh-34204
void fieldInSuperclassWithFactoryMethodInSubclass() { void fieldInSuperclassWithFactoryMethodInSuperclassAndInSubclass() {
assertThat(ctx.getBean("someBean")).as("applicationContext").hasToString("someBeanOverride"); // We do not expect "other in subclass", because the @TestBean declaration in
assertThat(super.someBean.value()).as("injection point").isEqualTo("someBeanOverride"); // 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
void fieldInSuperclassWithFactoryMethodInSupeclassAndInSubclass() {
assertThat(ctx.getBean("otherBean")).as("applicationContext").hasToString("other in subclass");
assertThat(super.otherBean.value()).as("injection point").isEqualTo("other in subclass");
} }
@Test @Test
@ -150,13 +140,13 @@ public class TestBeanInheritanceIntegrationTests {
assertThat(this.pojo2.value()).as("injection point").isEqualTo("in enclosing test class"); assertThat(this.pojo2.value()).as("injection point").isEqualTo("in enclosing test class");
} }
@Test // gh-34194 @Test // gh-34194, gh-34204
void testBeanInSubclassOverridesTestBeanInSuperclass() { void testBeanInSubclassOverridesTestBeanInSuperclass() {
assertThat(ctx.getBean("enigmaBean")).as("applicationContext").hasToString("enigma in subclass"); assertThat(ctx.getBean("enigmaBean")).as("applicationContext").hasToString("enigma in subclass");
assertThat(this.enigmaBean.value()).as("injection point").isEqualTo("enigma in subclass"); assertThat(this.enigmaBean.value()).as("injection point").isEqualTo("enigma in subclass");
} }
@Test // gh-34194 @Test // gh-34194, gh-34204
void testBeanInNestedClassOverridesTestBeanInEnclosingClass() { void testBeanInNestedClassOverridesTestBeanInEnclosingClass() {
assertThat(ctx.getBean("puzzleBean")).as("applicationContext").hasToString("puzzle in nested class"); assertThat(ctx.getBean("puzzleBean")).as("applicationContext").hasToString("puzzle in nested class");
assertThat(this.puzzleBean.value()).as("injection point").isEqualTo("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) @Configuration(proxyBeanMethods = false)
static class Config { static class Config {
@Bean
Pojo someBean() {
return new ProdPojo();
}
@Bean @Bean
Pojo otherBean() { Pojo otherBean() {
return new ProdPojo(); return new ProdPojo();
} }
@Bean @Bean
Pojo thirdBean() { Pojo anotherBean() {
return new ProdPojo(); 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; import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/** /**
* Tests for {@link TestBean}. * Tests for {@link TestBean @TestBean}.
* *
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Sam Brannen
*/ */
public class TestBeanTests { public class TestBeanTests {
@ -109,7 +110,7 @@ public class TestBeanTests {
.isThrownBy(() -> BeanOverrideContextCustomizerTestUtils.customizeApplicationContext( .isThrownBy(() -> BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(
FailureOverrideInParentWithoutFactoryMethod.class, context)) FailureOverrideInParentWithoutFactoryMethod.class, context))
.withMessage("No static method found named beanToOverride() in %s with return type %s", .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 @Test
@ -149,8 +150,7 @@ public class TestBeanTests {
@TestBean(name = "beanToOverride") @TestBean(name = "beanToOverride")
private String example; private String example;
// Expected static String example() { ... } // No example() or beanToOverride() method
// or static String beanToOverride() { ... }
} }
static class FailureMissingExplicitOverrideMethod { static class FailureMissingExplicitOverrideMethod {
@ -158,24 +158,21 @@ public class TestBeanTests {
@TestBean(methodName = "createExample") @TestBean(methodName = "createExample")
private String example; private String example;
// Expected static String createExample() { ... } // NO createExample() method
} }
abstract static class AbstractByNameLookup { abstract static class AbstractByNameLookup {
@TestBean(methodName = "beanToOverride") @TestBean
protected String beanToOverride; String beanToOverride;
}
static class FailureOverrideInParentWithoutFactoryMethod extends AbstractByNameLookup {
// No beanToOverride() method // No beanToOverride() method
} }
abstract static class AbstractCompetingMethods { static class FailureOverrideInParentWithoutFactoryMethod extends AbstractByNameLookup {
}
@TestBean(name = "beanToOverride") abstract static class AbstractCompetingMethods {
protected String example;
static String example() { static String example() {
throw new IllegalStateException("Should not be called"); throw new IllegalStateException("Should not be called");
@ -184,6 +181,9 @@ public class TestBeanTests {
static class FailureCompetingOverrideMethods extends AbstractCompetingMethods { static class FailureCompetingOverrideMethods extends AbstractCompetingMethods {
@TestBean(name = "beanToOverride")
String example;
static String beanToOverride() { static String beanToOverride() {
throw new IllegalStateException("Should not be called"); throw new IllegalStateException("Should not be called");
} }