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
- Read the listed descriptors: if two different engines claim the test, restrict discovery with an EngineFilter.includeEngines(...) to just one engine.
- If a custom SelectorResolver is involved, make it return exactly one Match (use Match.exact) instead of a collection, or merge the descriptors.
- Narrow the selector: use a UniqueIdSelector built from the full UniqueId rather than a broad ClassSelector/MethodSelector.
- 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
- Pin discovery to one engine via includeEngines when running classes that multiple engines could claim.
- Prefer UniqueIdSelector over ClassSelector/MethodSelector for programmatic runs.
- Custom SelectorResolvers should return a single Match, never a multi-element collection.
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
- Failed to create a java.net.URI from
- Could not find any resource(s) with name
- Detected cycle in inner class hierarchy between
- Failed to retrieve canonical path for directory
- Failed to retrieve canonical path for file
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)