Skip to content

Commit 6828a79

Browse files
Mohamed amine Bounyamarcphilipp
andauthored
Introduce DynamicTests generators for Named<Executable> (#3720)
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 <mail@marcphilipp.de>
1 parent 8e8268c commit 6828a79

File tree

5 files changed

+195
-6
lines changed

5 files changed

+195
-6
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-RC1.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ repository on GitHub.
5555
* `JAVA_24` has been added to the `JRE` enum for use with JRE-based execution conditions.
5656
* New `assertInstanceOf` methods added for Kotlin following up with similar Java
5757
`assertInstanceOf` methods introduced in `5.8` version.
58+
* New generators in `DynamicTest` that take a `Stream`/`Iterator` of `Named<Executable>`
59+
along with a convenient `NamedExecutable` interface that can simplify writing dynamic
60+
tests, in particular in recent version of Java that support records.
5861

5962

6063
[[release-notes-5.11.0-RC1-junit-vintage]]

documentation/src/test/java/example/DynamicTestsDemo.java

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.junit.jupiter.api.DynamicNode;
3636
import org.junit.jupiter.api.DynamicTest;
3737
import org.junit.jupiter.api.Named;
38+
import org.junit.jupiter.api.NamedExecutable;
3839
import org.junit.jupiter.api.Tag;
3940
import org.junit.jupiter.api.TestFactory;
4041
import org.junit.jupiter.api.function.ThrowingConsumer;
@@ -157,17 +158,47 @@ Stream<DynamicTest> dynamicTestsFromStreamFactoryMethod() {
157158
Stream<DynamicTest> dynamicTestsFromStreamFactoryMethodWithNames() {
158159
// Stream of palindromes to check
159160
Stream<Named<String>> inputStream = Stream.of(
160-
named("racecar is a palindrome", "racecar"),
161-
named("radar is also a palindrome", "radar"),
162-
named("mom also seems to be a palindrome", "mom"),
163-
named("dad is yet another palindrome", "dad")
164-
);
161+
named("racecar is a palindrome", "racecar"),
162+
named("radar is also a palindrome", "radar"),
163+
named("mom also seems to be a palindrome", "mom"),
164+
named("dad is yet another palindrome", "dad")
165+
);
165166

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

172+
@TestFactory
173+
Stream<DynamicTest> dynamicTestsFromStreamFactoryMethodWithNamedExecutables() {
174+
// Stream of palindromes to check
175+
Stream<PalindromeNamedExecutable> inputStream = Stream.of("racecar", "radar", "mom", "dad")
176+
.map(PalindromeNamedExecutable::new);
177+
178+
// Returns a stream of dynamic tests based on NamedExecutables.
179+
return DynamicTest.stream(inputStream);
180+
}
181+
182+
// Can be a record in Java 16 and later
183+
static class PalindromeNamedExecutable implements NamedExecutable {
184+
185+
private final String text;
186+
187+
public PalindromeNamedExecutable(String text) {
188+
this.text = text;
189+
}
190+
191+
@Override
192+
public String getName() {
193+
return String.format("'%s' is a palindrome", text);
194+
}
195+
196+
@Override
197+
public void execute() {
198+
assertTrue(isPalindrome(text));
199+
}
200+
}
201+
171202
@TestFactory
172203
Stream<DynamicNode> dynamicTestsWithContainers() {
173204
return Stream.of("A", "B", "C")
@@ -192,6 +223,5 @@ DynamicNode dynamicNodeSingleContainer() {
192223
.map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text)))
193224
));
194225
}
195-
196226
}
197227
// end::user_guide[]

junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicTest.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import static java.util.Spliterator.ORDERED;
1414
import static java.util.Spliterators.spliteratorUnknownSize;
15+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
1516
import static org.apiguardian.api.API.Status.MAINTAINED;
1617

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

230+
/**
231+
* Generate a stream of dynamic tests based on the given iterator.
232+
*
233+
* <p>Use this method when the set of dynamic tests is nondeterministic in
234+
* nature or when the input comes from an existing {@link Iterator}. See
235+
* {@link #stream(Stream)} as an alternative.
236+
*
237+
* <p>The given {@code iterator} is responsible for supplying
238+
* {@link Named} input values that provide an {@link Executable} code block.
239+
* A {@link DynamicTest} comprised of both parts will be added to the
240+
* resulting stream for each dynamically supplied input value.
241+
*
242+
* @param iterator an {@code Iterator} that supplies named executables;
243+
* never {@code null}
244+
* @param <T> the type of <em>input</em> supplied by the {@code inputStream}
245+
* @return a stream of dynamic tests based on the given iterator; never
246+
* {@code null}
247+
* @since 5.11
248+
* @see #dynamicTest(String, Executable)
249+
* @see #stream(Stream)
250+
* @see NamedExecutable
251+
*/
252+
@API(status = EXPERIMENTAL, since = "5.11")
253+
public static <T extends Named<E>, E extends Executable> Stream<DynamicTest> stream(
254+
Iterator<? extends T> iterator) {
255+
Preconditions.notNull(iterator, "iterator must not be null");
256+
257+
return stream(StreamSupport.stream(spliteratorUnknownSize(iterator, ORDERED), false));
258+
}
259+
260+
/**
261+
* Generate a stream of dynamic tests based on the given input stream.
262+
*
263+
* <p>Use this method when the set of dynamic tests is nondeterministic in
264+
* nature or when the input comes from an existing {@link Stream}. See
265+
* {@link #stream(Iterator)} as an alternative.
266+
*
267+
* <p>The given {@code inputStream} is responsible for supplying
268+
* {@link Named} input values that provide an {@link Executable} code block.
269+
* A {@link DynamicTest} comprised of both parts will be added to the
270+
* resulting stream for each dynamically supplied input value.
271+
*
272+
* @param inputStream a {@code Stream} that supplies named executables;
273+
* never {@code null}
274+
* @param <T> the type of <em>input</em> supplied by the {@code inputStream}
275+
* @return a stream of dynamic tests based on the given stream; never
276+
* {@code null}
277+
* @since 5.11
278+
* @see #dynamicTest(String, Executable)
279+
* @see #stream(Iterator)
280+
* @see NamedExecutable
281+
*/
282+
@API(status = EXPERIMENTAL, since = "5.11")
283+
public static <T extends Named<E>, E extends Executable> Stream<DynamicTest> stream(
284+
Stream<? extends T> inputStream) {
285+
Preconditions.notNull(inputStream, "inputStream must not be null");
286+
287+
return inputStream. //
288+
map(input -> dynamicTest(input.getName(), input.getPayload()));
289+
}
290+
229291
private final Executable executable;
230292

231293
private DynamicTest(String displayName, URI testSourceUri, Executable executable) {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2015-2024 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.jupiter.api;
12+
13+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
14+
15+
import java.util.Iterator;
16+
import java.util.stream.Stream;
17+
18+
import org.apiguardian.api.API;
19+
import org.junit.jupiter.api.function.Executable;
20+
21+
/**
22+
* {@code NamedExecutable} joins {@code Executable} and {@code Named} in a
23+
* one self-typed functional interface.
24+
*
25+
* <p>The default implementation of {@link #getName()} returns the result of
26+
* calling {@link Object#toString()} on the implementing instance but may be
27+
* overridden by concrete implementations to provide a more meaningful name.
28+
*
29+
* <p>On Java 16 or later, it is recommended to implement this interface using
30+
* a record type.
31+
*
32+
* @since 5.11
33+
* @see DynamicTest#stream(Stream)
34+
* @see DynamicTest#stream(Iterator)
35+
*/
36+
@FunctionalInterface
37+
@API(status = EXPERIMENTAL, since = "5.11")
38+
public interface NamedExecutable extends Named<Executable>, Executable {
39+
@Override
40+
default String getName() {
41+
return toString();
42+
}
43+
44+
@Override
45+
default Executable getPayload() {
46+
return this;
47+
}
48+
}

junit-jupiter-engine/src/test/java/org/junit/jupiter/api/DynamicTestTests.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.ArrayList;
2323
import java.util.Iterator;
2424
import java.util.List;
25+
import java.util.Locale;
2526
import java.util.function.Function;
2627
import java.util.stream.Collectors;
2728
import java.util.stream.Stream;
@@ -90,6 +91,18 @@ void streamFromIteratorWithNamesPreconditions() {
9091
assertThrows(PreconditionViolationException.class, () -> DynamicTest.stream(emptyIterator(), null));
9192
}
9293

94+
@Test
95+
void streamFromStreamWithNamedExecutablesPreconditions() {
96+
assertThrows(PreconditionViolationException.class,
97+
() -> DynamicTest.stream((Stream<DummyNamedExecutableForTests>) null));
98+
}
99+
100+
@Test
101+
void streamFromIteratorWithNamedExecutablesPreconditions() {
102+
assertThrows(PreconditionViolationException.class,
103+
() -> DynamicTest.stream((Iterator<DummyNamedExecutableForTests>) null));
104+
}
105+
93106
@Test
94107
void streamFromStream() throws Throwable {
95108
Stream<DynamicTest> stream = DynamicTest.stream(Stream.of("foo", "bar", "baz"), String::toUpperCase,
@@ -119,6 +132,26 @@ void streamFromIteratorWithNames() throws Throwable {
119132
assertStream(stream);
120133
}
121134

135+
@Test
136+
void streamFromStreamWithNamedExecutables() throws Throwable {
137+
Stream<DynamicTest> stream = DynamicTest.stream(
138+
Stream.of(new DummyNamedExecutableForTests("foo", this::throwingConsumer),
139+
new DummyNamedExecutableForTests("bar", this::throwingConsumer),
140+
new DummyNamedExecutableForTests("baz", this::throwingConsumer)));
141+
142+
assertStream(stream);
143+
}
144+
145+
@Test
146+
void streamFromIteratorWithNamedExecutables() throws Throwable {
147+
Stream<DynamicTest> stream = DynamicTest.stream(
148+
List.of(new DummyNamedExecutableForTests("foo", this::throwingConsumer),
149+
new DummyNamedExecutableForTests("bar", this::throwingConsumer),
150+
new DummyNamedExecutableForTests("baz", this::throwingConsumer)).iterator());
151+
152+
assertStream(stream);
153+
}
154+
122155
private void assertStream(Stream<DynamicTest> stream) throws Throwable {
123156
List<DynamicTest> dynamicTests = stream.collect(Collectors.toList());
124157

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

236+
record DummyNamedExecutableForTests(String name, ThrowingConsumer<String> consumer) implements NamedExecutable {
237+
238+
@Override
239+
public String getName() {
240+
return name.toUpperCase(Locale.ROOT);
241+
}
242+
243+
@Override
244+
public void execute() throws Throwable {
245+
consumer.accept(name);
246+
}
247+
}
248+
203249
}

0 commit comments

Comments
 (0)