Skip to content

Commit 82f1f4b

Browse files
committed
Treat overloaded local & external @MethodSource factory methods equally
Prior to this commit, given overloaded @MethodSource factory methods, a local qualified method name (LQMN) was not treated the same as a fully-qualified method name (FQMN). Specifically, if the user provided a FQMN without specifying the parameter list, JUnit Jupiter would find the factory method that accepts zero arguments. Whereas, if the user provided an LQMN without specifying the parameter list, JUnit Jupiter would fail to find the factory method. This commit fixes that bug by reworking the internals of MethodArgumentsProvider so that overloaded local and external factory methods are looked up with the same semantics. The commit also improves diagnostics for failure to find a factory method specified via LQMN by falling back to the same lenient search semantics that are used to locate a "default" local factory method. Furthermore, this commit modifies the internals of MethodArgumentsProvider so that it consistently throws PreconditionViolationExceptions for user configuration errors. This commit also introduces additional tests for error use cases. See: #3130, #3131 Closes: #3266
1 parent 5b4a000 commit 82f1f4b

File tree

4 files changed

+300
-125
lines changed

4 files changed

+300
-125
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.9.3.adoc

+7-1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ JUnit repository on GitHub.
4040
`@MethodSource("myFactory([I)"` (which was already supported) and
4141
`@MethodSource("myFactory(java.lang.String[])` instead of
4242
`@MethodSource("myFactory([Ljava.lang.String;)`.
43+
* The search algorithm used to find `@MethodSource` factory methods now applies consistent
44+
semantics for _local_ qualified method names and fully-qualified method names for
45+
overloaded factory methods.
4346
* Exceptions thrown for files that cannot be deleted when cleaning up a temporary
4447
directory created via `@TempDir` now include the root cause.
4548
* Lifecycle methods are allowed to be declared as `private` again for backwards
@@ -52,7 +55,10 @@ JUnit repository on GitHub.
5255

5356
==== New Features and Improvements
5457

55-
* ❓
58+
* The search algorithm used to find `@MethodSource` factory methods now falls back to
59+
lenient search semantics when a factory method cannot be found by qualified name
60+
(without a parameter list) and also provides better diagnostics when a unique factory
61+
method cannot be found.
5662

5763

5864
[[release-notes-5.9.3-junit-vintage]]

junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodArgumentsProvider.java

+64-49
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.junit.jupiter.api.extension.ExtensionContext;
2929
import org.junit.jupiter.params.support.AnnotationConsumer;
3030
import org.junit.platform.commons.JUnitException;
31+
import org.junit.platform.commons.PreconditionViolationException;
3132
import org.junit.platform.commons.util.CollectionUtils;
3233
import org.junit.platform.commons.util.Preconditions;
3334
import org.junit.platform.commons.util.ReflectionUtils;
@@ -40,6 +41,9 @@ class MethodArgumentsProvider implements ArgumentsProvider, AnnotationConsumer<M
4041

4142
private String[] methodNames;
4243

44+
private static final Predicate<Method> isFactoryMethod = //
45+
method -> isConvertibleToStream(method.getReturnType()) && !isTestMethod(method);
46+
4347
@Override
4448
public void accept(MethodSource annotation) {
4549
this.methodNames = annotation.value();
@@ -52,28 +56,37 @@ public Stream<Arguments> provideArguments(ExtensionContext context) {
5256
Object testInstance = context.getTestInstance().orElse(null);
5357
// @formatter:off
5458
return stream(this.methodNames)
55-
.map(factoryMethodName -> getFactoryMethod(testClass, testMethod, factoryMethodName))
59+
.map(factoryMethodName -> findFactoryMethod(testClass, testMethod, factoryMethodName))
5660
.map(factoryMethod -> context.getExecutableInvoker().invoke(factoryMethod, testInstance))
5761
.flatMap(CollectionUtils::toStream)
5862
.map(MethodArgumentsProvider::toArguments);
5963
// @formatter:on
6064
}
6165

62-
private Method getFactoryMethod(Class<?> testClass, Method testMethod, String factoryMethodName) {
63-
if (!StringUtils.isBlank(factoryMethodName)) {
64-
if (looksLikeAFullyQualifiedMethodName(factoryMethodName)) {
65-
return getFactoryMethodByFullyQualifiedName(factoryMethodName);
66-
}
67-
else if (looksLikeALocalQualifiedMethodName(factoryMethodName)) {
68-
return getFactoryMethodByFullyQualifiedName(testClass.getName() + "#" + factoryMethodName);
69-
}
70-
}
71-
else {
72-
// User did not provide a factory method name, so we search for a
73-
// factory method with the same name as the parameterized test method.
66+
private static Method findFactoryMethod(Class<?> testClass, Method testMethod, String factoryMethodName) {
67+
String originalFactoryMethodName = factoryMethodName;
68+
69+
// If the user did not provide a factory method name, find a "default" local
70+
// factory method with the same name as the parameterized test method.
71+
if (StringUtils.isBlank(factoryMethodName)) {
7472
factoryMethodName = testMethod.getName();
73+
return findFactoryMethodBySimpleName(testClass, testMethod, factoryMethodName);
7574
}
76-
return findFactoryMethodBySimpleName(testClass, testMethod, factoryMethodName);
75+
76+
// Convert local factory method name to fully-qualified method name.
77+
if (!looksLikeAFullyQualifiedMethodName(factoryMethodName)) {
78+
factoryMethodName = testClass.getName() + "#" + factoryMethodName;
79+
}
80+
81+
// Find factory method using fully-qualified name.
82+
Method factoryMethod = findFactoryMethodByFullyQualifiedName(testMethod, factoryMethodName);
83+
84+
// Ensure factory method has a valid return type and is not a test method.
85+
Preconditions.condition(isFactoryMethod.test(factoryMethod), () -> format(
86+
"Could not find valid factory method [%s] for test class [%s] but found the following invalid candidate: %s",
87+
originalFactoryMethodName, testClass.getName(), factoryMethod));
88+
89+
return factoryMethod;
7790
}
7891

7992
private static boolean looksLikeAFullyQualifiedMethodName(String factoryMethodName) {
@@ -90,52 +103,54 @@ private static boolean looksLikeAFullyQualifiedMethodName(String factoryMethodNa
90103
return indexOfDot < indexOfOpeningParenthesis;
91104
}
92105
// If we get this far, we conclude the supplied factory method name "looks"
93-
// like it was intended to be a fully qualified method name, even if the
106+
// like it was intended to be a fully-qualified method name, even if the
94107
// syntax is invalid. We do this in order to provide better diagnostics for
95-
// the user when a fully qualified method name is in fact invalid.
108+
// the user when a fully-qualified method name is in fact invalid.
96109
return true;
97110
}
98111

99-
private static boolean looksLikeALocalQualifiedMethodName(String factoryMethodName) {
100-
// This method is intended to be called after looksLikeAFullyQualifiedMethodName()
101-
// and therefore does not check for the absence of '#' and does not reason about
102-
// the presence or absence of a fully qualified class name.
103-
if (factoryMethodName.endsWith("()")) {
104-
return true;
105-
}
106-
int indexOfLastOpeningParenthesis = factoryMethodName.lastIndexOf('(');
107-
return (indexOfLastOpeningParenthesis > 0)
108-
&& (indexOfLastOpeningParenthesis < factoryMethodName.lastIndexOf(')'));
109-
}
110-
111-
private Method getFactoryMethodByFullyQualifiedName(String fullyQualifiedMethodName) {
112+
private static Method findFactoryMethodByFullyQualifiedName(Method testMethod, String fullyQualifiedMethodName) {
112113
String[] methodParts = ReflectionUtils.parseFullyQualifiedMethodName(fullyQualifiedMethodName);
113114
String className = methodParts[0];
114115
String methodName = methodParts[1];
115116
String methodParameters = methodParts[2];
117+
Class<?> clazz = loadRequiredClass(className);
118+
119+
// Attempt to find an exact match first.
120+
Method factoryMethod = ReflectionUtils.findMethod(clazz, methodName, methodParameters).orElse(null);
121+
if (factoryMethod != null) {
122+
return factoryMethod;
123+
}
124+
125+
boolean explicitParameterListSpecified = //
126+
StringUtils.isNotBlank(methodParameters) || fullyQualifiedMethodName.endsWith("()");
127+
128+
// If we didn't find an exact match but an explicit parameter list was specified,
129+
// that's a user configuration error.
130+
Preconditions.condition(!explicitParameterListSpecified,
131+
() -> format("Could not find factory method [%s(%s)] in class [%s]", methodName, methodParameters,
132+
className));
116133

117-
return ReflectionUtils.findMethod(loadRequiredClass(className), methodName, methodParameters).orElseThrow(
118-
() -> new JUnitException(format("Could not find factory method [%s(%s)] in class [%s]", methodName,
119-
methodParameters, className)));
134+
// Otherwise, fall back to the same lenient search semantics that are used
135+
// to locate a "default" local factory method.
136+
return findFactoryMethodBySimpleName(clazz, testMethod, methodName);
120137
}
121138

122139
/**
123-
* Find all methods in the given {@code testClass} with the desired {@code factoryMethodName}
124-
* which have return types that can be converted to a {@link Stream}, ignoring the
125-
* {@code testMethod} itself as well as any {@code @Test}, {@code @TestTemplate},
126-
* or {@code @TestFactory} methods with the same name.
127-
* @return the factory method, if found
128-
* @throws org.junit.platform.commons.PreconditionViolationException if the
129-
* factory method was not found or if multiple competing factory methods with
130-
* the same name were found
140+
* Find the factory method by searching for all methods in the given {@code clazz}
141+
* with the desired {@code factoryMethodName} which have return types that can be
142+
* converted to a {@link Stream}, ignoring the {@code testMethod} itself as well
143+
* as any {@code @Test}, {@code @TestTemplate}, or {@code @TestFactory} methods
144+
* with the same name.
145+
* @return the single factory method matching the search criteria
146+
* @throws PreconditionViolationException if the factory method was not found or
147+
* multiple competing factory methods with the same name were found
131148
*/
132-
private Method findFactoryMethodBySimpleName(Class<?> testClass, Method testMethod, String factoryMethodName) {
149+
private static Method findFactoryMethodBySimpleName(Class<?> clazz, Method testMethod, String factoryMethodName) {
133150
Predicate<Method> isCandidate = candidate -> factoryMethodName.equals(candidate.getName())
134151
&& !testMethod.equals(candidate);
135-
List<Method> candidates = ReflectionUtils.findMethods(testClass, isCandidate);
152+
List<Method> candidates = ReflectionUtils.findMethods(clazz, isCandidate);
136153

137-
Predicate<Method> isFactoryMethod = method -> isConvertibleToStream(method.getReturnType())
138-
&& !isTestMethod(method);
139154
List<Method> factoryMethods = candidates.stream().filter(isFactoryMethod).collect(toList());
140155

141156
Preconditions.condition(factoryMethods.size() > 0, () -> {
@@ -145,23 +160,23 @@ private Method findFactoryMethodBySimpleName(Class<?> testClass, Method testMeth
145160
if (candidates.size() > 0) {
146161
return format(
147162
"Could not find valid factory method [%s] in class [%s] but found the following invalid candidates: %s",
148-
factoryMethodName, testClass.getName(), candidates);
163+
factoryMethodName, clazz.getName(), candidates);
149164
}
150165
// Otherwise, report that we didn't find anything.
151-
return format("Could not find factory method [%s] in class [%s]", factoryMethodName, testClass.getName());
166+
return format("Could not find factory method [%s] in class [%s]", factoryMethodName, clazz.getName());
152167
});
153168
Preconditions.condition(factoryMethods.size() == 1,
154169
() -> format("%d factory methods named [%s] were found in class [%s]: %s", factoryMethods.size(),
155-
factoryMethodName, testClass.getName(), factoryMethods));
170+
factoryMethodName, clazz.getName(), factoryMethods));
156171
return factoryMethods.get(0);
157172
}
158173

159-
private boolean isTestMethod(Method candidate) {
174+
private static boolean isTestMethod(Method candidate) {
160175
return isAnnotated(candidate, Test.class) || isAnnotated(candidate, TestTemplate.class)
161176
|| isAnnotated(candidate, TestFactory.class);
162177
}
163178

164-
private Class<?> loadRequiredClass(String className) {
179+
private static Class<?> loadRequiredClass(String className) {
165180
return ReflectionUtils.tryToLoadClass(className).getOrThrow(
166181
cause -> new JUnitException(format("Could not load class [%s]", className), cause));
167182
}

junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java

+12-4
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,20 @@
110110
* The names of factory methods within the test class or in external classes
111111
* to use as sources for arguments.
112112
*
113-
* <p>Factory methods in external classes must be referenced by <em>fully
114-
* qualified method name</em> &mdash; for example,
115-
* {@code com.example.StringsProviders#blankStrings} or
116-
* {@code com.example.TopLevelClass$NestedClass#classMethod} for a factory
113+
* <p>Factory methods in external classes must be referenced by
114+
* <em>fully-qualified method name</em> &mdash; for example,
115+
* {@code "com.example.StringsProviders#blankStrings"} or
116+
* {@code "com.example.TopLevelClass$NestedClass#classMethod"} for a factory
117117
* method in a static nested class.
118118
*
119+
* <p>If a factory method accepts arguments that are provided by a
120+
* {@link org.junit.jupiter.api.extension.ParameterResolver ParameterResolver},
121+
* you can supply the formal parameter list in the qualified method name to
122+
* disambiguate between overloaded variants of the factory method. For example,
123+
* {@code "blankStrings(int)"} for a local qualified method name or
124+
* {@code "com.example.StringsProviders#blankStrings(int)"} for a fully-qualified
125+
* method name.
126+
*
119127
* <p>If no factory method names are declared, a method within the test class
120128
* that has the same name as the test method will be used as the factory
121129
* method by default.

0 commit comments

Comments
 (0)