Skip to content

Commit

Permalink
Introduce DynamicTests generators for Named<Executable> (#3720)
Browse files Browse the repository at this point in the history
Introduce two methods in `DynamicTest` to generate a `Stream` of 
`DynamicTest` from a `Stream`/`Iterator` of `Named<Executable>`.

The new `NamedExecutable` interface provides default implementations of 
`getName()` and `getPayload()` so only `execute()` has to be implemented
and is particularly well-suited to be implemented by a Java record 
class.

Resolves #3261.

---------

Co-authored-by: Marc Philipp <[email protected]>
  • Loading branch information
Mohamed amine Bounya and marcphilipp authored Jul 18, 2024
1 parent 8e8268c commit 6828a79
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ repository on GitHub.
* `JAVA_24` has been added to the `JRE` enum for use with JRE-based execution conditions.
* New `assertInstanceOf` methods added for Kotlin following up with similar Java
`assertInstanceOf` methods introduced in `5.8` version.
* New generators in `DynamicTest` that take a `Stream`/`Iterator` of `Named<Executable>`
along with a convenient `NamedExecutable` interface that can simplify writing dynamic
tests, in particular in recent version of Java that support records.


[[release-notes-5.11.0-RC1-junit-vintage]]
Expand Down
42 changes: 36 additions & 6 deletions documentation/src/test/java/example/DynamicTestsDemo.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.NamedExecutable;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.function.ThrowingConsumer;
Expand Down Expand Up @@ -157,17 +158,47 @@ Stream<DynamicTest> dynamicTestsFromStreamFactoryMethod() {
Stream<DynamicTest> dynamicTestsFromStreamFactoryMethodWithNames() {
// Stream of palindromes to check
Stream<Named<String>> inputStream = Stream.of(
named("racecar is a palindrome", "racecar"),
named("radar is also a palindrome", "radar"),
named("mom also seems to be a palindrome", "mom"),
named("dad is yet another palindrome", "dad")
);
named("racecar is a palindrome", "racecar"),
named("radar is also a palindrome", "radar"),
named("mom also seems to be a palindrome", "mom"),
named("dad is yet another palindrome", "dad")
);

// Returns a stream of dynamic tests.
return DynamicTest.stream(inputStream,
text -> assertTrue(isPalindrome(text)));
}

@TestFactory
Stream<DynamicTest> dynamicTestsFromStreamFactoryMethodWithNamedExecutables() {
// Stream of palindromes to check
Stream<PalindromeNamedExecutable> inputStream = Stream.of("racecar", "radar", "mom", "dad")
.map(PalindromeNamedExecutable::new);

// Returns a stream of dynamic tests based on NamedExecutables.
return DynamicTest.stream(inputStream);
}

// Can be a record in Java 16 and later
static class PalindromeNamedExecutable implements NamedExecutable {

private final String text;

public PalindromeNamedExecutable(String text) {
this.text = text;
}

@Override
public String getName() {
return String.format("'%s' is a palindrome", text);
}

@Override
public void execute() {
assertTrue(isPalindrome(text));
}
}

@TestFactory
Stream<DynamicNode> dynamicTestsWithContainers() {
return Stream.of("A", "B", "C")
Expand All @@ -192,6 +223,5 @@ DynamicNode dynamicNodeSingleContainer() {
.map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text)))
));
}

}
// end::user_guide[]
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import static java.util.Spliterator.ORDERED;
import static java.util.Spliterators.spliteratorUnknownSize;
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
import static org.apiguardian.api.API.Status.MAINTAINED;

import java.net.URI;
Expand Down Expand Up @@ -226,6 +227,67 @@ public static <T> Stream<DynamicTest> stream(Stream<? extends Named<T>> inputStr
.map(input -> dynamicTest(input.getName(), () -> testExecutor.accept(input.getPayload())));
}

/**
* Generate a stream of dynamic tests based on the given iterator.
*
* <p>Use this method when the set of dynamic tests is nondeterministic in
* nature or when the input comes from an existing {@link Iterator}. See
* {@link #stream(Stream)} as an alternative.
*
* <p>The given {@code iterator} is responsible for supplying
* {@link Named} input values that provide an {@link Executable} code block.
* A {@link DynamicTest} comprised of both parts will be added to the
* resulting stream for each dynamically supplied input value.
*
* @param iterator an {@code Iterator} that supplies named executables;
* never {@code null}
* @param <T> the type of <em>input</em> supplied by the {@code inputStream}
* @return a stream of dynamic tests based on the given iterator; never
* {@code null}
* @since 5.11
* @see #dynamicTest(String, Executable)
* @see #stream(Stream)
* @see NamedExecutable
*/
@API(status = EXPERIMENTAL, since = "5.11")
public static <T extends Named<E>, E extends Executable> Stream<DynamicTest> stream(
Iterator<? extends T> iterator) {
Preconditions.notNull(iterator, "iterator must not be null");

return stream(StreamSupport.stream(spliteratorUnknownSize(iterator, ORDERED), false));
}

/**
* Generate a stream of dynamic tests based on the given input stream.
*
* <p>Use this method when the set of dynamic tests is nondeterministic in
* nature or when the input comes from an existing {@link Stream}. See
* {@link #stream(Iterator)} as an alternative.
*
* <p>The given {@code inputStream} is responsible for supplying
* {@link Named} input values that provide an {@link Executable} code block.
* A {@link DynamicTest} comprised of both parts will be added to the
* resulting stream for each dynamically supplied input value.
*
* @param inputStream a {@code Stream} that supplies named executables;
* never {@code null}
* @param <T> the type of <em>input</em> supplied by the {@code inputStream}
* @return a stream of dynamic tests based on the given stream; never
* {@code null}
* @since 5.11
* @see #dynamicTest(String, Executable)
* @see #stream(Iterator)
* @see NamedExecutable
*/
@API(status = EXPERIMENTAL, since = "5.11")
public static <T extends Named<E>, E extends Executable> Stream<DynamicTest> stream(
Stream<? extends T> inputStream) {
Preconditions.notNull(inputStream, "inputStream must not be null");

return inputStream. //
map(input -> dynamicTest(input.getName(), input.getPayload()));
}

private final Executable executable;

private DynamicTest(String displayName, URI testSourceUri, Executable executable) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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.api;

import static org.apiguardian.api.API.Status.EXPERIMENTAL;

import java.util.Iterator;
import java.util.stream.Stream;

import org.apiguardian.api.API;
import org.junit.jupiter.api.function.Executable;

/**
* {@code NamedExecutable} joins {@code Executable} and {@code Named} in a
* one self-typed functional interface.
*
* <p>The default implementation of {@link #getName()} returns the result of
* calling {@link Object#toString()} on the implementing instance but may be
* overridden by concrete implementations to provide a more meaningful name.
*
* <p>On Java 16 or later, it is recommended to implement this interface using
* a record type.
*
* @since 5.11
* @see DynamicTest#stream(Stream)
* @see DynamicTest#stream(Iterator)
*/
@FunctionalInterface
@API(status = EXPERIMENTAL, since = "5.11")
public interface NamedExecutable extends Named<Executable>, Executable {
@Override
default String getName() {
return toString();
}

@Override
default Executable getPayload() {
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand Down Expand Up @@ -90,6 +91,18 @@ void streamFromIteratorWithNamesPreconditions() {
assertThrows(PreconditionViolationException.class, () -> DynamicTest.stream(emptyIterator(), null));
}

@Test
void streamFromStreamWithNamedExecutablesPreconditions() {
assertThrows(PreconditionViolationException.class,
() -> DynamicTest.stream((Stream<DummyNamedExecutableForTests>) null));
}

@Test
void streamFromIteratorWithNamedExecutablesPreconditions() {
assertThrows(PreconditionViolationException.class,
() -> DynamicTest.stream((Iterator<DummyNamedExecutableForTests>) null));
}

@Test
void streamFromStream() throws Throwable {
Stream<DynamicTest> stream = DynamicTest.stream(Stream.of("foo", "bar", "baz"), String::toUpperCase,
Expand Down Expand Up @@ -119,6 +132,26 @@ void streamFromIteratorWithNames() throws Throwable {
assertStream(stream);
}

@Test
void streamFromStreamWithNamedExecutables() throws Throwable {
Stream<DynamicTest> stream = DynamicTest.stream(
Stream.of(new DummyNamedExecutableForTests("foo", this::throwingConsumer),
new DummyNamedExecutableForTests("bar", this::throwingConsumer),
new DummyNamedExecutableForTests("baz", this::throwingConsumer)));

assertStream(stream);
}

@Test
void streamFromIteratorWithNamedExecutables() throws Throwable {
Stream<DynamicTest> stream = DynamicTest.stream(
List.of(new DummyNamedExecutableForTests("foo", this::throwingConsumer),
new DummyNamedExecutableForTests("bar", this::throwingConsumer),
new DummyNamedExecutableForTests("baz", this::throwingConsumer)).iterator());

assertStream(stream);
}

private void assertStream(Stream<DynamicTest> stream) throws Throwable {
List<DynamicTest> dynamicTests = stream.collect(Collectors.toList());

Expand Down Expand Up @@ -200,4 +233,17 @@ private void assert1Equals50Reflectively() throws Throwable {
method.invoke(null, 1, 50);
}

record DummyNamedExecutableForTests(String name, ThrowingConsumer<String> consumer) implements NamedExecutable {

@Override
public String getName() {
return name.toUpperCase(Locale.ROOT);
}

@Override
public void execute() throws Throwable {
consumer.accept(name);
}
}

}

0 comments on commit 6828a79

Please sign in to comment.