Stop using a conventional suffix for TestBean factory methods

This commit changes how factory method for `@TestBean` usage is
discovered. Previously the field name or bean name suffixed with
'TestOverride' was used. It sounds more natural to just use the
field name or bean name, leaving cases where a suffix is required
to explicitly providing the method name.

As part of this change, the exception messages have been revisited as
it's less since the method name candidates have the exact same name
as the field or bean name. A `()` is added to make it more clear the
name is for a method.

Closes gh-32940
This commit is contained in:
Stéphane Nicoll
2024-06-10 12:41:02 +02:00
parent 5787bc569d
commit 96302a7102
12 changed files with 105 additions and 104 deletions

View File

@@ -43,9 +43,9 @@ import org.springframework.test.context.bean.override.BeanOverride;
* <ul>
* <li>If the {@link #methodName()} is specified, look for a static method with
* that name.</li>
* <li>If a method name is not specified, look for exactly one static method named
* with a suffix equal to {@value #CONVENTION_SUFFIX} and starting with either the
* name of the annotated field or the name of the bean (if specified).</li>
* <li>If a method name is not specified, look for exactly one static method
* named with either the name of the annotated field or the name of the bean
* (if specified).</li>
* </ul>
*
* <p>Consider the following example.
@@ -58,16 +58,16 @@ import org.springframework.test.context.bean.override.BeanOverride;
*
* // Tests
*
* private static CustomerRepository repositoryTestOverride() {
* private static CustomerRepository repository() {
* return new TestCustomerRepository();
* }
* }</code></pre>
*
* <p>In the example above, the {@code repository} bean is replaced by the
* instance generated by the {@code repositoryTestOverride()} method. Not only
* is the overridden instance injected into the {@code repository} field, but it
* is also replaced in the {@code BeanFactory} so that other injection points
* for that bean use the overridden bean instance.
* instance generated by the {@code repository()} method. Not only is the
* overridden instance injected into the {@code repository} field, but it is
* also replaced in the {@code BeanFactory} so that other injection points for
* that bean use the overridden bean instance.
*
* <p>To make things more explicit, the bean and method names can be set,
* as shown in the following example.
@@ -75,7 +75,7 @@ import org.springframework.test.context.bean.override.BeanOverride;
* <pre><code>
* class CustomerServiceTests {
*
* &#064;TestBean(name = "repository", methodName = "createTestCustomerRepository")
* &#064;TestBean(name = "customerRepository", methodName = "createTestCustomerRepository")
* private CustomerRepository repository;
*
* // Tests
@@ -97,14 +97,6 @@ import org.springframework.test.context.bean.override.BeanOverride;
@BeanOverride(TestBeanOverrideProcessor.class)
public @interface TestBean {
/**
* Required suffix for the name of a factory method that overrides a bean
* instance when the factory method is detected by convention.
* @see #methodName()
*/
String CONVENTION_SUFFIX = "TestOverride";
/**
* Alias for {@link #name()}.
* <p>Intended to be used when no other attributes are needed &mdash; for
@@ -130,8 +122,7 @@ public @interface TestBean {
* also considered. Similarly, in case the test class inherits from a base
* class the whole class hierarchy is considered.
* <p>If left unspecified, the name of the factory method will be detected
* based on convention.
* @see #CONVENTION_SUFFIX
* based either on the name of the annotated field or the name of the bean.
*/
String methodName() default "";

View File

@@ -21,9 +21,11 @@ import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.ResolvableType;
@@ -41,6 +43,7 @@ import org.springframework.util.StringUtils;
*
* @author Simon Baslé
* @author Sam Brannen
* @author Stephane Nicoll
* @since 6.2
*/
class TestBeanOverrideProcessor implements BeanOverrideProcessor {
@@ -58,14 +61,14 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor {
overrideMethod = findTestBeanFactoryMethod(testClass, field.getType(), methodName);
}
else {
// Otherwise, search for candidate factory methods using the convention
// suffix and the field name or explicit bean name (if any).
// Otherwise, search for candidate factory methods the field name
// or explicit bean name (if any).
List<String> candidateMethodNames = new ArrayList<>();
candidateMethodNames.add(field.getName() + TestBean.CONVENTION_SUFFIX);
candidateMethodNames.add(field.getName());
String beanName = testBeanAnnotation.name();
if (StringUtils.hasText(beanName)) {
candidateMethodNames.add(beanName + TestBean.CONVENTION_SUFFIX);
candidateMethodNames.add(beanName);
}
overrideMethod = findTestBeanFactoryMethod(testClass, field.getType(), candidateMethodNames);
}
@@ -75,7 +78,7 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor {
/**
* Find a test bean factory {@link Method} for the given {@link Class}.
* <p>Delegates to {@link #findTestBeanFactoryMethod(Class, Class, List)}.
* <p>Delegates to {@link #findTestBeanFactoryMethod(Class, Class, Collection)}.
*/
Method findTestBeanFactoryMethod(Class<?> clazz, Class<?> methodReturnType, String... methodNames) {
return findTestBeanFactoryMethod(clazz, methodReturnType, List.of(methodNames));
@@ -104,7 +107,7 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor {
* @throws IllegalStateException if a matching factory method cannot
* be found or multiple methods match
*/
Method findTestBeanFactoryMethod(Class<?> clazz, Class<?> methodReturnType, List<String> methodNames) {
Method findTestBeanFactoryMethod(Class<?> clazz, Class<?> methodReturnType, Collection<String> methodNames) {
Assert.notEmpty(methodNames, "At least one candidate method name is required");
Set<String> supportedNames = new LinkedHashSet<>(methodNames);
MethodFilter methodFilter = method -> (Modifier.isStatic(method.getModifiers()) &&
@@ -113,16 +116,16 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor {
Set<Method> methods = findMethods(clazz, methodFilter);
Assert.state(!methods.isEmpty(), () -> """
Failed to find a static test bean factory method in %s with return type %s \
whose name matches one of the supported candidates %s""".formatted(
clazz.getName(), methodReturnType.getName(), supportedNames));
String methodNamesDescription = supportedNames.stream()
.map(name -> name + "()").collect(Collectors.joining(" or "));
Assert.state(!methods.isEmpty(), () ->
"No static method found named %s in %s with return type %s".formatted(
methodNamesDescription, clazz.getName(), methodReturnType.getName()));
long uniqueMethodNameCount = methods.stream().map(Method::getName).distinct().count();
Assert.state(uniqueMethodNameCount == 1, () -> """
Found %d competing static test bean factory methods in %s with return type %s \
whose name matches one of the supported candidates %s""".formatted(
uniqueMethodNameCount, clazz.getName(), methodReturnType.getName(), supportedNames));
Assert.state(uniqueMethodNameCount == 1, () ->
"Found %d competing static methods named %s in %s with return type %s".formatted(
uniqueMethodNameCount, methodNamesDescription, clazz.getName(), methodReturnType.getName()));
return methods.iterator().next();
}