junit-team/junit5 · error · JUnitException

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

Error message

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

What it means

Thrown by EngineDiscoveryRequestResolution.DefaultContext.resolve (line 226-245) when a nested selector resolves to more than one Match. The discovery model expects that resolving a selector within a parent context yields at most one TestDescriptor; multiple matches indicate ambiguity. The exception lists the colliding descriptors so the user can see what overlapped.

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

Solutions

  1. If you author a SelectorResolver, ensure resolve(...) returns a Resolution with a single exact match when the engine calls into a context that requires uniqueness (use Match.exact for one descriptor).
  2. Eliminate duplicate classes/resources on the classpath that cause a single selector to match multiple descriptors.
  3. Disambiguate the selector: use MethodSelector with explicit parameter types or a UniqueIdSelector instead of a broad ClassSelector/PackageSelector.
  4. Read the listed descriptors in the message to identify which two elements collide, then fix their UniqueIds or discovery logic.

Example fix

// before: custom resolver returns multiple matches
@Override public Resolution resolve(ClassSelector sel, Context ctx) {
    return Resolution.match(Match.exact(d1)).match(Match.exact(d2)); // ambiguous
}

// after: resolve to exactly one descriptor
@Override public Resolution resolve(ClassSelector sel, Context ctx) {
    TestDescriptor unique = resolveOne(sel);
    return Resolution.match(Match.exact(unique));
}
Defensive patterns

Strategy: validation

Validate before calling

// When authoring a SelectorResolver, never return multiple exact matches from a
// context that requires a unique parent. Resolve to exactly one descriptor:
@Override public Resolution resolve(MethodSelector sel, Context ctx) {
    Optional<TestDescriptor> one = findSingle(sel);
    return one.map(d -> Resolution.match(Match.exact(d))).orElseGet(Resolution::unresolved);
}

Try / catch

try {
    context.resolve(mySelector);
} catch (JUnitException e) {
    // message lists the colliding descriptors; use them to disambiguate
    log.error("ambiguous resolution: {}", e.getMessage());
    throw e;
}

Prevention

When it happens

Trigger: A SelectorResolver registered by a TestEngine returns a Resolution with multiple exact Match entries for a single selector, and EngineDiscoveryRequestResolution.DefaultContext.resolve() (used by addToParent that needs a unique parent) cannot pick one. Typically happens with custom resolvers or when two test descriptors collide on the same UniqueId/selector.

Common situations: Writing a custom SelectorResolver that over-resolves (returns multiple matches for a method/class selector); duplicate classes on the classpath so a class selector matches twice; overloaded methods where a resolver returns broad matches; an engine bug producing non-unique UniqueIds.

Related errors


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