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

Introduce DynamicTests generators for Named<Executable> #3720

Merged
merged 7 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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 @@ -30,6 +30,9 @@ repository on GitHub.
-- for use in third-party extensions and test engines.
* Error messages for type mismatches in `NamespacedHierarchicalStore` now include the
actual type and value in addition to the required type.
* New `NamedExecutable` interface that associates a name, a payload and an Executable.
* New DynamicTests generators in DynamicTest class that takes one Stream/Iterator of NamedExecutables,
this helps simplify the generation of DynamicTests Stream.


[[release-notes-5.11.0-M1-junit-jupiter]]
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 org.apiguardian.api.API;
import org.junit.jupiter.api.function.Executable;

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

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

/**
* {@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,18 @@ 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);
}
}

}