junit-team/junit5 · critical · JUnitException

Cannot create Launcher for multiple engines with the same ID

Error message

Cannot create Launcher for multiple engines with the same ID '%s'.

What it means

Thrown by EngineIdValidator.validate() during Launcher creation when two distinct TestEngine implementations report the same getId(). The launcher refuses to construct because engine IDs must be unique to route discovery and execution correctly. It is a JUnitException naming the duplicated ID.

Source

Thrown at junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineIdValidator.java:50

	}

	static void validateReservedPrefix(TestEngine testEngine, UniqueId uniqueEngineId,
			DiscoveryIssueCollector issueCollector) {
		String engineId = testEngine.getId();
		if (engineId.startsWith("junit-") && wellKnownClassNameForEngineId(testEngine) == null) {
			var message = "Third-party TestEngine implementations are forbidden to use the reserved 'junit-' prefix for their ID";
			issueCollector.issueEncountered(uniqueEngineId, DiscoveryIssue.create(WARNING, message));
		}
	}

	static Iterable<TestEngine> validate(Iterable<TestEngine> testEngines) {
		Set<String> ids = new HashSet<>();
		for (TestEngine testEngine : testEngines) {
			// check usage of reserved ids
			validateReservedIds(testEngine);
			// check uniqueness
			if (!ids.add(testEngine.getId())) {
				throw new JUnitException(
					"Cannot create Launcher for multiple engines with the same ID '%s'.".formatted(testEngine.getId()));
			}
		}
		return testEngines;
	}

	// https://github.com/junit-team/junit-framework/issues/1557
	private static void validateReservedIds(TestEngine testEngine) {
		var expectedClassName = wellKnownClassNameForEngineId(testEngine);
		if (expectedClassName == null) {
			return;
		}
		validateWellKnownClassName(testEngine, expectedClassName);
	}

	private static @Nullable String wellKnownClassNameForEngineId(TestEngine testEngine) {
		String engineId = Preconditions.notBlank(testEngine.getId(),
			() -> "ID for TestEngine [%s] must not be null or blank".formatted(testEngine.getClass().getName()));

View on GitHub (pinned to 956246301e)

Solutions

  1. Run 'gradle dependencies' / 'mvn dependency:tree' and exclude the duplicate engine artifact.
  2. If shading, use filters to prevent duplicate ServiceLoader entries for TestEngine.
  3. Ensure only one version of each engine JAR is on the test runtime classpath.
  4. Inspect META-INF/services/org.junit.platform.engine.TestEngine in your classpath JARs for duplicates.

Example fix

// before: both junit-jupiter-engine 5.x and a relocated copy on classpath

// after (gradle)
configurations {
    testRuntimeClasspath {
        exclude group: 'org.junit.jupiter', module: 'junit-jupiter-engine' // for the dup
    }
}
Defensive patterns

Strategy: validation

Validate before calling

import org.junit.platform.engine.TestEngine;
Set<String> seen = new HashSet<>();
for (var eng : ServiceLoader.load(TestEngine.class)) {
    if (!seen.add(eng.getId())) {
        throw new IllegalStateException("Duplicate engine id: " + eng.getId() + " from " + eng.getClass().getName());
    }
}

Type guard

static boolean noDuplicateEngineIds(Iterable<TestEngine> engines) {
    Set<String> ids = new HashSet<>();
    for (var e : engines) if (!ids.add(e.getId())) return false;
    return true;
}

Prevention

When it happens

Trigger: Two TestEngine ServiceLoader providers on the classpath returning the same id from TestEngine.getId(). Common with shading, fat-jars, or duplicate dependencies bundling the same engine twice under different class names.

Common situations: A shaded/uber-jar that includes junit-jupiter-engine twice (e.g. direct dependency plus a transitive one with relocated classes). Conflicting third-party engines that both claim a custom ID like 'spek'. Dependency version mismatches causing two copies of an engine artifact.

Related errors


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