From 6828a799b64c88f28e11c31d2eda04707c4e3983 Mon Sep 17 00:00:00 2001 From: Mohamed amine Bounya Date: Thu, 18 Jul 2024 07:35:10 +0100 Subject: [PATCH] Introduce DynamicTests generators for Named (#3720) Introduce two methods in `DynamicTest` to generate a `Stream` of `DynamicTest` from a `Stream`/`Iterator` of `Named`. 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 --- .../release-notes-5.11.0-RC1.adoc | 3 + .../test/java/example/DynamicTestsDemo.java | 42 +++++++++++-- .../org/junit/jupiter/api/DynamicTest.java | 62 +++++++++++++++++++ .../junit/jupiter/api/NamedExecutable.java | 48 ++++++++++++++ .../junit/jupiter/api/DynamicTestTests.java | 46 ++++++++++++++ 5 files changed, 195 insertions(+), 6 deletions(-) create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/NamedExecutable.java diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-RC1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-RC1.adoc index 0ae91bdda350..0965e8d4e92a 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-RC1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-RC1.adoc @@ -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` + 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]] diff --git a/documentation/src/test/java/example/DynamicTestsDemo.java b/documentation/src/test/java/example/DynamicTestsDemo.java index e7e91d7a64e2..eb31750af91b 100644 --- a/documentation/src/test/java/example/DynamicTestsDemo.java +++ b/documentation/src/test/java/example/DynamicTestsDemo.java @@ -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; @@ -157,17 +158,47 @@ Stream dynamicTestsFromStreamFactoryMethod() { Stream dynamicTestsFromStreamFactoryMethodWithNames() { // Stream of palindromes to check Stream> 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 dynamicTestsFromStreamFactoryMethodWithNamedExecutables() { + // Stream of palindromes to check + Stream 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 dynamicTestsWithContainers() { return Stream.of("A", "B", "C") @@ -192,6 +223,5 @@ DynamicNode dynamicNodeSingleContainer() { .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))) )); } - } // end::user_guide[] diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicTest.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicTest.java index 87f4aeae166a..b61ccb71a57a 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicTest.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicTest.java @@ -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; @@ -226,6 +227,67 @@ public static Stream stream(Stream> inputStr .map(input -> dynamicTest(input.getName(), () -> testExecutor.accept(input.getPayload()))); } + /** + * Generate a stream of dynamic tests based on the given iterator. + * + *

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. + * + *

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 the type of input 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 , E extends Executable> Stream stream( + Iterator 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. + * + *

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. + * + *

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 the type of input 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 , E extends Executable> Stream stream( + Stream 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) { diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/NamedExecutable.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/NamedExecutable.java new file mode 100644 index 000000000000..16d5a72d2d18 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/NamedExecutable.java @@ -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. + * + *

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. + * + *

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 { + @Override + default String getName() { + return toString(); + } + + @Override + default Executable getPayload() { + return this; + } +} diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/DynamicTestTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/DynamicTestTests.java index 4fc95d19ca0f..1244dc697104 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/DynamicTestTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/DynamicTestTests.java @@ -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; @@ -90,6 +91,18 @@ void streamFromIteratorWithNamesPreconditions() { assertThrows(PreconditionViolationException.class, () -> DynamicTest.stream(emptyIterator(), null)); } + @Test + void streamFromStreamWithNamedExecutablesPreconditions() { + assertThrows(PreconditionViolationException.class, + () -> DynamicTest.stream((Stream) null)); + } + + @Test + void streamFromIteratorWithNamedExecutablesPreconditions() { + assertThrows(PreconditionViolationException.class, + () -> DynamicTest.stream((Iterator) null)); + } + @Test void streamFromStream() throws Throwable { Stream stream = DynamicTest.stream(Stream.of("foo", "bar", "baz"), String::toUpperCase, @@ -119,6 +132,26 @@ void streamFromIteratorWithNames() throws Throwable { assertStream(stream); } + @Test + void streamFromStreamWithNamedExecutables() throws Throwable { + Stream 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 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 stream) throws Throwable { List dynamicTests = stream.collect(Collectors.toList()); @@ -200,4 +233,17 @@ private void assert1Equals50Reflectively() throws Throwable { method.invoke(null, 1, 50); } + record DummyNamedExecutableForTests(String name, ThrowingConsumer consumer) implements NamedExecutable { + + @Override + public String getName() { + return name.toUpperCase(Locale.ROOT); + } + + @Override + public void execute() throws Throwable { + consumer.accept(name); + } + } + }