Skip to content

Commit

Permalink
Merge @⁠FieldSource feature into main
Browse files Browse the repository at this point in the history
This set of commits 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.

Closes #2014
  • Loading branch information
sbrannen committed Apr 14, 2024
2 parents 73548a9 + 06cfd08 commit f51e235
Show file tree
Hide file tree
Showing 13 changed files with 1,594 additions and 36 deletions.
1 change: 1 addition & 0 deletions documentation/src/docs/asciidoc/link-attributes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ endif::[]
:ArgumentsAggregator: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/aggregator/ArgumentsAggregator.html[ArgumentsAggregator]
:CsvArgumentsProvider: {junit5-repo}/blob/main/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvArgumentsProvider.java[CsvArgumentsProvider]
:EmptySource: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/EmptySource.html[@EmptySource]
:FieldSource: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/FieldSource.html[@FieldSource]
:MethodSource: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/MethodSource.html[@MethodSource]
:NullAndEmptySource: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/NullAndEmptySource.html[@NullAndEmptySource]
:NullSource: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/NullSource.html[@NullSource]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@

*Date of Release:* ❓

*Scope:* ❓
*Scope:*

* Numerous bug fixes and enhancements regarding field and method search algorithms
* `@FieldSource` annotation for use with `@ParameterizedTest` methods
* `@AutoClose` annotation to automatically close field resources in tests
* `ConversionSupport` utility for converting from a string to a supported target type

For a complete list of all _closed_ issues and pull requests for this release, consult the
link:{junit5-repo}+/milestone/68?closed=1+[5.11.0-M1] milestone page in the JUnit
Expand Down Expand Up @@ -77,13 +82,19 @@ repository on GitHub.
[[release-notes-5.11.0-M1-junit-jupiter-new-features-and-improvements]]
==== New Features and Improvements

* `JAVA_23` has been added to the `JRE` enum for use with JRE-based execution conditions.
* Improved documentation for semantics of a disabled test regarding class-level lifecycle
methods and callbacks.
* New `@FieldSource` annotation for use with `@ParameterizedTest` methods which allows
you to source arguments from a local field or an external field referenced by
fully-qualified field name. This feature is similar to the existing `@MethodSource`
feature. See the
<<../user-guide/index.adoc#writing-tests-parameterized-tests-sources-FieldSource, User
Guide>> for details.
* New `@AutoClose` annotation that can be applied to fields within tests to automatically
close the annotated resource after test execution. See the
<<../user-guide/index.adoc#writing-tests-built-in-extensions-AutoClose, User Guide>> for
details.
* `JAVA_23` has been added to the `JRE` enum for use with JRE-based execution conditions.
* Improved documentation for semantics of a disabled test regarding class-level lifecycle
methods and callbacks.


[[release-notes-5.11.0-M1-junit-vintage]]
Expand Down
113 changes: 113 additions & 0 deletions documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1512,6 +1512,119 @@ can be referenced by its fully qualified method name, e.g.
include::{testDir}/example/MethodSourceParameterResolutionDemo.java[tags=parameter_resolution_MethodSource_example]
----

[[writing-tests-parameterized-tests-sources-FieldSource]]
===== @FieldSource

`{FieldSource}` allows you to refer to one or more fields of the test class or external
classes.

Fields within the test class must be `static` unless the test class is annotated with
`@TestInstance(Lifecycle.PER_CLASS)`; whereas, fields in external classes must always be
`static`.

Each field must be able to supply a _stream_ of arguments, and each set of "arguments"
within the "stream" will be provided as the physical arguments for individual invocations
of the annotated `@ParameterizedTest` method.

In this context, a "stream" is anything that JUnit can reliably convert to a `Stream`;
however, the actual concrete return type can take on many forms. Generally speaking this
translates to a `Collection`, an `Iterable`, a `Supplier` of a stream (`Stream`,
`DoubleStream`, `LongStream`, or `IntStream`), a `Supplier` of an `Iterator`, an array of
objects, or an array of primitives. Each set of "arguments" within the "stream" can be
supplied as an instance of `Arguments`, an array of objects (for example, `Object[]`,
`String[]`, etc.), or a single value if the parameterized test method accepts a single
argument.

[WARNING]
====
In contrast to the supported return types for
<<writing-tests-parameterized-tests-sources-MethodSource, `@MethodSource`>> factory
methods, the value of a `@FieldSource` field cannot be an instance of `Stream`,
`DoubleStream`, `LongStream`, `IntStream`, or `Iterator`, since the values of such types
are _consumed_ the first time they are processed. However, if you wish to use one of
these types, you can wrap it in a `Supplier` — for example, `Supplier<IntStream>`.
====

Please note that a one-dimensional array of objects supplied as a set of "arguments" will
be handled differently than other types of arguments. Specifically, all of the elements
of a one-dimensional array of objects will be passed as individual physical arguments to
the `@ParameterizedTest` method. See the Javadoc for `{FieldSource}` for further details.

If you do not explicitly provide a field name via `@FieldSource`, JUnit Jupiter will
search in the test class for a field that has the same name as the current
`@ParameterizedTest` method by convention. This is demonstrated in the following example.
This parameterized test method will be invoked twice: with the values `"apple"` and
`"banana"`.

[source,java,indent=0]
----
include::{testDir}/example/ParameterizedTestDemo.java[tags=default_field_FieldSource_example]
----

The following example demonstrates how to provide a single explicit field name via
`@FieldSource`. This parameterized test method will be invoked twice: with the values
`"apple"` and `"banana"`.

[source,java,indent=0]
----
include::{testDir}/example/ParameterizedTestDemo.java[tags=explicit_field_FieldSource_example]
----

The following example demonstrates how to provide multiple explicit field names via
`@FieldSource`. This example uses the `listOfFruits` field from the previous example as
well as the `additionalFruits` field. Consequently, this parameterized test method will
be invoked four times: with the values `"apple"`, `"banana"`, `"cherry"`, and
`"dewberry"`.

[source,java,indent=0]
----
include::{testDir}/example/ParameterizedTestDemo.java[tags=multiple_fields_FieldSource_example]
----

It is also possible to provide a `Stream`, `DoubleStream`, `IntStream`, `LongStream`, or
`Iterator` as the source of arguments via a `@FieldSource` field as long as the stream is
wrapped in a `java.util.function.Supplier`. The following example demonstrates how to
provide a `Supplier` of a `Stream` of named arguments. This parameterized test method
will be invoked twice: with the values `"apple"` and `"banana"` and with display names
`Apple` and `Banana`, respectively.

[source,java,indent=0]
----
include::{testDir}/example/ParameterizedTestDemo.java[tags=named_arguments_FieldSource_example]
----

[NOTE]
====
Note that `arguments(Object...)` is a static factory method defined in the
`org.junit.jupiter.params.provider.Arguments` interface.
Similarly, `named(String, String)` is a static factory method defined in the
`org.junit.jupiter.api.Named` interface.
====

If a parameterized test method declares multiple parameters, the corresponding
`@FieldSource` field must be able to provide a collection, stream supplier, or array of
`Arguments` instances or object arrays as shown below (see the Javadoc for
`{FieldSource}` for further details on supported types).

[source,java,indent=0]
----
include::{testDir}/example/ParameterizedTestDemo.java[tags=multi_arg_FieldSource_example]
----

[NOTE]
====
Note that `arguments(Object...)` is a static factory method defined in the
`org.junit.jupiter.params.provider.Arguments` interface.
====

An external, `static` `@FieldSource` field can be referenced by providing its
_fully-qualified field name_ as demonstrated in the following example.

[source,java,indent=0]
----
include::{testDir}/example/ExternalFieldSourceDemo.java[tags=external_field_FieldSource_example]
----

[[writing-tests-parameterized-tests-sources-CsvSource]]
===== @CsvSource
Expand Down
35 changes: 35 additions & 0 deletions documentation/src/test/java/example/ExternalFieldSourceDemo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2015-2024 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package example;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.FieldSource;

class ExternalFieldSourceDemo {

// tag::external_field_FieldSource_example[]
@ParameterizedTest
@FieldSource("example.FruitUtils#tropicalFruits")
void testWithExternalFieldSource(String tropicalFruit) {
// test with tropicalFruit
}
// end::external_field_FieldSource_example[]
}

class FruitUtils {

public static final List<String> tropicalFruits = Collections.unmodifiableList(Arrays.asList("pineapple", "kiwi"));

}
75 changes: 75 additions & 0 deletions documentation/src/test/java/example/ParameterizedTestDemo.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.IntStream;
import java.util.stream.Stream;

Expand Down Expand Up @@ -61,6 +63,7 @@
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.EmptySource;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.FieldSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.NullSource;
Expand Down Expand Up @@ -206,6 +209,78 @@ static Stream<Arguments> stringIntAndListProvider() {
// end::multi_arg_MethodSource_example[]
// @formatter:on

// @formatter:off
// tag::default_field_FieldSource_example[]
@ParameterizedTest
@FieldSource
void arrayOfFruits(String fruit) {
assertFruit(fruit);
}

static final String[] arrayOfFruits = { "apple", "banana" };
// end::default_field_FieldSource_example[]
// @formatter:on

// @formatter:off
// tag::explicit_field_FieldSource_example[]
@ParameterizedTest
@FieldSource("listOfFruits")
void singleFieldSource(String fruit) {
assertFruit(fruit);
}

static final List<String> listOfFruits = Arrays.asList("apple", "banana");
// end::explicit_field_FieldSource_example[]
// @formatter:on

// @formatter:off
// tag::multiple_fields_FieldSource_example[]
@ParameterizedTest
@FieldSource({ "listOfFruits", "additionalFruits" })
void multipleFieldSources(String fruit) {
assertFruit(fruit);
}

static final Collection<String> additionalFruits = Arrays.asList("cherry", "dewberry");
// end::multiple_fields_FieldSource_example[]
// @formatter:on

// @formatter:off
// tag::named_arguments_FieldSource_example[]
@ParameterizedTest
@FieldSource
void namedArgumentsSupplier(String fruit) {
assertFruit(fruit);
}

static final Supplier<Stream<Arguments>> namedArgumentsSupplier = () -> Stream.of(
arguments(named("Apple", "apple")),
arguments(named("Banana", "banana"))
);
// end::named_arguments_FieldSource_example[]
// @formatter:on

private static void assertFruit(String fruit) {
assertTrue(Arrays.asList("apple", "banana", "cherry", "dewberry").contains(fruit));
}

// @formatter:off
// tag::multi_arg_FieldSource_example[]
@ParameterizedTest
@FieldSource("stringIntAndListArguments")
void testWithMultiArgFieldSource(String str, int num, List<String> list) {
assertEquals(5, str.length());
assertTrue(num >=1 && num <=2);
assertEquals(2, list.size());
}

static List<Arguments> stringIntAndListArguments = Arrays.asList(
arguments("apple", 1, Arrays.asList("a", "b")),
arguments("lemon", 2, Arrays.asList("x", "y"))
);
// end::multi_arg_FieldSource_example[]
// @formatter:on

// @formatter:off
// tag::CsvSource_example[]
@ParameterizedTest
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2015-2024 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.jupiter.params.provider;

import static org.junit.jupiter.params.provider.Arguments.arguments;

import org.junit.platform.commons.util.ReflectionUtils;

/**
* Collection of utilities for working with {@link Arguments}.
*
* @since 5.11, when it was extracted from {@link MethodArgumentsProvider}
*/
final class ArgumentsUtils {

private ArgumentsUtils() {
/* no-op */
}

/**
* Convert the supplied object into an {@link Arguments} instance.
*/
static Arguments toArguments(Object item) {
// Nothing to do except cast.
if (item instanceof Arguments) {
return (Arguments) item;
}

// Pass all multidimensional arrays "as is", in contrast to Object[].
// See https://github.com/junit-team/junit5/issues/1665
if (ReflectionUtils.isMultidimensionalArray(item)) {
return arguments(item);
}

// Special treatment for one-dimensional reference arrays.
// See https://github.com/junit-team/junit5/issues/1665
if (item instanceof Object[]) {
return arguments((Object[]) item);
}

// Pass everything else "as is".
return arguments(item);
}

}
Loading

0 comments on commit f51e235

Please sign in to comment.