Here's what the JUnit 5 User Guide has to say about Dynamic Tests:
§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.
[...]
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 explained, a dynamic test is generated at runtime and is represented by a DynamicTest
object. This means when you have a @TestFactory
method you are creating tests, not executing them. In order to support lazy execution you need to encapsulate the actual test in an object, which is done with Executable
. It may help to imagine a single DynamicTest
as a "normal" @Test
. Say you have:
@TestFactory
DynamicTest generateDynamicTest() {
return DynamicTest.dynamicTest(
"2 + 2 = 4",
() -> assertEquals(4, 2 + 2, "the world is burning")
);
}
As a @Test
method the above would look like:
@Test
@DisplayName("2 + 2 = 4")
void testMath() {
assertEquals(4, 2 + 2, "the world is burning");
}
Note: The two are not quite equivalent. As mentioned in the user guide, dynamic tests do not have the same lifecycle as normal @Test
methods—read the guide to understand the differences.
In other words, the Executable
is the body of the test method. You can think of a @TestFactory
as generating a bunch of test methods at runtime (conceptually). So when you wrap your test code in an Executable
you are creating a function and passing the function to the framework. This allows dynamic tests to mimic the behavior of non-dynamic tests and let's the framework execute the tests when it's ready to do so.
To answer your two additional questions you put in a comment:
By "actual code-to-be-tested" do you mean "actual code-to-be-tested-evaluation" (tests)? because i think code-to-be-tested = multiply(x,y) is invoked immediately , but it's the assertion() which waits, am I right?
The wording of "code-to-be-tested" is, I now realize, ambiguous if not just misleading. Yes, I mean the test code (i.e. the code wrapped in the Executable
, such as the assertions) is what you don't want to be invoked immediately, but rather at some later time—when the test framework is ready to execute the test.
Note you potentially have "double the laziness" in your example due to the use of Stream<DynamicTest>
. Since a Stream
is evaluated lazily, and you don't eagerly build the Stream
(e.g. with Stream.of
), it only creates the DynamicTest
objects as they're needed. This can be beneficial if creating the DynamicTest
is expensive because creating all the tests up front can be avoided. Whether or not JUnit Jupiter takes advantage of this, I'm not sure (haven't looked at the implementation), though I'd be surprised if they didn't.
And what is the point of executing-later? what is the advantage of later and not immediately? what are the method waiting for?
The DynamicTest
is waiting to be passed to the framework and then waiting for the framework to execute it (and executing the DynamicTest
involves executing the Executable
).
Remember, we're dealing with a test factory here, which means you are creating the tests and not executing the tests. Executing the tests is the responsibility of the framework. If the Executable
was executed eagerly then it'd be you executing the test instead of the framework. In effect, eager execution would hide each DynamicTest
inside the @TestFactory
method, preventing the framework from seeing them as individual tests; the framework has to know which test it's executing in order to give an accurate report. Plus, if eagerly executed, a test failure would prevent any remaining tests from being executed.
Note the example in your question could also be accomplished with a parameterized test.
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
class MultiplicationTests {
static Stream<Integer[]> numbersProvider() {
return Stream.of(
new Integer[]{1, 2, 2},
new Integer[]{5, 3, 15},
new Integer[]{121, 4, 484}
);
}
@ParameterizedTest(name = "{0} * {1} = {2}")
@MethodSource("numbersProvider")
void testMultiplication(int a, int b, int expectedResult) {
assertEquals(expectedResult, a * b);
}
}
Of course, this just seems like another way to do the same thing. So what's the difference between a @ParameterizedTest
and a @TestFactory
?
- A parameterized test goes through the normal lifecycle for each invocation.
- With a test factory, the entire test can be dynamically generated, not just the parameters. You could probably mimic this with a parameterized test but you'd be fighting against the design.
- With a test factory, you're literally creating tests; a parameterized test already exists, you're just providing different parameters for each invocation.
At least, that's how I understand the differences.