@TestFactory is a JUnit Jupiter feature that lets you generate tests at runtime rather than declaring them statically with @Test. This is useful when the number or nature of tests depends on data that’s only known at execution time.
Key Rules
- A
@TestFactorymethod must return one of:DynamicNode(or a subtype likeDynamicTest/DynamicContainer)Stream,Collection,Iterable,Iterator, or an array ofDynamicNode
- It must not be
privateorstatic. - Each generated
DynamicTestconsists of a display name and anExecutable(lambda with the assertion logic).
1. Basic Example — Collection of Tests
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
class CalculatorDynamicTests {
@TestFactory
List<DynamicTest> additionTests() {
return List.of(
dynamicTest("1 + 1 = 2", () -> assertEquals(2, 1 + 1)),
dynamicTest("2 + 3 = 5", () -> assertEquals(5, 2 + 3)),
dynamicTest("10 + 5 = 15", () -> assertEquals(15, 10 + 5))
);
}
}
2. Generating Tests from a Stream
Great for data-driven scenarios:
import java.util.stream.Stream;
@TestFactory
Stream<DynamicTest> squareTests() {
return Stream.of(1, 2, 3, 4, 5)
.map(n -> dynamicTest(
"square of " + n + " is " + (n * n),
() -> assertEquals(n * n, n * n)
));
}
3. Using an Input Generator, Display-Name Generator, and Test Executor
The DynamicTest.stream(...) helper simplifies iterator-based generation:
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PalindromeDynamicTests {
@TestFactory
Stream<DynamicTest> palindromeTests() {
Iterator<String> inputs = List.of("racecar", "level", "madam").iterator();
return DynamicTest.stream(
inputs,
input -> "isPalindrome('" + input + "')",
input -> assertTrue(isPalindrome(input))
);
}
private boolean isPalindrome(String s) {
return new StringBuilder(s).reverse().toString().equals(s);
}
}
4. Grouping Tests with DynamicContainer
You can nest dynamic tests into containers to build a hierarchy:
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.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
class MathDynamicTests {
@TestFactory
Stream<DynamicNode> mathOperations() {
return Stream.of(
dynamicContainer("Addition", List.of(
dynamicTest("1+1", () -> assertEquals(2, 1 + 1)),
dynamicTest("2+2", () -> assertEquals(4, 2 + 2))
)),
dynamicContainer("Multiplication", List.of(
dynamicTest("2*3", () -> assertEquals(6, 2 * 3)),
dynamicTest("4*5", () -> assertEquals(20, 4 * 5))
))
);
}
}
5. Reading Test Data from a File
Perfect for parameterized-style tests where inputs live outside code:
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
@TestFactory
Stream<DynamicTest> testsFromFile() throws Exception {
return Files.lines(Path.of("src/test/resources/cases.csv"))
.map(line -> line.split(","))
.map(parts -> dynamicTest(
"Case: " + parts[0],
() -> assertEquals(Integer.parseInt(parts[2]),
Integer.parseInt(parts[0]) + Integer.parseInt(parts[1]))
));
}
When to Use @TestFactory vs @ParameterizedTest
Use @ParameterizedTest |
Use @TestFactory |
|---|---|
| Fixed set of arguments, single test body | Fully dynamic generation (count, names, logic can all vary) |
| Simple data variations | Hierarchical / conditional / streamed test generation |
| Compile-time known inputs | Runtime-computed inputs |
Important Lifecycle Note
⚠️ Standard lifecycle callbacks like @BeforeEach and @AfterEach do not run around each generated dynamic test — only around the factory method itself. If you need per-test setup, do it inside each Executable.
