While standard dynamic tests provide a flat list of results, DynamicContainer allows for the creation of hierarchical dynamic tests. This is particularly useful when you need to group related dynamic tests into nested structures, similar to how @Nested works for static tests.
A @TestFactory method can return a Stream of DynamicNode, which is the common supertype for both DynamicTest (individual test cases) and DynamicContainer (groups of nodes).
|
In JUnit Jupiter, DynamicTest represents a single executable test case generated at runtime. A DynamicContainer is a group of dynamic nodes (tests or nested containers) used to organize dynamic tests hierarchically, like a test suite. DynamicNode is the common abstract superclass for both, providing the base structure for the dynamic test tree. Use DynamicTest for individual tests, DynamicContainer for grouping, and refer to DynamicNode when handling either type generically. |
Useful factory methods of DynamicContainer
public static DynamicContainer dynamicContainer(String displayName,
Iterable<? extends DynamicNode> dynamicNodes)
public static DynamicContainer dynamicContainer(String displayName,
Stream<? extends DynamicNode> dynamicNodes)
Examples
The following example creates a multi-level test hierarchy. We use dynamicContainer() to group tests and dynamicTest() for the actual assertions.
package com.logicbig.example;
import org.junit.jupiter.api.DynamicContainer;
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
public class HierarchicalDynamicNodeTest {
@TestFactory
Stream<DynamicContainer> dynamicNodeHierarchy() {
return Stream
.of("Node A", "Node B")
.map(HierarchicalDynamicNodeTest::getMainDynamicContainer);
}
private static DynamicContainer getMainDynamicContainer(String input) {
return dynamicContainer("Container for " + input,
createDynamicNodeStream(input));
}
private static Stream<DynamicNode> createDynamicNodeStream(String input) {
return Stream.of(
dynamicTest("test not null",
() -> assertNotNull(input)),
dynamicContainer("Sub-container",
createAnotherDynamicTest(input))
);
}
private static Stream<DynamicTest> createAnotherDynamicTest(String input) {
return Stream.of(
dynamicTest("test length",
() -> assertTrue(!input.isEmpty()))
);
}
}
Output$ mvn test -Dtest=HierarchicalDynamicNodeTest [INFO] Scanning for projects... [INFO] [INFO] ------< com.logicbig.example:junit-5-hierarchical-dynamic-nodes >------- [INFO] Building junit-5-hierarchical-dynamic-nodes 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ junit-5-hierarchical-dynamic-nodes --- [WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-dynamic-tests\junit-5-hierarchical-dynamic-nodes\src\main\resources [INFO] [INFO] --- compiler:3.11.0:compile (default-compile) @ junit-5-hierarchical-dynamic-nodes --- [INFO] No sources to compile [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ junit-5-hierarchical-dynamic-nodes --- [WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-dynamic-tests\junit-5-hierarchical-dynamic-nodes\src\test\resources [INFO] [INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ junit-5-hierarchical-dynamic-nodes --- [INFO] Nothing to compile - all classes are up to date [INFO] [INFO] --- surefire:3.5.0:test (default-test) @ junit-5-hierarchical-dynamic-nodes --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] +--com.logicbig.example.HierarchicalDynamicNodeTest - 0.104 ss [INFO] | +-- [OK] Container for Node A dynamicNodeHierarchy() test not null - 0.015 ss [INFO] | +-- [OK] Sub-container Container for Node A dynamicNodeHierarchy() test length - 0.001 ss [INFO] | +-- [OK] Container for Node B dynamicNodeHierarchy() test not null - 0.001 ss [INFO] | '-- [OK] Sub-container Container for Node B dynamicNodeHierarchy() test length - 0.002 ss [INFO] [INFO] Results: [INFO] [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.062 s [INFO] Finished at: 2025-12-28T21:57:56+08:00 [INFO] ------------------------------------------------------------------------
Using DynamicContainer
In this specific case, the factory method returns a Stream<DynamicContainer>. This is useful when every top-level element is guaranteed to be a group of tests.
package com.logicbig.example;
import org.junit.jupiter.api.DynamicContainer;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import java.util.Arrays;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
public class DynamicContainerTest {
@TestFactory
Stream<DynamicContainer> dynamicContainerExample() {
return Stream.
of("math", "logic")
.map(DynamicContainerTest::createMainDynamicContainer);
}
private static DynamicContainer createMainDynamicContainer(String topic) {
return dynamicContainer("Topic: " + topic,
Arrays.asList(
getDynamicTest(topic),
getAnotherDynamicTest(topic)
));
}
private static DynamicTest getAnotherDynamicTest(String topic) {
return dynamicTest(topic + " test 2",
() -> assertEquals(4, 2 + 2));
}
private static DynamicTest getDynamicTest(String topic) {
return dynamicTest(topic + " test 1",
() -> assertTrue(true));
}
}
Output$ mvn test -Dtest=DynamicContainerTest [INFO] Scanning for projects... [INFO] [INFO] ------< com.logicbig.example:junit-5-hierarchical-dynamic-nodes >------- [INFO] Building junit-5-hierarchical-dynamic-nodes 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ junit-5-hierarchical-dynamic-nodes --- [WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-dynamic-tests\junit-5-hierarchical-dynamic-nodes\src\main\resources [INFO] [INFO] --- compiler:3.11.0:compile (default-compile) @ junit-5-hierarchical-dynamic-nodes --- [INFO] No sources to compile [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ junit-5-hierarchical-dynamic-nodes --- [WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-dynamic-tests\junit-5-hierarchical-dynamic-nodes\src\test\resources [INFO] [INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ junit-5-hierarchical-dynamic-nodes --- [INFO] Nothing to compile - all classes are up to date [INFO] [INFO] --- surefire:3.5.0:test (default-test) @ junit-5-hierarchical-dynamic-nodes --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] +--com.logicbig.example.DynamicContainerTest - 0.189 ss [INFO] | +-- [OK] Topic: math dynamicContainerExample() math test 1 - 0.034 ss [INFO] | +-- [OK] Topic: math dynamicContainerExample() math test 2 - 0.003 ss [INFO] | +-- [OK] Topic: logic dynamicContainerExample() logic test 1 - 0.002 ss [INFO] | '-- [OK] Topic: logic dynamicContainerExample() logic test 2 - 0.003 ss [INFO] [INFO] Results: [INFO] [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.411 s [INFO] Finished at: 2025-12-28T16:18:57+08:00 [INFO] ------------------------------------------------------------------------
Output Analysis
The above output confirms that JUnit 5 successfully resolved the nested structures provided by the @TestFactory methods. By returning DynamicNode and DynamicContainer, we can see in the execution report how tests are logically grouped into folders or nodes, allowing for a cleaner representation of complex, runtime-generated test scenarios compared to a flat list of dynamic tests.
Example ProjectDependencies and Technologies Used: - junit-jupiter-engine 6.0.1 (Module "junit-jupiter-engine" of JUnit)
Version Compatibility: 5.0.0 - 6.0.1 Version compatibilities of junit-jupiter-engine with this example:
- 5.0.0
- 5.0.1
- 5.0.2
- 5.0.3
- 5.1.0
- 5.1.1
- 5.2.0
- 5.3.0
- 5.3.1
- 5.3.2
- 5.4.0
- 5.4.1
- 5.4.2
- 5.5.0
- 5.5.1
- 5.5.2
- 5.6.0
- 5.6.1
- 5.6.2
- 5.6.3
- 5.7.0
- 5.7.1
- 5.7.2
- 5.8.0
- 5.8.1
- 5.8.2
- 5.9.0
- 5.9.1
- 5.9.2
- 5.9.3
- 5.10.0
- 5.10.1
- 5.10.2
- 5.10.3
- 5.10.4
- 5.10.5
- 5.11.0
- 5.11.1
- 5.11.2
- 5.11.3
- 5.11.4
- 5.12.0
- 5.12.1
- 5.12.2
- 5.13.0
- 5.13.1
- 5.13.2
- 5.13.3
- 5.13.4
- 5.14.0
- 5.14.1
- 6.0.0
- 6.0.1
Versions in green have been tested.
- JDK 25
- Maven 3.9.11
|