Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,10 +32,11 @@
*
* @apiNote <p>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:
*
* <ul>
* <li>The standard Java collections</li>
Expand Down Expand Up @@ -58,21 +65,40 @@ public interface Arguments {
* Get the arguments used for an invocation of the
* {@code @ParameterizedTest} method.
*
* @apiNote If you need a type-safe way to access some or all of the arguments,
* please read the {@linkplain Arguments class-level API note}.
* @apiNote If you need a type-safe way to access some or all of the
* arguments, please read the {@linkplain Arguments class-level API note}.
*
* @return the arguments; never {@code null} but may contain {@code null}
*/
@Nullable
Object[] get();

/**
* Convert the arguments to a new mutable {@link List} containing the same
* elements as {@link #get()}.
*
* <p>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}.
*
* @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...)
*/
Expand All @@ -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}.
*
* <p>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}.
Expand All @@ -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}.
*
* <p>This method is an <em>alias</em> for {@link Arguments#from} and is
* intended to be used when statically imported &mdash; for example, via:
* {@code import static org.junit.jupiter.params.provider.Arguments.argumentsFrom;}
*
* <p>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}.
*
* <p>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.
*
* <p>This method is well suited to be used as a static import &mdash; 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}.
*
* <p>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.
*
* <p>This method is well suited to be used as a static import &mdash; for
* example, via:
* {@code import static org.junit.jupiter.params.provider.Arguments.argumentSetFrom;}.
*
* <p>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 {
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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();
}
}

}