2.17. Dynamic Tests

The standard @Test annotation in JUnit Jupiter described in Annotations is very similar to the @Test annotation in JUnit 4. Both describe methods that implement test cases. These test cases are static in the sense that they are fully specified at compile time, and their behavior cannot be changed by anything happening at runtime. Assumptions provide a basic form of dynamic behavior but are intentionally rather limited in their expressiveness.

In addition to these standard tests a completely new kind of test programming model has been introduced in JUnit Jupiter. This new kind of test is a dynamic test which is 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. 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. DynamicTest instances will be executed lazily, enabling dynamic and even non-deterministic generation of test cases.

Any Stream returned by a @TestFactory will be properly closed by calling stream.close(), making it safe to use a resource such as Files.lines().

As with @Test methods, @TestFactory methods must not be private or static and may optionally declare parameters to be resolved by ParameterResolvers.

A DynamicTest is a test case generated at runtime. It is composed of a display name and an Executable. Executable is a @FunctionalInterface which means that the implementations of dynamic tests can be provided as lambda expressions or method references.

Dynamic Test Lifecycle
The execution lifecycle of a dynamic test is quite different than it is for a standard @Test case. Specifically, there are no lifecycle callbacks for individual dynamic tests. This means that @BeforeEach and @AfterEach methods and their corresponding extension callbacks are executed for the @TestFactory method but not for each dynamic test. In other words, if you access fields from the test instance within a lambda expression for a dynamic test, those fields will not be reset by callback methods or extensions between the execution of individual dynamic tests generated by the same @TestFactory method.

As of JUnit Jupiter 5.7.0, dynamic tests must always be created by factory methods; however, this might be complemented by a registration facility in a later release.

2.17.1. Dynamic Test Examples

The following DynamicTestsDemo class demonstrates several examples of test factories and dynamic tests.

The first method returns an invalid return type. Since an invalid return type cannot be detected at compile time, a JUnitException is thrown when it is detected at runtime.

The next five methods are very simple examples that demonstrate the generation of a Collection, Iterable, Iterator, or Stream of DynamicTest instances. Most of these examples do not really exhibit dynamic behavior but merely demonstrate the supported return types in principle. However, dynamicTestsFromStream() and dynamicTestsFromIntStream() demonstrate how easy it is to generate dynamic tests for a given set of strings or a range of input numbers.

The next method is truly dynamic in nature. generateRandomNumberOfTests() implements an Iterator that generates random numbers, a display name generator, and a test executor and then provides all three to DynamicTest.stream(). Although the non-deterministic behavior of generateRandomNumberOfTests() is of course in conflict with test repeatability and should thus be used with care, it serves to demonstrate the expressiveness and power of dynamic tests.

The next method is similar to generateRandomNumberOfTests() in terms of flexibility; however, dynamicTestsFromStreamFactoryMethod() generates a stream of dynamic tests from an existing Stream via the DynamicTest.stream() factory method.

For demonstration purposes, the dynamicNodeSingleTest() method generates a single DynamicTest instead of a stream, and the dynamicNodeSingleContainer() method generates a nested hierarchy of dynamic tests utilizing DynamicContainer.

  1. import static example.util.StringUtils.isPalindrome;
  2. import static org.junit.jupiter.api.Assertions.assertEquals;
  3. import static org.junit.jupiter.api.Assertions.assertFalse;
  4. import static org.junit.jupiter.api.Assertions.assertNotNull;
  5. import static org.junit.jupiter.api.Assertions.assertTrue;
  6. import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
  7. import static org.junit.jupiter.api.DynamicTest.dynamicTest;
  8. import java.util.Arrays;
  9. import java.util.Collection;
  10. import java.util.Iterator;
  11. import java.util.List;
  12. import java.util.Random;
  13. import java.util.function.Function;
  14. import java.util.stream.IntStream;
  15. import java.util.stream.Stream;
  16. import example.util.Calculator;
  17. import org.junit.jupiter.api.DynamicNode;
  18. import org.junit.jupiter.api.DynamicTest;
  19. import org.junit.jupiter.api.Tag;
  20. import org.junit.jupiter.api.TestFactory;
  21. import org.junit.jupiter.api.function.ThrowingConsumer;
  22. class DynamicTestsDemo {
  23. private final Calculator calculator = new Calculator();
  24. // This will result in a JUnitException!
  25. @TestFactory
  26. List<String> dynamicTestsWithInvalidReturnType() {
  27. return Arrays.asList("Hello");
  28. }
  29. @TestFactory
  30. Collection<DynamicTest> dynamicTestsFromCollection() {
  31. return Arrays.asList(
  32. dynamicTest("1st dynamic test", () -> assertTrue(isPalindrome("madam"))),
  33. dynamicTest("2nd dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
  34. );
  35. }
  36. @TestFactory
  37. Iterable<DynamicTest> dynamicTestsFromIterable() {
  38. return Arrays.asList(
  39. dynamicTest("3rd dynamic test", () -> assertTrue(isPalindrome("madam"))),
  40. dynamicTest("4th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
  41. );
  42. }
  43. @TestFactory
  44. Iterator<DynamicTest> dynamicTestsFromIterator() {
  45. return Arrays.asList(
  46. dynamicTest("5th dynamic test", () -> assertTrue(isPalindrome("madam"))),
  47. dynamicTest("6th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
  48. ).iterator();
  49. }
  50. @TestFactory
  51. DynamicTest[] dynamicTestsFromArray() {
  52. return new DynamicTest[] {
  53. dynamicTest("7th dynamic test", () -> assertTrue(isPalindrome("madam"))),
  54. dynamicTest("8th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
  55. };
  56. }
  57. @TestFactory
  58. Stream<DynamicTest> dynamicTestsFromStream() {
  59. return Stream.of("racecar", "radar", "mom", "dad")
  60. .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));
  61. }
  62. @TestFactory
  63. Stream<DynamicTest> dynamicTestsFromIntStream() {
  64. // Generates tests for the first 10 even integers.
  65. return IntStream.iterate(0, n -> n + 2).limit(10)
  66. .mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0)));
  67. }
  68. @TestFactory
  69. Stream<DynamicTest> generateRandomNumberOfTestsFromIterator() {
  70. // Generates random positive integers between 0 and 100 until
  71. // a number evenly divisible by 7 is encountered.
  72. Iterator<Integer> inputGenerator = new Iterator<Integer>() {
  73. Random random = new Random();
  74. int current;
  75. @Override
  76. public boolean hasNext() {
  77. current = random.nextInt(100);
  78. return current % 7 != 0;
  79. }
  80. @Override
  81. public Integer next() {
  82. return current;
  83. }
  84. };
  85. // Generates display names like: input:5, input:37, input:85, etc.
  86. Function<Integer, String> displayNameGenerator = (input) -> "input:" + input;
  87. // Executes tests based on the current input value.
  88. ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0);
  89. // Returns a stream of dynamic tests.
  90. return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
  91. }
  92. @TestFactory
  93. Stream<DynamicTest> dynamicTestsFromStreamFactoryMethod() {
  94. // Stream of palindromes to check
  95. Stream<String> inputStream = Stream.of("racecar", "radar", "mom", "dad");
  96. // Generates display names like: racecar is a palindrome
  97. Function<String, String> displayNameGenerator = text -> text + " is a palindrome";
  98. // Executes tests based on the current input value.
  99. ThrowingConsumer<String> testExecutor = text -> assertTrue(isPalindrome(text));
  100. // Returns a stream of dynamic tests.
  101. return DynamicTest.stream(inputStream, displayNameGenerator, testExecutor);
  102. }
  103. @TestFactory
  104. Stream<DynamicNode> dynamicTestsWithContainers() {
  105. return Stream.of("A", "B", "C")
  106. .map(input -> dynamicContainer("Container " + input, Stream.of(
  107. dynamicTest("not null", () -> assertNotNull(input)),
  108. dynamicContainer("properties", Stream.of(
  109. dynamicTest("length > 0", () -> assertTrue(input.length() > 0)),
  110. dynamicTest("not empty", () -> assertFalse(input.isEmpty()))
  111. ))
  112. )));
  113. }
  114. @TestFactory
  115. DynamicNode dynamicNodeSingleTest() {
  116. return dynamicTest("'pop' is a palindrome", () -> assertTrue(isPalindrome("pop")));
  117. }
  118. @TestFactory
  119. DynamicNode dynamicNodeSingleContainer() {
  120. return dynamicContainer("palindromes",
  121. Stream.of("racecar", "radar", "mom", "dad")
  122. .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text)))
  123. ));
  124. }
  125. }

2.17.2. URI Test Sources for Dynamic Tests

The JUnit Platform provides TestSource, a representation of the source of a test or container used to navigate to its location by IDEs and build tools.

The TestSource for a dynamic test or dynamic container can be constructed from a java.net.URI which can be supplied via the DynamicTest.dynamicTest(String, URI, Executable) or DynamicContainer.dynamicContainer(String, URI, Stream) factory method, respectively. The URI will be converted to one of the following TestSource implementations.

ClasspathResourceSource

If the URI contains the classpath scheme — for example, classpath:/test/foo.xml?line=20,column=2.

DirectorySource

If the URI represents a directory present in the file system.

FileSource

If the URI represents a file present in the file system.

MethodSource

If the URI contains the method scheme and the fully qualified method name (FQMN) — for example, method:org.junit.Foo#bar(java.lang.String, java.lang.String[]). Please refer to the Javadoc for DiscoverySelectors.selectMethod(String) for the supported formats for a FQMN.

UriSource

If none of the above TestSource implementations are applicable.