diff --git a/documentation/modules/ROOT/partials/release-notes/release-notes-6.1.0-M2.adoc b/documentation/modules/ROOT/partials/release-notes/release-notes-6.1.0-M2.adoc index f55ef4a580f6..b18088de9484 100644 --- a/documentation/modules/ROOT/partials/release-notes/release-notes-6.1.0-M2.adoc +++ b/documentation/modules/ROOT/partials/release-notes/release-notes-6.1.0-M2.adoc @@ -47,6 +47,13 @@ repository on GitHub. * https://www.junit-pioneer.org/[JUnit Pioneer]'s `DefaultLocaleExtension` and `DefaultTimeZoneExtension` are now part of the JUnit Jupiter. Find examples in the xref:writing-tests/built-in-extensions.adoc#DefaultLocaleAndTimeZone[User Guide]. +* Support the creation of `Arguments` from iterables. These additions make it +easier to dynamically build arguments from collections when using +`@ParameterizedTest`: +** `Arguments.of(Iterable>)` +** `Arguments.argumentsFrom(Iterable>)` (alias) +** `Arguments.argumentSetFrom(String, Iterable>)` +** `Arguments.toList()` — returns a mutable `List<@Nullable Object>` [[v6.1.0-M2-junit-vintage]] === JUnit Vintage diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/Arguments.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/Arguments.java index 9b11b7a84f92..007c6c09d129 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/Arguments.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/Arguments.java @@ -10,9 +10,15 @@ package org.junit.jupiter.params.provider; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.MAINTAINED; import static org.apiguardian.api.API.Status.STABLE; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import org.junit.platform.commons.util.Preconditions; @@ -26,10 +32,11 @@ * * @apiNote
This interface is specifically designed as a simple holder of * arguments for a parameterized test. Therefore, if you end up - * {@linkplain java.util.stream.Stream#map(java.util.function.Function) transforming} or + * {@linkplain java.util.stream.Stream#map(java.util.function.Function) transforming} + * or * {@linkplain java.util.stream.Stream#filter(java.util.function.Predicate) filtering} - * the arguments, you should consider using one of the following in intermediate - * steps: + * the arguments, you should consider using one of the following in + * intermediate steps: * *
This is useful for test logic that benefits from {@code List} + * operations such as filtering, transformation, or assertions. + * + * @return a mutable List of arguments; never {@code null} but may contain + * {@code null} + * @since 6.1 + */ + @API(status = EXPERIMENTAL, since = "6.1") + default List<@Nullable Object> toList() { + // We could return List> here but the unbounded wildcard is painful + // to work with. + return new ArrayList<>(Arrays.asList(get())); + } + /** * Factory method for creating an instance of {@code Arguments} based on * the supplied {@code arguments}. @@ -73,6 +98,7 @@ public interface Arguments { * @param arguments the arguments to be used for an invocation of the test * method; must not be {@code null} but may contain {@code null} * @return an instance of {@code Arguments}; never {@code null} + * @see #from(Iterable) * @see #arguments(Object...) * @see #argumentSet(String, Object...) */ @@ -81,6 +107,35 @@ static Arguments of(@Nullable Object... arguments) { return () -> arguments; } + /** + * Factory method for creating an instance of {@code Arguments} based on + * the supplied {@link Iterable} of {@code arguments}. + * + *
The iterable supplied to this method should be a finite collection + * and have a reliable iteration order to provide arguments in a consistent + * order to tests. It is therefore recommended that the iterable be a + * {@link java.util.SequencedCollection} (on Java 21 or higher), + * {@link java.util.List}, or similar. + * + * @param arguments the arguments to be used for an invocation of the test + * method; must not be {@code null} but may contain {@code null} + * @return an instance of {@code Arguments}; never {@code null} + * @since 6.1 + * @see #argumentsFrom(Iterable) + */ + @API(status = EXPERIMENTAL, since = "6.1") + static Arguments from(Iterable> arguments) { + Preconditions.notNull(arguments, "arguments must not be null"); + + if (arguments instanceof Collection> collection) { + return of(collection.toArray()); + } + + var collection = new ArrayList<>(); + arguments.forEach(collection::add); + return of(collection.toArray()); + } + /** * Factory method for creating an instance of {@code Arguments} based on * the supplied {@code arguments}. @@ -94,45 +149,118 @@ static Arguments of(@Nullable Object... arguments) { * @return an instance of {@code Arguments}; never {@code null} * @since 5.3 * @see #argumentSet(String, Object...) + * @see #argumentsFrom(Iterable) */ static Arguments arguments(@Nullable Object... arguments) { return of(arguments); } + /** + * Factory method for creating an instance of {@code Arguments} based on + * the supplied {@link Iterable} of {@code arguments}. + * + *
This method is an alias for {@link Arguments#from} and is + * intended to be used when statically imported — for example, via: + * {@code import static org.junit.jupiter.params.provider.Arguments.argumentsFrom;} + * + *
The iterable supplied to this method should be a finite collection + * and have a reliable iteration order to provide arguments in a consistent + * order to tests. It is therefore recommended that the iterable be a + * {@link java.util.SequencedCollection} (on Java 21 or higher), + * {@link java.util.List}, or similar. + * + * @param arguments the arguments to be used for an invocation of the test + * method; must not be {@code null} but may contain {@code null} + * @return an instance of {@code Arguments}; never {@code null} + * @since 6.1 + * @see #arguments(Object...) + * @see #argumentSetFrom(String, Iterable) + */ + @API(status = EXPERIMENTAL, since = "6.1") + static Arguments argumentsFrom(Iterable> arguments) { + return from(arguments); + } + /** * Factory method for creating an {@link ArgumentSet} based on the supplied * {@code name} and {@code arguments}. * *
Favor this method over {@link Arguments#of Arguments.of(...)} and - * {@link Arguments#arguments arguments(...)} when you wish to assign a name - * to the entire set of arguments. + * {@link Arguments#arguments arguments(...)} when you wish to assign a + * name to the entire set of arguments. * *
This method is well suited to be used as a static import — for * example, via: * {@code import static org.junit.jupiter.params.provider.Arguments.argumentSet;}. * - * @param name the name of the argument set; must not be {@code null} or blank + * @param name the name of the argument set; must not be {@code null} or + * blank * @param arguments the arguments to be used for an invocation of the test * method; must not be {@code null} but may contain {@code null} * @return an {@code ArgumentSet}; never {@code null} * @since 5.11 * @see ArgumentSet - * @see org.junit.jupiter.params.ParameterizedTest#ARGUMENT_SET_NAME_PLACEHOLDER - * @see org.junit.jupiter.params.ParameterizedTest#ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER + * @see #argumentSetFrom(String, Iterable) + * @see org.junit.jupiter.params.ParameterizedInvocationConstants#ARGUMENT_SET_NAME_PLACEHOLDER + * @see org.junit.jupiter.params.ParameterizedInvocationConstants#ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER */ @API(status = MAINTAINED, since = "5.13.3") static ArgumentSet argumentSet(String name, @Nullable Object... arguments) { return new ArgumentSet(name, arguments); } + /** + * Factory method for creating an {@link ArgumentSet} based on the supplied + * {@code name} and {@link Iterable} of {@code arguments}. + * + *
Favor this method over {@link Arguments#from(Iterable) Arguments.from(...)} and + * {@link Arguments#argumentsFrom(Iterable) argumentsFrom(...)} when you wish to assign a + * name to the entire set of arguments. + * + *
This method is well suited to be used as a static import — for + * example, via: + * {@code import static org.junit.jupiter.params.provider.Arguments.argumentSetFrom;}. + * + *
The iterable supplied to this method should be a finite collection + * and have a reliable iteration order to provide arguments in a consistent + * order to tests. It is therefore recommended that the iterable be a + * {@link java.util.SequencedCollection} (on Java 21 or higher), + * {@link java.util.List}, or similar. + * + * @param name the name of the argument set; must not be {@code null} + * or blank + * @param arguments the arguments to be used for an invocation of the test + * method; must not be {@code null} but may contain {@code null} + * @return an {@code ArgumentSet}; never {@code null} + * @since 6.1 + * @see ArgumentSet + * @see #argumentSet(String, Object...) + * @see org.junit.jupiter.params.ParameterizedInvocationConstants#ARGUMENT_SET_NAME_PLACEHOLDER + * @see org.junit.jupiter.params.ParameterizedInvocationConstants#ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER + */ + @API(status = EXPERIMENTAL, since = "6.1") + static ArgumentSet argumentSetFrom(String name, Iterable> arguments) { + Preconditions.notBlank(name, "name must not be null or blank"); + Preconditions.notNull(arguments, "arguments list must not be null"); + + if (arguments instanceof Collection> collection) { + return argumentSet(name, collection.toArray()); + } + + var collection = new ArrayList<>(); + arguments.forEach(collection::add); + return argumentSet(name, collection.toArray()); + } + /** * Specialization of {@link Arguments} that associates a {@link #getName() name} * with a set of {@link #get() arguments}. * * @since 5.11 * @see Arguments#argumentSet(String, Object...) - * @see org.junit.jupiter.params.ParameterizedTest#ARGUMENT_SET_NAME_PLACEHOLDER - * @see org.junit.jupiter.params.ParameterizedTest#ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER + * @see Arguments#argumentSetFrom(String, Iterable) + * @see org.junit.jupiter.params.ParameterizedInvocationConstants#ARGUMENT_SET_NAME_PLACEHOLDER + * @see org.junit.jupiter.params.ParameterizedInvocationConstants#ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER */ @API(status = MAINTAINED, since = "5.13.3") final class ArgumentSet implements Arguments { @@ -150,7 +278,8 @@ private ArgumentSet(String name, @Nullable Object[] arguments) { /** * Get the name of this {@code ArgumentSet}. - * @return the name of this {@code ArgumentSet}; never {@code null} or blank + * @return the name of this {@code ArgumentSet}; never {@code null} or + * blank */ public String getName() { return this.name; diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/ArgumentsTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/ArgumentsTests.java index 76dd591f7101..3027c72d6109 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/ArgumentsTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/ArgumentsTests.java @@ -10,11 +10,18 @@ package org.junit.jupiter.params.provider; +import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.junit.jupiter.params.provider.Arguments.of; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; + +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; /** @@ -56,4 +63,115 @@ void argumentsReturnsSameArrayUsedForCreating() { assertThat(arguments.get()).isSameAs(input); } + @Test + void fromSupportsCollection() { + Collection<@Nullable Object> input = Arrays.asList(1, "two", null, 3.0); + var arguments = Arguments.from(input); + + assertArrayEquals(new Object[] { 1, "two", null, 3.0 }, arguments.get()); + } + + @Test + void fromSupportsIterable() { + var input = new IterableWithNullableElements(1, "two", null, 3.0); + var arguments = Arguments.from(input); + + assertArrayEquals(new Object[] { 1, "two", null, 3.0 }, arguments.get()); + } + + @Test + void fromSupportsListDefensiveCopy() { + var input = new ArrayList<@Nullable Object>(asList(1, "two", null, 3.0)); + var arguments = Arguments.from(input); + + // Modify input + input.set(1, "changed"); + input.add("new"); + + // Assert that arguments are unchanged + assertArrayEquals(new Object[] { 1, "two", null, 3.0 }, arguments.get()); + } + + @Test + void argumentsFromSupportsCollection() { + Collection<@Nullable Object> input = asList("a", 2, null); + var arguments = Arguments.argumentsFrom(input); + + assertArrayEquals(new Object[] { "a", 2, null }, arguments.get()); + } + + @Test + void argumentsFromSupportsIterable() { + var input = new IterableWithNullableElements("a", 2, null); + var arguments = Arguments.argumentsFrom(input); + + assertArrayEquals(new Object[] { "a", 2, null }, arguments.get()); + } + + @Test + void argumentSetSupportsCollection() { + Collection<@Nullable Object> input = asList("x", null, 42); + var argumentSet = Arguments.argumentSetFrom("list-test", input); + + assertArrayEquals(new Object[] { "x", null, 42 }, argumentSet.get()); + assertThat(argumentSet.getName()).isEqualTo("list-test"); + } + + @Test + void argumentSetSupportsIterable() { + var input = new IterableWithNullableElements("x", null, 42); + var argumentSet = Arguments.argumentSetFrom("list-test", input); + + assertArrayEquals(new Object[] { "x", null, 42 }, argumentSet.get()); + assertThat(argumentSet.getName()).isEqualTo("list-test"); + } + + @Test + void toListReturnsMutableListOfArguments() { + var arguments = Arguments.of("a", 2, null); + + var result = arguments.toList(); + + assertThat(result).containsExactly("a", 2, null); // preserves content + result.add("extra"); // confirms mutability + assertThat(result).contains("extra"); + } + + @Test + void toListDoesNotAffectInternalArgumentsState() { + var arguments = Arguments.of("a", 2, null); + + var result = arguments.toList(); + result.add("extra"); // mutate the returned list + + // Confirm that internal state was not modified + var freshCopy = arguments.toList(); + assertThat(freshCopy).containsExactly("a", 2, null); + } + + @Test + void toListWorksOnEmptyArguments() { + var arguments = Arguments.of(); + + var result = arguments.toList(); + + assertThat(result).isEmpty(); + result.add("extra"); + assertThat(result).containsExactly("extra"); + } + + private static final class IterableWithNullableElements implements Iterable<@Nullable Object> { + + private final Collection<@Nullable Object> collection; + + private IterableWithNullableElements(@Nullable Object... items) { + this.collection = asList(items); + } + + @Override + public Iterator<@Nullable Object> iterator() { + return collection.iterator(); + } + } + }