Skip to content

Commit 825ea38

Browse files
juliette-derancourtsbrannen
authored andcommitted
Introduce new @MethodSource syntax to differentiate overloaded local factory methods
Closes #3080 Closes #3101
1 parent 0c40f5e commit 825ea38

File tree

6 files changed

+124
-21
lines changed

6 files changed

+124
-21
lines changed

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ JUnit repository on GitHub.
3838

3939
==== Bug Fixes
4040

41-
* ❓
41+
* Introduce new `@MethodSource` syntax to add the possibility to explicitly select
42+
an overloaded local factory method without specifying its fully qualified name
4243

4344
==== Deprecations and Breaking Changes
4445

documentation/src/docs/asciidoc/user-guide/writing-tests.adoc

+5-3
Original file line numberDiff line numberDiff line change
@@ -1406,9 +1406,11 @@ include::{testDir}/example/ExternalMethodSourceDemo.java[tags=external_MethodSou
14061406
Factory methods can declare parameters, which will be provided by registered
14071407
implementations of the `ParameterResolver` extension API. In the following example, the
14081408
factory method is referenced by its name since there is only one such method in the test
1409-
class. If there are several methods with the same name, the factory method must be
1410-
referenced by its fully qualified method name – for example,
1411-
`@MethodSource("example.MyTests#factoryMethodWithArguments(java.lang.String)")`.
1409+
class. If there are several local methods with the same name, parameters can also be
1410+
provided to differentiate them – for example, `@MethodSource("factoryMethod()")` or
1411+
`@MethodSource("factoryMethod(java.lang.String)")`. Alternatively, the factory method
1412+
can be referenced by its fully qualified method name, e.g.
1413+
`@MethodSource("example.MyTests#factoryMethod(java.lang.String)")`.
14121414

14131415
[source,java,indent=0]
14141416
----

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

+49-7
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.util.ClassUtils;
3132
import org.junit.platform.commons.util.CollectionUtils;
3233
import org.junit.platform.commons.util.Preconditions;
3334
import org.junit.platform.commons.util.ReflectionUtils;
@@ -62,10 +63,21 @@ private Method getFactoryMethod(ExtensionContext context, String factoryMethodNa
6263
if (StringUtils.isBlank(factoryMethodName)) {
6364
factoryMethodName = testMethod.getName();
6465
}
65-
if (factoryMethodName.contains(".") || factoryMethodName.contains("#")) {
66+
if (looksLikeAFullyQualifiedMethodName(factoryMethodName)) {
6667
return getFactoryMethodByFullyQualifiedName(factoryMethodName);
6768
}
68-
return getFactoryMethodBySimpleName(context.getRequiredTestClass(), testMethod, factoryMethodName);
69+
return getFactoryMethodBySimpleOrQualifiedName(context.getRequiredTestClass(), testMethod, factoryMethodName);
70+
}
71+
72+
private static boolean looksLikeAFullyQualifiedMethodName(String factoryMethodName) {
73+
if (factoryMethodName.contains("#")) {
74+
return true;
75+
}
76+
if (factoryMethodName.contains(".") && factoryMethodName.contains("(")) {
77+
// Excluding cases of simple method names with parameters
78+
return factoryMethodName.indexOf(".") < factoryMethodName.indexOf("(");
79+
}
80+
return factoryMethodName.contains(".");
6981
}
7082

7183
private Method getFactoryMethodByFullyQualifiedName(String fullyQualifiedMethodName) {
@@ -79,19 +91,41 @@ private Method getFactoryMethodByFullyQualifiedName(String fullyQualifiedMethodN
7991
methodParameters, className)));
8092
}
8193

94+
private Method getFactoryMethodBySimpleOrQualifiedName(Class<?> testClass, Method testMethod,
95+
String simpleOrQualifiedMethodName) {
96+
String[] methodParts = ReflectionUtils.parseQualifiedMethodName(simpleOrQualifiedMethodName);
97+
String methodSimpleName = methodParts[0];
98+
String methodParameters = methodParts[1];
99+
100+
List<Method> factoryMethods = findFactoryMethodsBySimpleName(testClass, testMethod, methodSimpleName);
101+
if (factoryMethods.size() == 1) {
102+
return factoryMethods.get(0);
103+
}
104+
105+
List<Method> exactMatches = filterFactoryMethodsWithMatchingParameters(factoryMethods,
106+
simpleOrQualifiedMethodName, methodParameters);
107+
Preconditions.condition(exactMatches.size() == 1,
108+
() -> format("%d factory methods named [%s] were found in class [%s]: %s", factoryMethods.size(),
109+
simpleOrQualifiedMethodName, testClass.getName(), factoryMethods));
110+
return exactMatches.get(0);
111+
}
112+
82113
/**
83114
* Find all methods in the given {@code testClass} with the desired {@code factoryMethodName}
84115
* which have return types that can be converted to a {@link Stream}, ignoring the
85116
* {@code testMethod} itself as well as any {@code @Test}, {@code @TestTemplate},
86117
* or {@code @TestFactory} methods with the same name.
87118
*/
88-
private Method getFactoryMethodBySimpleName(Class<?> testClass, Method testMethod, String factoryMethodName) {
119+
private List<Method> findFactoryMethodsBySimpleName(Class<?> testClass, Method testMethod,
120+
String factoryMethodName) {
89121
Predicate<Method> isCandidate = candidate -> factoryMethodName.equals(candidate.getName())
90122
&& !testMethod.equals(candidate);
91123
List<Method> candidates = ReflectionUtils.findMethods(testClass, isCandidate);
124+
92125
Predicate<Method> isFactoryMethod = method -> isConvertibleToStream(method.getReturnType())
93126
&& !isTestMethod(method);
94127
List<Method> factoryMethods = candidates.stream().filter(isFactoryMethod).collect(toList());
128+
95129
Preconditions.condition(factoryMethods.size() > 0, () -> {
96130
// If we didn't find the factory method using the isFactoryMethod Predicate, perhaps
97131
// the specified factory method has an invalid return type or is a test method.
@@ -104,10 +138,18 @@ private Method getFactoryMethodBySimpleName(Class<?> testClass, Method testMetho
104138
// Otherwise, report that we didn't find anything.
105139
return format("Could not find factory method [%s] in class [%s]", factoryMethodName, testClass.getName());
106140
});
107-
Preconditions.condition(factoryMethods.size() == 1,
108-
() -> format("%d factory methods named [%s] were found in class [%s]: %s", factoryMethods.size(),
109-
factoryMethodName, testClass.getName(), factoryMethods));
110-
return factoryMethods.get(0);
141+
return factoryMethods;
142+
}
143+
144+
private static List<Method> filterFactoryMethodsWithMatchingParameters(List<Method> factoryMethods,
145+
String factoryMethodName, String factoryMethodParameters) {
146+
if (!factoryMethodName.endsWith(")")) {
147+
// If parameters are not specified, no choice is made
148+
return factoryMethods;
149+
}
150+
Predicate<Method> hasRequiredParameters = method -> factoryMethodParameters.equals(
151+
ClassUtils.nullSafeToString(method.getParameterTypes()));
152+
return factoryMethods.stream().filter(hasRequiredParameters).collect(toList());
111153
}
112154

113155
private boolean isTestMethod(Method candidate) {

junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/MethodArgumentsProviderTests.java

+14
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,20 @@ void providesArgumentsUsingFullyQualifiedNameWithParameter() {
394394
assertThat(arguments).containsExactly(array("foo!"), array("bar!"));
395395
}
396396

397+
@Test
398+
void providesArgumentsUsingSimpleNameWithoutParameter() {
399+
var arguments = provideArguments("stringStreamProviderWithOrWithoutParameter()");
400+
401+
assertThat(arguments).containsExactly(array("foo"), array("bar"));
402+
}
403+
404+
@Test
405+
void providesArgumentsUsingSimpleNameWithParameter() {
406+
var arguments = provideArguments("stringStreamProviderWithOrWithoutParameter(java.lang.String)");
407+
408+
assertThat(arguments).containsExactly(array("foo!"), array("bar!"));
409+
}
410+
397411
@Test
398412
void throwsExceptionWhenSeveralFactoryMethodsWithSameNameAreAvailable() {
399413
var exception = assertThrows(PreconditionViolationException.class,

junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java

+33-10
Original file line numberDiff line numberDiff line change
@@ -909,21 +909,44 @@ public static String[] parseFullyQualifiedMethodName(String fullyQualifiedMethod
909909
+ "and then the method name, optionally followed by a parameter list enclosed in parentheses.");
910910

911911
String className = fullyQualifiedMethodName.substring(0, indexOfFirstHashtag);
912-
String methodPart = fullyQualifiedMethodName.substring(indexOfFirstHashtag + 1);
913-
String methodName = methodPart;
912+
String qualifiedMethodName = fullyQualifiedMethodName.substring(indexOfFirstHashtag + 1);
913+
String[] methodPart = parseQualifiedMethodName(qualifiedMethodName);
914+
915+
return new String[] { className, methodPart[0], methodPart[1] };
916+
}
917+
918+
/**
919+
* Parse the supplied method name into a 2-element {@code String[]} with
920+
* the following content.
921+
*
922+
* <ul>
923+
* <li>index {@code 0}: the name of the method</li>
924+
* <li>index {@code 1}: a comma-separated list of parameter types, or a
925+
* blank string if the method does not declare any formal parameters</li>
926+
* </ul>
927+
*
928+
* @param qualifiedMethodName a qualified method name, never {@code null} or blank
929+
* @return a 2-element array of strings containing the parsed values
930+
*/
931+
@API(status = INTERNAL, since = "1.9")
932+
public static String[] parseQualifiedMethodName(String qualifiedMethodName) {
933+
String methodName = qualifiedMethodName;
914934
String methodParameters = "";
915935

916-
if (methodPart.endsWith("()")) {
917-
methodName = methodPart.substring(0, methodPart.length() - 2);
936+
if (qualifiedMethodName.endsWith("()")) {
937+
methodName = qualifiedMethodName.substring(0, qualifiedMethodName.length() - 2);
918938
}
919-
else if (methodPart.endsWith(")")) {
920-
int indexOfLastOpeningParenthesis = methodPart.lastIndexOf('(');
921-
if ((indexOfLastOpeningParenthesis > 0) && (indexOfLastOpeningParenthesis < methodPart.length() - 1)) {
922-
methodName = methodPart.substring(0, indexOfLastOpeningParenthesis);
923-
methodParameters = methodPart.substring(indexOfLastOpeningParenthesis + 1, methodPart.length() - 1);
939+
else if (qualifiedMethodName.endsWith(")")) {
940+
int indexOfLastOpeningParenthesis = qualifiedMethodName.lastIndexOf('(');
941+
if ((indexOfLastOpeningParenthesis > 0)
942+
&& (indexOfLastOpeningParenthesis < qualifiedMethodName.length() - 1)) {
943+
methodName = qualifiedMethodName.substring(0, indexOfLastOpeningParenthesis);
944+
methodParameters = qualifiedMethodName.substring(indexOfLastOpeningParenthesis + 1,
945+
qualifiedMethodName.length() - 1);
924946
}
925947
}
926-
return new String[] { className, methodName, methodParameters };
948+
949+
return new String[] { methodName, methodParameters };
927950
}
928951

929952
/**

platform-tests/src/test/java/org/junit/platform/commons/util/ReflectionUtilsTests.java

+21
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
import org.junit.jupiter.api.Test;
5656
import org.junit.jupiter.api.fixtures.TrackLogRecords;
5757
import org.junit.jupiter.api.io.TempDir;
58+
import org.junit.jupiter.params.ParameterizedTest;
59+
import org.junit.jupiter.params.provider.ValueSource;
5860
import org.junit.platform.commons.JUnitException;
5961
import org.junit.platform.commons.PreconditionViolationException;
6062
import org.junit.platform.commons.logging.LogRecordListener;
@@ -700,6 +702,25 @@ void parseFullyQualifiedMethodNameForMethodWithMultipleParameters() {
700702
.containsExactly("com.example.Test", "method", "int, java.lang.Object");
701703
}
702704

705+
@ParameterizedTest
706+
@ValueSource(strings = { "method", "method()" })
707+
void parseSimpleMethodNameForMethodWithoutParameters(String methodName) {
708+
assertThat(ReflectionUtils.parseQualifiedMethodName(methodName))//
709+
.containsExactly("method", "");
710+
}
711+
712+
@Test
713+
void parseSimpleMethodNameForMethodWithSingleParameter() {
714+
assertThat(ReflectionUtils.parseQualifiedMethodName("method(java.lang.Object)"))//
715+
.containsExactly("method", "java.lang.Object");
716+
}
717+
718+
@Test
719+
void parseSimpleMethodNameForMethodWithMultipleParameters() {
720+
assertThat(ReflectionUtils.parseQualifiedMethodName("method(int, java.lang.Object)"))//
721+
.containsExactly("method", "int, java.lang.Object");
722+
}
723+
703724
@Test
704725
@SuppressWarnings("deprecation")
705726
void getOutermostInstancePreconditions() {

0 commit comments

Comments
 (0)