Appearance
Dynamic Tests with @TestFactory
Repeated and parameterized tests (chapters 7 to 9) run one test method several times. A test factory goes one step further and creates the tests themselves while the suite runs.
A @TestFactory method is not a test: it is a factory that creates tests, at runtime.
Static vs dynamic tests
A normal @Test is static: the test is fully known at compile time.
java
@Test
void addTest() {
assertEquals(7, 4 + 3);
}A dynamic test is created while the tests run:
java
@TestFactory
DynamicTest addDynamicTest() {
return DynamicTest.dynamicTest(
"addTest", // display name
() -> assertEquals(7, 4 + 3) // executable: the actual test
);
}DynamicTest.dynamicTest takes a display name and an Executable (a lambda with no parameters): the code of the test.
Inside the engine
From the architecture chapter: in the discover phase the engine builds TestDescriptors, and in the execute phase it runs them.
- A normal
@Testbecomes aTestMethodTestDescriptor. Its display name is the method name, and its "executable" is the method itself: the framework wraps the method in a lambda that calls it through reflection. - A dynamic test becomes a
DynamicTestTestDescriptor. Its display name is the one you gave, and its executable is your lambda, which the engine simply runs.
So to the engine both are "a display name plus something to execute".
One difference: the per-test lifecycle. @BeforeEach and @AfterEach run around the @TestFactory method as a whole, once, not around each dynamic test it produces. If every generated test needs fresh setup, dynamic tests may not fit; do the setup inside each test's lambda. (@BeforeAll and @AfterAll behave as usual.)
text
@BeforeEach
Positive Check for number: 1
Positive Check for number: 2
Positive Check for number: 3
Positive Check for number: -5
@AfterEachWhen dynamic tests help
A payment gateway receives a daily settlement.csv file from a bank.
The file is stable: a fixed schema, and the same validation for every row. Two options:
- a parameterized test with
@CsvFileSource(chapters 8 and 9): one test method executed N times, once per row, with the same assertions; - dynamic tests: N independent tests, one per row.
Both work.
The file is not stable:
- columns may appear or disappear,
- the column order may change,
- different rows need different validations (row 1 needs check 1; row 2 checks 1 and 2; row 3 check 3...).
A parameterized test runs the same method for every row, so it cannot vary the checks. With dynamic tests, the code that builds the tests can decide, row by row, which tests to create. The same applies to rows read from a database. For most cases (well over 90%) a parameterized test is enough; dynamic tests are for when the tests themselves depend on the data.
The hierarchy: a composite
This is the composite design pattern: a DynamicTest is a leaf (it runs); a DynamicContainer is a named group, like a folder, holding dynamic tests and/or other containers, nested to any depth.
text
settlement.csv (container)
├── file exists and is readable (test)
├── header and footer present (test)
└── transactions (container)
├── at least one transaction (test)
└── ...Writing dynamic tests
1. A single test
java
@TestFactory
DynamicTest addDynamicTest() {
return DynamicTest.dynamicTest("addTest", () -> assertEquals(7, 4 + 3));
}The return type may also be DynamicNode, the parent type.
2. Many tests
Return a Stream, a Collection, an Iterable, an Iterator or an array of dynamic nodes. A stream is the most common:
java
@TestFactory
Stream<DynamicTest> multipleDynamicTest() {
return Stream.of(1, 2, 3, -5)
.map(n -> DynamicTest.dynamicTest(
"Positive Check for number: " + n,
() -> assertTrue(n > 0)));
}Four tests are created; the one for -5 fails.
java
@TestFactory
Collection<DynamicTest> multipleDynamicTestsCollectionReturn() {
List<DynamicTest> tests = new ArrayList<>();
for (int n : List.of(1, 2, 3, -5)) {
tests.add(DynamicTest.dynamicTest("Positive Check for number: " + n, () -> assertTrue(n > 0)));
}
return tests;
}
@TestFactory
DynamicTest[] multipleDynamicTestsArrayReturn() {
return new DynamicTest[] {
DynamicTest.dynamicTest("Positive Check for number: 1", () -> assertTrue(1 > 0)),
DynamicTest.dynamicTest("Positive Check for number: 2", () -> assertTrue(2 > 0)),
DynamicTest.dynamicTest("Positive Check for number: 3", () -> assertTrue(3 > 0))
};
}3. Containers: different tests for different rows
text
Txn_Id Amount Currency
Txn1 100 INR
Txn2 ABC USDEvery row must have an integer amount; INR rows must also have an amount greater than zero.
java
@TestFactory
Stream<DynamicContainer> fileRowDynamicTests() {
List<String[]> rows = List.of(
new String[]{"Txn1", "100", "INR"},
new String[]{"Txn2", "ABC", "USD"}
);
return rows.stream().map(row -> {
String txnId = row[0];
String amount = row[1];
String currency = row[2];
List<DynamicTest> tests = new ArrayList<>();
// every row
tests.add(DynamicTest.dynamicTest("amount is integer",
() -> assertTrue(amount.matches("\\d+"))));
// INR rows only
if ("INR".equals(currency)) {
tests.add(DynamicTest.dynamicTest("amount is greater than zero",
() -> assertTrue(Integer.parseInt(amount) > 0)));
}
// group this row's tests
return DynamicContainer.dynamicContainer("Row " + txnId, tests);
});
}text
fileRowDynamicTests()
├── Row Txn1
│ ├── amount is integer ✔
│ └── amount is greater than zero ✔
└── Row Txn2
└── amount is integer ✘ ("ABC")Row Txn1 got two tests, row Txn2 one. This is what a parameterized test cannot do.
4. A container with tests and containers
java
@TestFactory
DynamicContainer containerWithTestAndContainer() {
DynamicTest simpleTest = DynamicTest.dynamicTest("addTest", () -> assertEquals(2, 1 + 1));
DynamicContainer nestedContainer = DynamicContainer.dynamicContainer(
"Nested container",
Stream.of(DynamicTest.dynamicTest("MultiplyTest", () -> assertEquals(4, 2 * 2))));
return DynamicContainer.dynamicContainer(
"Parent container",
Stream.of(simpleTest, nestedContainer));
}text
containerWithTestAndContainer()
└── Parent container
├── addTest
└── Nested container
└── MultiplyTestNext: That completes JUnit 5. The rest of the series is about Mockito: replacing a unit's dependencies with fake objects so the unit can be tested in isolation, as chapter 2 required. Chapter 14 starts with its three core ideas: mocks, stubs and spies.
Check yourself
- Is a
@TestFactorymethod a test? What does it do? - To the Jupiter engine, what do a normal test and a dynamic test each consist of?
- How do
@BeforeEachand@AfterEachbehave with a factory that creates five dynamic tests? - When is a parameterized test enough, and when do you need dynamic tests?
- Explain
DynamicNode,DynamicTestandDynamicContaineras a composite. - Which return types can a
@TestFactorymethod have? - How would you give INR rows an extra check that other rows do not get?
Answers
- No; it creates tests at runtime and returns them.
- A display name and something to execute: for
@Test, the method (called via reflection); for a dynamic test, your lambda. - They run once, around the factory method, not around each dynamic test.
- Parameterized: the same validation for every input. Dynamic: when the tests to run depend on the data, e.g. different rows need different checks.
DynamicNodeis the abstract parent;DynamicTestis a leaf that runs;DynamicContainergroups nodes (tests or containers) like a folder.- A single
DynamicNode(DynamicTestorDynamicContainer), or aStream,Collection,Iterable,Iteratoror array of them. - In the code that builds each row's tests, add the extra
DynamicTestonlyif ("INR".equals(currency)), and group the row's tests in aDynamicContainer.