Skip to content

Commit 7613b86

Browse files
committed
Introduce @⁠FieldSource for use with @⁠ParameterizedTest
This commit introduces @⁠FieldSource which provides access to values returned from fields of the class in which the annotation is declared or from static fields in external classes referenced by fully qualified field name. The feature is analogous to the existing @⁠MethodSource support. See #2014
1 parent 84d426c commit 7613b86

File tree

7 files changed

+542
-32
lines changed

7 files changed

+542
-32
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2015-2024 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.jupiter.params.provider;
12+
13+
import static org.junit.jupiter.params.provider.Arguments.arguments;
14+
15+
import org.junit.platform.commons.util.ReflectionUtils;
16+
17+
/**
18+
* Collection of utilities for working with {@link Arguments}.
19+
*
20+
* @since 5.11, when it was extracted from {@link MethodArgumentsProvider}
21+
*/
22+
final class ArgumentsUtils {
23+
24+
private ArgumentsUtils() {
25+
/* no-op */
26+
}
27+
28+
/**
29+
* Convert the supplied object into an {@link Arguments} instance.
30+
*/
31+
static Arguments toArguments(Object item) {
32+
// Nothing to do except cast.
33+
if (item instanceof Arguments) {
34+
return (Arguments) item;
35+
}
36+
37+
// Pass all multidimensional arrays "as is", in contrast to Object[].
38+
// See https://github.com/junit-team/junit5/issues/1665
39+
if (ReflectionUtils.isMultidimensionalArray(item)) {
40+
return arguments(item);
41+
}
42+
43+
// Special treatment for one-dimensional reference arrays.
44+
// See https://github.com/junit-team/junit5/issues/1665
45+
if (item instanceof Object[]) {
46+
return arguments((Object[]) item);
47+
}
48+
49+
// Pass everything else "as is".
50+
return arguments(item);
51+
}
52+
53+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
* Copyright 2015-2024 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.jupiter.params.provider;
12+
13+
import static java.lang.String.format;
14+
import static java.util.Arrays.stream;
15+
16+
import java.lang.reflect.Field;
17+
import java.lang.reflect.ParameterizedType;
18+
import java.lang.reflect.Type;
19+
import java.util.Iterator;
20+
import java.util.function.Predicate;
21+
import java.util.function.Supplier;
22+
import java.util.stream.DoubleStream;
23+
import java.util.stream.IntStream;
24+
import java.util.stream.LongStream;
25+
import java.util.stream.Stream;
26+
27+
import org.junit.jupiter.api.extension.ExtensionContext;
28+
import org.junit.platform.commons.JUnitException;
29+
import org.junit.platform.commons.util.ClassLoaderUtils;
30+
import org.junit.platform.commons.util.CollectionUtils;
31+
import org.junit.platform.commons.util.Preconditions;
32+
import org.junit.platform.commons.util.ReflectionUtils;
33+
import org.junit.platform.commons.util.ReflectionUtils.HierarchyTraversalMode;
34+
35+
/**
36+
* {@link ArgumentsProvider} for {@link FieldSource @FieldSource}.
37+
*
38+
* @since 5.11
39+
*/
40+
class FieldArgumentsProvider extends AnnotationBasedArgumentsProvider<FieldSource> {
41+
42+
@Override
43+
protected Stream<? extends Arguments> provideArguments(ExtensionContext context, FieldSource fieldSource) {
44+
Class<?> testClass = context.getRequiredTestClass();
45+
Object testInstance = context.getTestInstance().orElse(null);
46+
String[] fieldNames = fieldSource.value();
47+
if (fieldNames.length == 0) {
48+
fieldNames = new String[] { context.getRequiredTestMethod().getName() };
49+
}
50+
// @formatter:off
51+
return stream(fieldNames)
52+
.map(fieldName -> findField(testClass, fieldName))
53+
.map(field -> validateField(field, testInstance))
54+
.map(field -> readField(field, testInstance))
55+
.flatMap(fieldValue -> {
56+
if (fieldValue instanceof Supplier<?>) {
57+
fieldValue = ((Supplier<?>) fieldValue).get();
58+
}
59+
return CollectionUtils.toStream(fieldValue);
60+
})
61+
.map(ArgumentsUtils::toArguments);
62+
// @formatter:on
63+
}
64+
65+
// package-private for testing
66+
static Field findField(Class<?> testClass, String fieldName) {
67+
Preconditions.notBlank(fieldName, "Field name must not be blank");
68+
fieldName = fieldName.trim();
69+
70+
Class<?> clazz = testClass;
71+
if (fieldName.contains("#")) {
72+
String[] fieldParts = ReflectionUtils.parseFullyQualifiedFieldName(fieldName);
73+
String className = fieldParts[0];
74+
fieldName = fieldParts[1];
75+
ClassLoader classLoader = ClassLoaderUtils.getClassLoader(testClass);
76+
clazz = ReflectionUtils.loadRequiredClass(className, classLoader);
77+
}
78+
79+
Class<?> resolvedClass = clazz;
80+
String resolvedFieldName = fieldName;
81+
Predicate<Field> nameMatches = field -> field.getName().equals(resolvedFieldName);
82+
Field field = ReflectionUtils.streamFields(resolvedClass, nameMatches, HierarchyTraversalMode.BOTTOM_UP)//
83+
.findFirst()//
84+
.orElse(null);
85+
86+
Preconditions.notNull(field,
87+
() -> format("Could not find field named [%s] in class [%s]", resolvedFieldName, resolvedClass.getName()));
88+
return field;
89+
}
90+
91+
private static Object readField(Field field, Object testInstance) {
92+
Object value = ReflectionUtils.tryToReadFieldValue(field, testInstance).getOrThrow(
93+
cause -> new JUnitException(format("Could not read field [%s]", field.getName()), cause));
94+
95+
String fieldName = field.getName();
96+
String declaringClass = field.getDeclaringClass().getName();
97+
98+
Preconditions.notNull(value,
99+
() -> format("The value of field [%s] in class [%s] must not be null", fieldName, declaringClass));
100+
101+
boolean isStream = value instanceof Stream//
102+
|| value instanceof DoubleStream//
103+
|| value instanceof IntStream//
104+
|| value instanceof LongStream;
105+
106+
Preconditions.condition(!isStream,
107+
() -> format(
108+
"The value of field [%s] in class [%s] must not be a Stream, IntStream, LongStream, or DoubleStream",
109+
fieldName, declaringClass));
110+
111+
Preconditions.condition(!(value instanceof Iterator),
112+
() -> format("The value of field [%s] in class [%s] must not be an Iterator", fieldName, declaringClass));
113+
114+
Preconditions.condition(isConvertibleToStream(field, value),
115+
() -> format("The value of field [%s] in class [%s] must be convertible to a Stream", fieldName,
116+
declaringClass));
117+
118+
return value;
119+
}
120+
121+
/**
122+
* Determine if the supplied value can be converted into a {@code Stream} or
123+
* if the declared type of the supplied field is a {@link Supplier} of a type
124+
* that can be converted into a {@code Stream}.
125+
*/
126+
private static boolean isConvertibleToStream(Field field, Object value) {
127+
// Check actual value type.
128+
if (CollectionUtils.isConvertibleToStream(value.getClass())) {
129+
return true;
130+
}
131+
132+
// Check declared type T of Supplier<T>.
133+
if (Supplier.class.isAssignableFrom(field.getType())) {
134+
Type genericType = field.getGenericType();
135+
if (genericType instanceof ParameterizedType) {
136+
ParameterizedType parameterizedType = (ParameterizedType) genericType;
137+
Type[] typeArguments = parameterizedType.getActualTypeArguments();
138+
if (typeArguments.length == 1) {
139+
Type type = typeArguments[0];
140+
if (type instanceof Class) {
141+
Class<?> clazz = (Class<?>) type;
142+
return CollectionUtils.isConvertibleToStream(clazz);
143+
}
144+
if (type instanceof ParameterizedType) {
145+
Type rawType = ((ParameterizedType) type).getRawType();
146+
if (rawType instanceof Class<?>) {
147+
Class<?> clazz = (Class<?>) rawType;
148+
return CollectionUtils.isConvertibleToStream(clazz);
149+
}
150+
}
151+
}
152+
}
153+
}
154+
return false;
155+
}
156+
157+
private static Field validateField(Field field, Object testInstance) {
158+
Preconditions.condition(field.getDeclaringClass().isInstance(testInstance) || ReflectionUtils.isStatic(field),
159+
() -> format("Field '%s' must be static: local @FieldSource fields must be static "
160+
+ "unless the PER_CLASS @TestInstance lifecycle mode is used; "
161+
+ "external @FieldSource fields must always be static.",
162+
field.toGenericString()));
163+
return field;
164+
}
165+
166+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Copyright 2015-2024 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.jupiter.params.provider;
12+
13+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
14+
15+
import java.lang.annotation.Documented;
16+
import java.lang.annotation.ElementType;
17+
import java.lang.annotation.Retention;
18+
import java.lang.annotation.RetentionPolicy;
19+
import java.lang.annotation.Target;
20+
21+
import org.apiguardian.api.API;
22+
import org.junit.jupiter.params.ParameterizedTest;
23+
24+
/**
25+
* {@code @FieldSource} is an {@link ArgumentsSource} which provides access to
26+
* values returned from {@linkplain #value() fields} of the class in which this
27+
* annotation is declared or from static fields in external classes referenced
28+
* by <em>fully-qualified field name</em>.
29+
*
30+
* <p>Each field must be able to supply a <em>stream</em> of <em>arguments</em>,
31+
* and each set of "arguments" within the "stream" will be provided as the physical
32+
* arguments for individual invocations of the annotated
33+
* {@link ParameterizedTest @ParameterizedTest} method.
34+
*
35+
* <p>In this context, a "stream" is anything that JUnit can reliably convert to
36+
* a {@link java.util.stream.Stream Stream}; however, the actual concrete return
37+
* type can take on many forms. Generally speaking this translates to a
38+
* {@link java.util.Collection Collection}, an {@link Iterable}, a
39+
* {@link java.util.function.Supplier Supplier} of a stream
40+
* ({@link java.util.stream.Stream Stream},
41+
* {@link java.util.stream.DoubleStream DoubleStream},
42+
* {@link java.util.stream.LongStream LongStream}, or
43+
* {@link java.util.stream.IntStream IntStream}), a {@code Supplier} of an
44+
* {@link java.util.Iterator Iterator}, an array of objects, or an array of
45+
* primitives. Each set of "arguments" within the "stream" can be supplied as an
46+
* instance of {@link Arguments}, an array of objects (for example, {@code Object[]},
47+
* {@code String[]}, etc.), or a single <em>value</em> if the parameterized test
48+
* method accepts a single argument.
49+
*
50+
* <p>In contrast to the supported return types for {@link MethodSource @MethodSource}
51+
* factory methods, the value of a {@code @FieldSource} field cannot be an instance of
52+
* {@link java.util.stream.Stream Stream},
53+
* {@link java.util.stream.DoubleStream DoubleStream},
54+
* {@link java.util.stream.LongStream LongStream},
55+
* {@link java.util.stream.IntStream IntStream}, or
56+
* {@link java.util.Iterator Iterator}, since the values of such types are
57+
* <em>consumed</em> the first time they are processed. However, if you wish to
58+
* use one of these types, you can wrap it in a {@code Supplier} &mdash; for
59+
* example, {@code Supplier<IntStream>}.
60+
*
61+
* <p>Please note that a one-dimensional array of objects supplied as a set of
62+
* "arguments" will be handled differently than other types of arguments.
63+
* Specifically, all of the elements of a one-dimensional array of objects will
64+
* be passed as individual physical arguments to the {@code @ParameterizedTest}
65+
* method. This behavior can be seen in the table below for the
66+
* {@code Supplier<Stream<Object[]>> objectArrayStreamSupplier} field: the
67+
* {@code @ParameterizedTest} method accepts individual {@code String} and
68+
* {@code int} arguments rather than a single {@code Object[]} array. In contrast,
69+
* any multidimensional array supplied as a set of "arguments" will be passed as
70+
* a single physical argument to the {@code @ParameterizedTest} method without
71+
* modification. This behavior can be seen in the table below for the
72+
* {@code Supplier<Stream<int[][]>> twoDimensionalIntArrayStreamSupplier} and
73+
* {@code Supplier<Stream<Object[][]>> twoDimensionalObjectArrayStreamSupplier}
74+
* fields: the {@code @ParameterizedTest} methods for those fields accept individual
75+
* {@code int[][]} and {@code Object[][]} arguments, respectively.
76+
*
77+
* <h2>Examples</h2>
78+
*
79+
* <p>The following table displays compatible method signatures for parameterized
80+
* test methods and their corresponding {@code @FieldSource} fields.
81+
*
82+
* <table class="plain">
83+
* <caption>Compatible method signatures and field declarations</caption>
84+
* <tr><th>{@code @ParameterizedTest} method</th><th>{@code @FieldSource} field</th></tr>
85+
* <tr><td>{@code void test(String)}</td><td>{@code static List<String> listOfStrings}</td></tr>
86+
* <tr><td>{@code void test(String)}</td><td>{@code static String[] arrayOfStrings}</td></tr>
87+
* <tr><td>{@code void test(int)}</td><td>{@code static int[] intArray}</td></tr>
88+
* <tr><td>{@code void test(int[])}</td><td>{@code static int[][] twoDimensionalIntArray}</td></tr>
89+
* <tr><td>{@code void test(String, String)}</td><td>{@code static String[][] twoDimensionalStringArray}</td></tr>
90+
* <tr><td>{@code void test(String, int)}</td><td>{@code static Object[][] twoDimensionalObjectArray}</td></tr>
91+
* <tr><td>{@code void test(int)}</td><td>{@code static Supplier<IntStream> intStreamSupplier}</td></tr>
92+
* <tr><td>{@code void test(String)}</td><td>{@code static Supplier<Stream<String>> stringStreamSupplier}</td></tr>
93+
* <tr><td>{@code void test(String, int)}</td><td>{@code static Supplier<Stream<Object[]>> objectArrayStreamSupplier}</td></tr>
94+
* <tr><td>{@code void test(String, int)}</td><td>{@code static Supplier<Stream<Arguments>> argumentsStreamSupplier}</td></tr>
95+
* <tr><td>{@code void test(int[])}</td><td>{@code static Supplier<Stream<int[]>> intArrayStreamSupplier}</td></tr>
96+
* <tr><td>{@code void test(int[][])}</td><td>{@code static Supplier<Stream<int[][]>> twoDimensionalIntArrayStreamSupplier}</td></tr>
97+
* <tr><td>{@code void test(Object[][])}</td><td>{@code static Supplier<Stream<Object[][]>> twoDimensionalObjectArrayStreamSupplier}</td></tr>
98+
* </table>
99+
*
100+
* <p>Fields within the test class must be {@code static} unless the
101+
* {@link org.junit.jupiter.api.TestInstance.Lifecycle#PER_CLASS PER_CLASS}
102+
* test instance lifecycle mode is used; whereas, fields in external classes must
103+
* always be {@code static}.
104+
*
105+
* @since 5.11
106+
* @see MethodSource
107+
* @see Arguments
108+
* @see ArgumentsSource
109+
* @see ParameterizedTest
110+
* @see org.junit.jupiter.api.TestInstance
111+
*/
112+
@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD })
113+
@Retention(RetentionPolicy.RUNTIME)
114+
@Documented
115+
@API(status = EXPERIMENTAL, since = "5.11")
116+
@ArgumentsSource(FieldArgumentsProvider.class)
117+
@SuppressWarnings("exports")
118+
public @interface FieldSource {
119+
120+
/**
121+
* The names of fields within the test class or in external classes to use
122+
* as sources for arguments.
123+
*
124+
* <p>Fields in external classes must be referenced by <em>fully-qualified
125+
* field name</em> &mdash; for example,
126+
* {@code "com.example.WebUtils#httpMethodNames"} or
127+
* {@code "com.example.TopLevelClass$NestedClass#numbers"} for a field in a
128+
* static nested class.
129+
*
130+
* <p>If no field names are declared, a field within the test class that has
131+
* the same name as the test method will be used as the field by default.
132+
*
133+
* <p>For further information, see the {@linkplain FieldSource class-level Javadoc}.
134+
*/
135+
String[] value() default {};
136+
137+
}

0 commit comments

Comments
 (0)