junit-team/junit5 · error · JUnitException
@TestFactory method must not return null
Error message
@TestFactory method must not return null
What it means
Thrown when a @TestFactory method returns null. JUnit Jupiter treats @TestFactory methods as dynamic-test producers; null is neither a single DynamicNode nor a convertible collection/stream, so there is no way to enumerate tests and the framework aborts with an explicit message instead of silently producing zero tests.
Solutions
- Return Stream.empty(), Collections.emptyList(), or an empty Iterable instead of null to express 'no dynamic tests'.
- Fix the missing return statement or the conditional that fell through to null.
- Use a single DynamicTest.dynamicTest(...) when you have exactly one test.
Example fix
// before
@TestFactory
Stream<DynamicTest> factory() {
if (inputs.isEmpty()) return null;
return inputs.stream().map(i -> dynamicTest(i, () -> run(i)));
}
// after
@TestFactory
Stream<DynamicTest> factory() {
if (inputs.isEmpty()) return Stream.empty();
return inputs.stream().map(i -> dynamicTest(i, () -> run(i)));
} Defensive patterns
Strategy: validation
Validate before calling
Object result = invokeTestFactory();
if (result == null) {
throw new IllegalStateException("@TestFactory returned null - return Stream.empty() instead");
} Prevention
- Always return a non-null Stream/Iterable/DynamicNode from @TestFactory methods.
- Use Stream.empty() or Collections.emptyList() to express 'no tests'.
- Add a unit test that asserts the factory method's return value is non-null.
When it happens
Trigger: toDynamicNodeStream(@Nullable Object) is called with a null result from the @TestFactory method; the explicit null check at the top of that method throws JUnitException.
Common situations: Method returns null by accident (early return, missing return statement, conditional return that falls through), or returns null to signal 'no tests' which is not allowed - use Collections.emptyList() or Stream.empty() instead.
Related errors
AI-assisted analysis of junit-team/junit5@f070c699a0 (2026-08-11).
Data as JSON: /api/errors/6b1e8238535b8f86.
Report an issue: GitHub.
Appendix: source
Thrown at junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java:139
while (iterator.hasNext()) {
DynamicNode dynamicNode = iterator.next();
Optional<JupiterTestDescriptor> descriptor = createDynamicDescriptor(this, dynamicNode, index,
defaultTestSource, getDynamicDescendantFilter(), configuration);
descriptor.ifPresent(dynamicTestExecutor::execute);
index++;
}
}
catch (ClassCastException ex) {
throw invalidReturnTypeException(ex);
}
dynamicTestExecutor.awaitFinished();
});
}
@SuppressWarnings("unchecked")
private Stream<DynamicNode> toDynamicNodeStream(@Nullable Object testFactoryMethodResult) {
if (testFactoryMethodResult == null) {
throw new JUnitException("@TestFactory method must not return null");
}
if (testFactoryMethodResult instanceof DynamicNode node) {
return Stream.of(node);
}
return (Stream<DynamicNode>) CollectionUtils.toStream(testFactoryMethodResult);
}
private JUnitException invalidReturnTypeException(Throwable cause) {
String message = "Objects produced by @TestFactory method '%s' must be of type %s.".formatted(
getTestMethod().toGenericString(), DynamicNode.class.getName());
return new JUnitException(message, cause);
}
static Optional<JupiterTestDescriptor> createDynamicDescriptor(JupiterTestDescriptor parent, DynamicNode node,
int index, TestSource defaultTestSource, DynamicDescendantFilter dynamicDescendantFilter,
JupiterConfiguration configuration) {
UniqueId uniqueId;View on GitHub (pinned to f070c699a0)