junit-team/junit5 · error · JUnitException

@TestFactory method must not return null

Error message

@TestFactory method must not return null

What it means

Thrown by TestFactoryTestDescriptor.toDynamicNodeStream() when a @TestFactory method returns a literal null. A @TestFactory must return a stream/collection/iterator/array of DynamicNode (or a single DynamicNode); null is not a valid dynamic test source and is rejected as a JUnitException.

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 956246301e)

Solutions

  1. Return Stream.empty() (or Collections.emptyList()) instead of null to express 'no dynamic tests'.
  2. Ensure the returned collection/stream is initialized before the method returns.
  3. If a single DynamicNode is intended, return it directly (non-null) per the DynamicNode branch in toDynamicNodeStream.

Example fix

// before
@TestFactory
Stream<DynamicTest> tests() {
    return maybeTests; // maybeTests is null
}
// after
@TestFactory
Stream<DynamicTest> tests() {
    return maybeTests != null ? maybeTests : Stream.empty();
}
Defensive patterns

Strategy: validation

Validate before calling

// Guard the @TestFactory return before returning
Stream<DynamicTest> tests = computeTests();
if (tests == null) tests = Stream.empty();
return tests;

Type guard

static boolean isNonNullDynamicSource(Object result) {
    return result != null && (result instanceof DynamicNode || result instanceof java.util.Collection || result instanceof java.util.stream.Stream || result.getClass().isArray());
}

Prevention

When it happens

Trigger: A @TestFactory method whose body returns null (e.g. a field was never initialized, a computed stream evaluated to null, or an early-return null was used as a 'no tests' sentinel).

Common situations: Conditionally returning null from a @TestFactory to mean 'skip'; refactoring a method that previously returned a collection so it now returns an uninitialized field; a mock-based factory returning null by default.

Related errors


AI-assisted analysis of junit-team/junit5@956246301e (2026-08-04). Data as JSON: /data/errors/6b1e8238535b8f86.json. Report an issue: GitHub.