Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add @CartesianTest.MethodParameterSource #797

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ The least we can do is to thank them and list some of their accomplishments here

==== 2024

* https://github.com/jpenilla[Jason Penilla] added https://junit-pioneer.org/docs/cartesian-product/#cartesiantest-methodparametersource[`@CartesianTest.MethodParameterSource`] (#796 / #797)
* https://github.com/TWiStErRob[Papp Róbert (TWiStErRob)] updated Gradle and GitHub Actions tooling to latest. (#803 / #804 / #805)

==== 2023
Expand Down
37 changes: 36 additions & 1 deletion docs/cartesian-product.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,41 @@ To demonstrate with a table:

For more information, please see the link:/docs/range-sources[separate documentation about range sources].

=== `@CartesianTest.MethodParameterSource`

`@CartesianTest.MethodParameterSource` can be used to specify factory methods that supply arguments for a single parameter of a `@CartesianTest`.

Similar to https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests-sources-MethodSource[JUnit's `@MethodSource`], methods can be specified with their simple name when in the same class, (i.e. `methodName`), or their fully qualified name otherwise (i.e. `com.example.Class#methodName`).
Methods must be static, except for methods in the same class when the test class is annotated with `@TestInstance(Lifecycle.PER_CLASS)`.
Methods may return a `Stream` (including the primitive-specialized variants), `Iterable`, `Iterator`, or array of values.

Let's look at an example.

[source,java,indent=0]
----
include::{demo}[tag=cartesian_parameter_source_basic]
----

In this example, the test method would be invoked 12 times.
To demonstrate with a table:

|===
| # of test | value of `name` | value of `points`

| 1st test | "John" | 12
| 2nd test | "John" | 18
| 3rd test | "John" | 22
| 4th test | "Annie" | 12
| 5th test | "Annie" | 18
| 6th test | "Annie" | 22
| 7th test | "Bob" | 12
| 8th test | "Bob" | 18
| 9th test | "Bob" | 22
| 10th test | "Sofia" | 12
| 11th test | "Sofia" | 18
| 12th test | "Sofia" | 22
|===

== Defining arguments with factories

You can annotate your test method to supply arguments to all parameters simultaneously.
Expand All @@ -205,7 +240,7 @@ You can annotate your test method to supply arguments to all parameters simultan

`@CartesianTest.MethodFactory` can be used to name a factory method that supplies your arguments.
The `value` annotation parameter is mandatory.
Just like with JUnit's `@MethodSource`, you can specify the factory method with its fully-qualified name (including the class), e.g. `com.example.Class#factory`.
Just like with https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests-sources-MethodSource[JUnit's `@MethodSource`], you can specify the factory method with its fully-qualified name (including the class), e.g. `com.example.Class#factory`.
This method must return `ArgumentSets`.

`ArgumentSets` is a helper class, specifically for creating sets for `@CartesianTest`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@
import java.time.temporal.TemporalUnit;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.junit.jupiter.api.DisplayName;
Expand All @@ -36,6 +38,7 @@
import org.junit.jupiter.api.TestReporter;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junitpioneer.jupiter.cartesian.CartesianTest.Enum;
import org.junitpioneer.jupiter.cartesian.CartesianTest.MethodParameterSource;
import org.junitpioneer.jupiter.cartesian.CartesianTest.Values;
import org.junitpioneer.jupiter.params.LongRangeSource;
import org.junitpioneer.jupiter.params.ShortRangeSource;
Expand Down Expand Up @@ -179,6 +182,21 @@ static ArgumentSets stringIntFactory() {
}
// end::cartesian_argument_sets_reuse[]

// tag::cartesian_parameter_source_basic[]
@CartesianTest
void testScores(@MethodParameterSource("names") String name, @MethodParameterSource("points") int points) {
// test code
}

static List<String> names() {
return List.of("John", "Annie", "Bob", "Sofia");
}

static IntStream points() {
return IntStream.of(12, 18, 22);
}
// end::cartesian_parameter_source_basic[]

// these tests fail intentionally ~> no @Nested
static class MisconfiguredExamples {

Expand Down
15 changes: 15 additions & 0 deletions src/main/java/org/junitpioneer/internal/PioneerPreconditions.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,19 @@ public static <T extends Collection<?>> T notEmpty(T collection, Supplier<String
return collection;
}

/**
* Assert that the supplied {@code predicate} is {@code true}.
*
* @param predicate the predicate to check
* @param messageSupplier precondition violation message supplier
* @throws PreconditionViolationException if the predicate is {@code false}
*/
public static void condition(boolean predicate, Supplier<String> messageSupplier)
throws PreconditionViolationException {

if (!predicate) {
throw new PreconditionViolationException(messageSupplier.get());
}
}

}
115 changes: 115 additions & 0 deletions src/main/java/org/junitpioneer/internal/PioneerUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,31 @@

package org.junitpioneer.internal;

import static java.util.Spliterator.ORDERED;
import static java.util.Spliterators.spliteratorUnknownSize;
import static org.junit.platform.commons.support.ReflectionSupport.findMethod;

import java.lang.invoke.MethodType;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collector;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.platform.commons.PreconditionViolationException;

/**
* Pioneer-internal utility class.
Expand Down Expand Up @@ -175,4 +186,108 @@ public static Locale createLocale(String language) {
return new Locale.Builder().setLanguage(language).build();
}

/**
* Determine if an instance of the supplied type can be converted into a
* {@code Stream}.
*
* <p>If this method returns {@code true}, {@link #toStream(Object)} can
* successfully convert an object of the specified type into a stream. See
* {@link #toStream(Object)} for supported types.
*
* <p>Based on the method with the same name in org.junit.platform.commons.util.CollectionUtils</p>
*
* @param type the type to check; may be {@code null}
* @return {@code true} if an instance of the type can be converted into a stream
* @see #toStream(Object)
*/
public static boolean isConvertibleToStream(Class<?> type) {
if (type == null || type == void.class) {
return false;
}
return (Stream.class.isAssignableFrom(type)//
|| DoubleStream.class.isAssignableFrom(type)//
|| IntStream.class.isAssignableFrom(type)//
|| LongStream.class.isAssignableFrom(type)//
|| Iterable.class.isAssignableFrom(type)//
|| Iterator.class.isAssignableFrom(type)//
|| Object[].class.isAssignableFrom(type)//
|| (type.isArray() && type.getComponentType().isPrimitive()));
}

/**
* Convert an object of one of the following supported types into a {@code Stream}.
*
* <ul>
* <li>{@link Stream}</li>
* <li>{@link DoubleStream}</li>
* <li>{@link IntStream}</li>
* <li>{@link LongStream}</li>
* <li>{@link Collection}</li>
* <li>{@link Iterable}</li>
* <li>{@link Iterator}</li>
* <li>{@link Object} array</li>
* <li>primitive array</li>
* </ul>
*
* <p>Based on the method with the same name in org.junit.platform.commons.util.CollectionUtils</p>
*
* @param object the object to convert into a stream; never {@code null}
* @return the resulting stream
* @throws PreconditionViolationException if the supplied object is {@code null}
* or not one of the supported types
* @see #isConvertibleToStream(Class)
*/
public static Stream<?> toStream(Object object) {
PioneerPreconditions.notNull(object, "Object must not be null");
if (object instanceof Stream) {
return (Stream<?>) object;
}
if (object instanceof DoubleStream) {
return ((DoubleStream) object).boxed();
}
if (object instanceof IntStream) {
return ((IntStream) object).boxed();
}
if (object instanceof LongStream) {
return ((LongStream) object).boxed();
}
if (object instanceof Collection) {
return ((Collection<?>) object).stream();
}
if (object instanceof Iterable) {
return StreamSupport.stream(((Iterable<?>) object).spliterator(), false);
}
if (object instanceof Iterator) {
return StreamSupport.stream(spliteratorUnknownSize((Iterator<?>) object, ORDERED), false);
}
if (object instanceof Object[]) {
return Arrays.stream((Object[]) object);
}
if (object instanceof double[]) {
return DoubleStream.of((double[]) object).boxed();
}
if (object instanceof int[]) {
return IntStream.of((int[]) object).boxed();
}
if (object instanceof long[]) {
return LongStream.of((long[]) object).boxed();
}
if (object.getClass().isArray() && object.getClass().getComponentType().isPrimitive()) {
return IntStream.range(0, Array.getLength(object)).mapToObj(i -> Array.get(object, i));
}
throw new PreconditionViolationException(
"Cannot convert instance of " + object.getClass().getName() + " into a Stream: " + object);
}

/**
* Determine if the supplied {@link String} is <em>blank</em> (i.e.,
* {@code null} or consisting only of whitespace characters).
*
* @param str the string to check; may be {@code null}
* @return {@code true} if the string is blank
*/
public static boolean isBlank(String str) {
return (str == null || str.trim().isEmpty());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -323,4 +323,29 @@ void validate(CartesianTest.Enum enumSource, Set<? extends java.lang.Enum<?>> co

}

/**
* Analogue to {@link org.junit.jupiter.params.provider.MethodSource},
* but for {@link CartesianTest}. Provides values for a single parameter
* from factory methods.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
@CartesianArgumentsSource(MethodParameterProvider.class)
@interface MethodParameterSource {

/**
* Methods that back this source.
*
* <p>Methods may be referenced by their simple name if in the same class,
* or by fully qualified name otherwise (i.e. {@code a.b.c.SomeClass#someMethod}).</p>
*
* <p>Methods should return a {@link java.util.stream.Stream}, {@link Iterable}, {@link java.util.Iterator},
* or array of values.</p>
*
* @return method strings
*/
String[] value();

}

}