Skip to content
Merged
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 @@ -45,7 +45,8 @@ repository on GitHub.
[[release-notes-5.13.0-RC1-junit-jupiter-new-features-and-improvements]]
==== New Features and Improvements

* ❓
* Add support for Kotlin `Sequence` to `@MethodSource`, `@FieldSource`, and
`@TestFactory`.


[[release-notes-5.13.0-RC1-junit-vintage]]
Expand Down
24 changes: 15 additions & 9 deletions documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1949,10 +1949,11 @@ of the annotated `@ParameterizedClass` or `@ParameterizedTest`. Generally speaki
translates to a `Stream` of `Arguments` (i.e., `Stream<Arguments>`); however, the actual
concrete return type can take on many forms. In this context, a "stream" is anything that
JUnit can reliably convert into a `Stream`, such as `Stream`, `DoubleStream`,
`LongStream`, `IntStream`, `Collection`, `Iterator`, `Iterable`, an array of objects, or
an array of primitives. The "arguments" within the stream can be supplied as an instance
of `Arguments`, an array of objects (e.g., `Object[]`), or a single value if the
parameterized class or test method accepts a single argument.
`LongStream`, `IntStream`, `Collection`, `Iterator`, `Iterable`, an array of objects or
primitives, or any type that provides an `iterator(): Iterator` method (such as, for
example, a `kotlin.sequences.Sequence`). The "arguments" within the stream can be supplied
as an instance of `Arguments`, an array of objects (e.g., `Object[]`), or a single value
if the parameterized class or test method accepts a single argument.

If the return type is `Stream` or one of the primitive streams,
JUnit will properly close it by calling `BaseStream.close()`,
Expand Down Expand Up @@ -2038,10 +2039,11 @@ In this context, a "stream" is anything that JUnit can reliably convert to a `St
however, the actual concrete field 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 class or test method accepts a
single argument.
objects or primitives, or any type that provides an `iterator(): Iterator` method (such
as, for example, a `kotlin.sequences.Sequence`). 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 class ortest method accepts
a single argument.

[WARNING]
====
Expand Down Expand Up @@ -2870,7 +2872,11 @@ generated at runtime by a factory method that is annotated with `@TestFactory`.
In contrast to `@Test` methods, a `@TestFactory` method is not itself a test case but
rather a factory for test cases. Thus, a dynamic test is the product of a factory.
Technically speaking, a `@TestFactory` method must return a single `DynamicNode` or a
`Stream`, `Collection`, `Iterable`, `Iterator`, or array of `DynamicNode` instances.
_stream_ of `DynamicNode` instances or any of its subclasses. In this context, a "stream"
is anything that JUnit can reliably convert into a `Stream`, such as `Stream`,
`Collection`, `Iterator`, `Iterable`, an array of objects, or any type that provides an
`iterator(): Iterator` method (such as, for example, a `kotlin.sequences.Sequence`).

Instantiable subclasses of `DynamicNode` are `DynamicContainer` and `DynamicTest`.
`DynamicContainer` instances are composed of a _display name_ and a list of dynamic child
nodes, enabling the creation of arbitrarily nested hierarchies of dynamic nodes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
*
* <p>{@code @TestFactory} methods must not be {@code private} or {@code static}
* and must return a {@code Stream}, {@code Collection}, {@code Iterable},
* {@code Iterator}, or array of {@link DynamicNode} instances. Supported
* {@code Iterator}, array of {@link DynamicNode} instances, or any type that
* provides an {@link java.util.Iterator Iterator}-returning {@code iterator()}
* method (such as, for example, a {@code kotlin.sequences.Sequence}). Supported
* subclasses of {@code DynamicNode} include {@link DynamicContainer} and
* {@link DynamicTest}. <em>Dynamic tests</em> will be executed lazily,
* enabling dynamic and even non-deterministic generation of test cases.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
public class IsTestFactoryMethod extends IsTestableMethod {

private static final String EXPECTED_RETURN_TYPE_MESSAGE = String.format(
"must return a single %1$s or a Stream, Collection, Iterable, Iterator, or array of %1$s",
"must return a single %1$s or a Stream, Collection, Iterable, Iterator, Iterator provider, or array of %1$s",
DynamicNode.class.getName());

public IsTestFactoryMethod(DiscoveryIssueReporter issueReporter) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@
* {@link java.util.stream.DoubleStream DoubleStream},
* {@link java.util.stream.LongStream LongStream}, or
* {@link java.util.stream.IntStream IntStream}), a {@code Supplier} of an
* {@link java.util.Iterator Iterator}, an array of objects, or an array of
* primitives. Each set of "arguments" within the "stream" can be supplied as an
* instance of {@link Arguments}, an array of objects (for example, {@code Object[]},
* {@code String[]}, etc.), or a single <em>value</em> if the parameterized
* class or test accepts a single argument.
* {@link java.util.Iterator Iterator}, an array of objects or primitives, or
* any type that provides an {@link java.util.Iterator Iterator}-returning
* {@code iterator()} method (such as, for example, a
* {@code kotlin.sequences.Sequence}). Each set of "arguments" within the
* "stream" can be supplied as an instance of {@link Arguments}, an array of
* objects (for example, {@code Object[]}, {@code String[]}, etc.), or a single
* <em>value</em> if the parameterized class or test accepts a single argument.
*
* <p>In contrast to the supported return types for {@link MethodSource @MethodSource}
* factory methods, the value of a {@code @FieldSource} field cannot be an instance of
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,13 @@
* {@link java.util.stream.LongStream LongStream},
* {@link java.util.stream.IntStream IntStream},
* {@link java.util.Collection Collection},
* {@link java.util.Iterator Iterator},
* {@link Iterable}, an array of objects, or an array of primitives. Each set of
* "arguments" within the "stream" can be supplied as an instance of
* {@link Arguments}, an array of objects (e.g., {@code Object[]},
* {@code String[]}, etc.), or a single <em>value</em> if the parameterized test
* method accepts a single argument.
* {@link java.util.Iterator Iterator}, an array of objects or primitives, or
* any type that provides an {@link java.util.Iterator Iterator}-returning
* {@code iterator()} method (such as, for example, a
* {@code kotlin.sequences.Sequence}). Each set of "arguments" within the
* "stream" can be supplied as an instance of {@link Arguments}, an array of
* objects (e.g., {@code Object[]}, {@code String[]}, etc.), or a single
* <em>value</em> if the parameterized test method accepts a single argument.
*
* <p>If the return type is {@code Stream} or
* one of the primitive streams, JUnit will properly close it by calling
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
import static java.util.stream.Collectors.toList;
import static java.util.stream.StreamSupport.stream;
import static org.apiguardian.api.API.Status.INTERNAL;
import static org.junit.platform.commons.support.ReflectionSupport.invokeMethod;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
Expand All @@ -36,6 +38,7 @@

import org.apiguardian.api.API;
import org.junit.platform.commons.PreconditionViolationException;
import org.junit.platform.commons.support.ReflectionSupport;

/**
* Collection of utilities for working with {@link Collection Collections}.
Expand Down Expand Up @@ -122,7 +125,7 @@ public static <T> Set<T> toSet(T[] values) {
* returned, so if more control over the returned list is required,
* consider creating a new {@code Collector} implementation like the
* following:
*
* <p>
* <pre class="code">
* public static &lt;T&gt; Collector&lt;T, ?, List&lt;T&gt;&gt; toUnmodifiableList(Supplier&lt;List&lt;T&gt;&gt; listSupplier) {
* return Collectors.collectingAndThen(Collectors.toCollection(listSupplier), Collections::unmodifiableList);
Expand Down Expand Up @@ -161,7 +164,8 @@ public static boolean isConvertibleToStream(Class<?> type) {
|| Iterable.class.isAssignableFrom(type)//
|| Iterator.class.isAssignableFrom(type)//
|| Object[].class.isAssignableFrom(type)//
|| (type.isArray() && type.getComponentType().isPrimitive()));
|| (type.isArray() && type.getComponentType().isPrimitive())//
|| findIteratorMethod(type).isPresent());
}

/**
Expand All @@ -177,6 +181,9 @@ public static boolean isConvertibleToStream(Class<?> type) {
* <li>{@link Iterator}</li>
* <li>{@link Object} array</li>
* <li>primitive array</li>
* <li>any type that provides an
* {@link java.util.Iterator Iterator}-returning {@code iterator()} method
* (such as, for example, a {@code kotlin.sequences.Sequence})</li>
* </ul>
*
* @param object the object to convert into a stream; never {@code null}
Expand Down Expand Up @@ -223,8 +230,21 @@ public static Stream<?> toStream(Object object) {
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);
return tryConvertToStreamByReflection(object);
}

private static Stream<?> tryConvertToStreamByReflection(Object object) {
return findIteratorMethod(object.getClass()) //
.map(method -> (Iterator<?>) invokeMethod(method, object)) //
.map(iterator -> spliteratorUnknownSize(iterator, ORDERED)) //
.map(spliterator -> stream(spliterator, false)) //
.orElseThrow(() -> new PreconditionViolationException(String.format(
"Cannot convert instance of %s into a Stream: %s", object.getClass().getName(), object)));
}

private static Optional<Method> findIteratorMethod(Class<?> type) {
return ReflectionSupport.findMethod(type, "iterator") //
.filter(method -> method.getReturnType() == Iterator.class);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ void invalidFactoryMethods(String methodName) {
var issue = getOnlyElement(discoveryIssues);
assertThat(issue.severity()).isEqualTo(DiscoveryIssue.Severity.WARNING);
assertThat(issue.message()).isEqualTo(
"@TestFactory method '%s' must return a single org.junit.jupiter.api.DynamicNode or a Stream, Collection, Iterable, Iterator, or array of org.junit.jupiter.api.DynamicNode. "
"@TestFactory method '%s' must return a single org.junit.jupiter.api.DynamicNode or a "
+ "Stream, Collection, Iterable, Iterator, Iterator provider, or array of org.junit.jupiter.api.DynamicNode. "
+ "It will not be executed.",
method.toGenericString());
assertThat(issue.source()).contains(MethodSource.from(method));
Expand All @@ -83,7 +84,8 @@ void suspiciousFactoryMethods(String methodName) {
assertThat(issue.severity()).isEqualTo(DiscoveryIssue.Severity.INFO);
assertThat(issue.message()).isEqualTo(
"The declared return type of @TestFactory method '%s' does not support static validation. "
+ "It must return a single org.junit.jupiter.api.DynamicNode or a Stream, Collection, Iterable, Iterator, or array of org.junit.jupiter.api.DynamicNode.",
+ "It must return a single org.junit.jupiter.api.DynamicNode or a "
+ "Stream, Collection, Iterable, Iterator, Iterator provider, or array of org.junit.jupiter.api.DynamicNode.",
method.toGenericString());
assertThat(issue.source()).contains(MethodSource.from(method));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2015-2025 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.api

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DynamicTest.dynamicTest
import java.math.BigDecimal
import java.math.BigDecimal.ONE
import java.math.MathContext
import java.math.BigInteger as BigInt
import java.math.RoundingMode as Rounding

/**
* Unit tests for JUnit Jupiter [TestFactory] use in kotlin classes.
*
* @since 5.12
*/
class KotlinDynamicTests {
@Nested
inner class SequenceReturningTestFactoryTests {
@TestFactory
fun `Dynamic tests returned as Kotlin sequence`() =
generateSequence(0) { it + 2 }
.map { dynamicTest("$it should be even") { assertEquals(0, it % 2) } }
.take(10)

@TestFactory
fun `Consecutive fibonacci nr ratios, should converge to golden ratio as n increases`(): Sequence<DynamicTest> {
val scale = 5
val goldenRatio =
(ONE + 5.toBigDecimal().sqrt(MathContext(scale + 10, Rounding.HALF_UP)))
.divide(2.toBigDecimal(), scale, Rounding.HALF_UP)

fun shouldApproximateGoldenRatio(
cur: BigDecimal,
next: BigDecimal
) = next.divide(cur, scale, Rounding.HALF_UP).let {
dynamicTest("$cur / $next = $it should approximate the golden ratio in $scale decimals") {
assertEquals(goldenRatio, it)
}
}
return generateSequence(BigInt.ONE to BigInt.ONE) { (cur, next) -> next to cur + next }
.map { (cur) -> cur.toBigDecimal() }
.zipWithNext(::shouldApproximateGoldenRatio)
.drop(14)
.take(10)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2015-2025 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

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.params.provider.Arguments.arguments
import org.junit.jupiter.params.provider.FieldSource
import org.junit.jupiter.params.provider.MethodSource
import java.time.Month

/**
* Tests for Kotlin compatibility of ParameterizedTest
*/
object ParameterizedTestKotlinSequenceIntegrationTests {
@ParameterizedTest
@MethodSource("dataProvidedByKotlinSequenceMethod")
fun `a method source can be supplied by a Sequence-returning method`(
value: Int,
month: Month
) {
assertEquals(value, month.value)
}

@JvmStatic
private fun dataProvidedByKotlinSequenceMethod() = dataProvidedByKotlinSequenceField

@JvmStatic
val dataProvidedByKotlinSequenceField =
sequenceOf(
arguments(1, Month.JANUARY),
arguments(3, Month.MARCH),
arguments(8, Month.AUGUST),
arguments(5, Month.MAY),
arguments(12, Month.DECEMBER)
)

@ParameterizedTest
@FieldSource("dataProvidedByKotlinSequenceField")
fun `a field source can be supplied by a Sequence-typed field`(
value: Int,
month: Month
) {
assertEquals(value, month.value)
}
}
Loading