junit-team/junit5 · error · JUnitException

Selector did not yield unique test descriptor

Error message

Selector ${selector} did not yield unique test descriptor: ${stringRepresentation}

What it means

Thrown during discovery resolution when a single DiscoverySelector resolves to more than one TestDescriptor match. The engine's resolver contract expects a selector to identify at most one descriptor when used in a parent-resolving context, so an ambiguous match is a hard error. The message lists every matched descriptor so you can see exactly what collided.

Solutions

  1. Read the listed descriptors: if two different engines claim the test, restrict discovery with an EngineFilter.includeEngines(...) to just one engine.
  2. If a custom SelectorResolver is involved, make it return exactly one Match (use Match.exact) instead of a collection, or merge the descriptors.
  3. Narrow the selector: use a UniqueIdSelector built from the full UniqueId rather than a broad ClassSelector/MethodSelector.
  4. Remove the duplicate registration (e.g. a class listed twice in @SelectClasses, or a @Suite and a direct run overlapping).

Example fix

// before: broad selector resolves to multiple descriptors
request = LauncherDiscoveryRequestBuilder.request()
    .selectors(selectClass(MyTests.class)).build();
// after: pin to a single unique id / single engine
request = LauncherDiscoveryRequestBuilder.request()
    .selectors(selectUniqueId(UniqueId.forEngine("junit-jupiter")
        .append("class", MyTests.class.getName())))
    .filters(includeEngines("junit-jupiter")).build();
Defensive patterns

Strategy: validation

Validate before calling

// Before resolving, restrict to a single engine and prefer unique selectors
var request = LauncherDiscoveryRequestBuilder.request()
    .selectors(selectUniqueId(fullUniqueId))
    .filters(includeEngines("junit-jupiter"))
    .build();
// For custom resolvers, return Match.exact(descriptor) not a list.

Type guard

// narrowing is on the engine-internal Resolution; guard at the selector level
static boolean isUnique(Resolution r) {
    return r != null && r.getMatches().size() == 1;
}

Try / catch

try {
    context.resolve(selector);
} catch (JUnitException e) {
    if (e.getMessage().contains("did not yield unique test descriptor")) {
        // narrow selector or restrict engines, then retry discovery
    }
    throw e;
}

Prevention

When it happens

Trigger: Calling Context.resolve(selector) / addToParent with a selector that an engine's discovery resolves to 2+ matches. Happens with custom DiscoverySelectors whose resolver is too broad, UniqueId selectors that match across engines, or ClassSelector/MethodSelector reused where duplicate descriptors exist. Also triggered by programmatic discovery via LauncherDiscoveryRequestBuilder when the same logical test is registered twice under the same selector.

Common situations: Two test engines both resolve the same selector (e.g. junit-jupiter and junit-vintage both claim a class); a custom TestEngine's SelectorResolver returns multiple Match objects instead of a single one; duplicate @Suite / @SelectClasses registrations that collapse onto one UniqueId; migration from JUnit 4 where a class is picked up by both vintage and jupiter engines.

Related errors


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

Appendix: source

Thrown at junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/EngineDiscoveryRequestResolution.java:236

				Function<TestDescriptor, Optional<T>> creator) {
			if (parent != null) {
				return createAndAdd(parent, creator);
			}
			return resolve(parentSelectorSupplier.get()).flatMap(parent -> createAndAdd(parent, creator));
		}

		@Override
		public Optional<TestDescriptor> resolve(DiscoverySelector selector) {
			// @formatter:off
			return EngineDiscoveryRequestResolution.this.resolve(selector)
					.map(Resolution::getMatches)
					.flatMap(matches -> {
						if (matches.size() > 1) {
							String stringRepresentation = matches.stream()
									.map(Match::getTestDescriptor)
									.map(Objects::toString)
									.collect(joining(", "));
							throw new JUnitException(
								"Selector " + selector + " did not yield unique test descriptor: " + stringRepresentation);
						}
						if (matches.size() == 1) {
							return Optional.of(getOnlyElement(matches).getTestDescriptor());
						}
						return Optional.empty();
					});
			// @formatter:on
		}

		@SuppressWarnings("unchecked")
		private <T extends TestDescriptor> Optional<T> createAndAdd(TestDescriptor parent,
				Function<TestDescriptor, Optional<T>> creator) {
			Optional<T> child = creator.apply(parent);
			if (child.isPresent()) {
				UniqueId uniqueId = child.get().getUniqueId();
				if (resolvedUniqueIds.containsKey(uniqueId)) {
					return Optional.of((T) resolvedUniqueIds.get(uniqueId).getTestDescriptor());

View on GitHub (pinned to f070c699a0)