junit-team/junit5 · error · 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

EngineIdValidator.validate throws if two registered TestEngines report the same getId(). The launcher cannot disambiguate engines by id during discovery and execution, so duplicate ids are a hard error at launcher creation. The id is reported in the message.

Solutions

  1. Inspect the classpath for duplicate engine artifacts (gradle dependencies / mvn dependency:tree) and exclude the duplicate.
  2. If authoring a custom TestEngine, choose a unique id (not 'junit-jupiter', 'junit-vintage', 'junit-platform-suite').
  3. When building a fat/shaded jar, use services resource merging so the same engine provider is not registered twice.

Example fix

// before - duplicate engine on classpath (gradle)
testImplementation('org.junit.jupiter:junit-jupiter-engine')
testImplementation('org.junit.platform:junit-platform-console-standalone') // also bundles it
// after - keep only one source
testImplementation('org.junit.jupiter:junit-jupiter')
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
Defensive patterns

Strategy: validation

Validate before calling

// At build time, detect duplicate engine providers
Set<String> ids = new HashSet<>();
for (TestEngine e : ServiceLoader.load(TestEngine.class)) {
    if (!ids.add(e.getId())) throw new IllegalStateException("duplicate engine id: " + e.getId());
}

Type guard

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

Try / catch

try {
    LauncherFactory.create();
} catch (JUnitException e) {
    if (e.getMessage().contains("multiple engines with the same ID")) {
        // dedupe classpath, then recreate
    }
}

Prevention

When it happens

Trigger: Two TestEngine ServiceLoader providers (or manually registered engines) return the same string from TestEngine.getId(). For example, two copies of junit-jupiter-engine on the classpath, or a custom engine whose id collides with a built-in.

Common situations: Duplicate dependency: junit-jupiter-engine present both as a transitive and an explicit dependency, or a shaded/fat jar bundling it twice; a custom engine hardcoded to id 'junit-jupiter' or another taken id; running with both the standalone console (which bundles engines) and explicit engine jars on the classpath.

Related errors


AI-assisted analysis of junit-team/junit5@f070c699a0 (2026-08-11). Data as JSON: /api/errors/705f6cc7e9ec1d8b. Report an issue: GitHub.

Appendix: 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 f070c699a0)