skip to content

When you build a JUnit Platform discovery request you can add both selectors and filters. What is the difference between them, and at what point in a run is each one applied?

level: middleimportance: should knowfreq 22%

answer

  1. Selectors additive, filters subtractive
  2. No selectors → nothing found
  3. EngineFilter before, ClassNameFilter during, TagFilter after
  4. Excludes beat includes
  5. selectUniqueId for rerunning exactly one node

basics

~20 s

Selectors say where to look — a package, class, method, classpath root or unique id — and engines interpret them while discovering. Filters remove things from what was found: class-name and package filters during discovery, engine and tag filters applied by the launcher around it.

solid answer

~40 s

They are additive versus subtractive. **Selectors** (`DiscoverySelectors.selectPackage`, `selectClass`, `selectMethod`, `selectClasspathRoots`, `selectUniqueId`, `selectFile`…) define the *search space*. They are passed into engines, and each engine interprets them in its own terms — Jupiter looks for annotated classes under a package, Cucumber for feature files. More selectors mean a potentially larger set. **Filters** are predicates that *remove* candidates: - `ClassNameFilter`/`PackageNameFilter` (`includeClassNamePatterns(".*Test")`) are discovery filters, applied by engines while they walk the search space — cheap, because non-matching classes need not be examined further. - `EngineFilter` (`includeEngines("junit-jupiter")`) is applied by the launcher *before* engines are consulted at all. - `PostDiscoveryFilter`, notably `TagFilter.includeTags("fast")`, is applied by the launcher to the discovered tree, after engines have reported. Rule of thumb: with no selectors you find nothing; with no filters you find everything the selectors reach.

code

java · 9 lines
java
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
        .selectors(
                selectPackage("com.example.orders"),
                selectClass("com.example.billing.InvoiceTest"))
        .filters(
                includeEngines("junit-jupiter"),          // before engines run
                includeClassNamePatterns(".*Test"),        // during discovery
                excludeTags("slow"))                       // after discovery
        .build();

go deeper

for a junior

Grasp the direction: selectors say where to look, filters throw candidates away.

for a middle

Name the concrete APIs and the three application moments — engine filters before, class/package filters during, tag filters after discovery.

for a senior

Discuss which lever to pull for which job (engine filters for CI slicing, tags for semantics, unique ids for reruns) and the include/exclude precedence.

for a principal

Tie it to test-selection strategy at scale: tags as a governed vocabulary, unique-id selection for quarantine and change-based runs, and why naming conventions age worse than tags.

## Two different jobs A `LauncherDiscoveryRequest` carries selectors, filters and configuration parameters. Selectors and filters are easy to confuse because both narrow what runs, but they work in opposite directions and at different times. - **Selectors are additive.** They describe where to look. An empty selector list discovers nothing at all. - **Filters are subtractive.** They prune candidates the selectors reached. An empty filter list keeps everything. ## Selectors: the search space From `org.junit.platform.engine.discovery.DiscoverySelectors`: - `selectClasspathRoots(Set<Path>)` — scan whole output directories (what build tools typically use). - `selectPackage("com.example.orders")` — a package and its subpackages. - `selectClass(CartTest.class)` / `selectClass("com.example.CartTest")`. - `selectMethod(CartTest.class, "total")`, plus parameter-typed and nested variants. - `selectUniqueId("[engine:junit-jupiter]/[class:…]/[method:…]")` — address exactly one previously discovered node; the basis of "rerun this test" and flaky-test quarantine. - `selectFile`, `selectDirectory`, `selectModule`, `selectUri` — for engines whose tests are resources, e.g. Cucumber feature files. - `selectIteration(...)` — a specific invocation of a repeated or parameterised test. Crucially, selectors are handed to **every** engine, and each interprets what it understands. A `selectFile` means nothing to Jupiter and everything to a resource-driven engine. This is why the same request can drive a heterogeneous suite. ## Filters: three kinds, three moments **1. `EngineFilter` — before engines run.** `includeEngines("junit-jupiter")` / `excludeEngines("junit-vintage")`. The launcher consults this first and simply does not call excluded engines. It is the cheapest possible narrowing and the right tool for "this job runs only architecture tests". **2. `DiscoveryFilter` — during discovery, inside the engine.** `ClassNameFilter.includeClassNamePatterns(".*Test")` / `excludeClassNamePatterns`, `PackageNameFilter.includePackageNames(...)`. Engines apply these as they walk the search space, so non-matching classes are never fully examined. This is where naming conventions are enforced. **3. `PostDiscoveryFilter` — after discovery, on the tree.** `TagFilter.includeTags("fast")` / `excludeTags("slow")`, and custom implementations returning `FilterResult.included/excluded`. These need the discovered model because a tag is a property of a discovered node, not of a class name. The launcher applies them to the merged tree, then prunes containers that end up with no remaining tests. ## Interaction rules worth knowing - **Includes are OR'd within a kind, and kinds are AND'd across.** Multiple class-name include patterns mean "matches any"; a tag filter and a class-name filter both have to pass. - **Excludes beat includes.** `excludeTags("slow")` wins over `includeTags("fast")` for a test tagged both. - **Filters can only shrink.** No filter can add a test that no selector reached. Symmetrically, adding a selector cannot resurrect something a filter removed. - **Empty results are legitimate.** A request whose filters remove everything simply produces an empty plan; the platform does not treat that as an error by itself. ## Choosing the right lever - Restricting *which framework's* tests run → `EngineFilter`. Cheapest, clearest. - Enforcing a *naming convention* → `ClassNameFilter` during discovery. - Slicing by *semantics* (fast/slow, integration, flaky) → `@Tag` plus `TagFilter` post-discovery. Tags are the right tool because they express intent, whereas class-name patterns encode it in the file name. - Rerunning *specific tests* → `selectUniqueId` selectors, not filters. Filters cannot express "only these 12 nodes" nearly as precisely. ## Frequent confusions - *"I'll add a tag filter so my selected class runs"* — a filter can never widen the set; you need a selector. - *"Selecting a package overrides my exclude filter"* — it does not; excludes still apply. - *"Tag filters are applied by the engine"* — tag filtering is a post-discovery step performed by the launcher across all engines, which is what makes tags work uniformly. - *"Class-name filters are the same as tags"* — one is syntactic (file naming), the other semantic and independent of names. ## One-line summary Selectors open doors, filters close them; engines interpret selectors and discovery filters, while the launcher applies engine filters before discovery and tag/post-discovery filters after.

  • Can a filter cause a test to be included that no selector reached?
    No. Filters are strictly subtractive predicates applied to candidates the selectors produced. If the search space never contained the class, no include-filter can bring it back. Widening always means adding or broadening a selector — for example moving from `selectClass` to `selectPackage` or a classpath root.
  • Why is tag filtering applied after discovery rather than during it?
    A tag is a property of a discovered node, not something derivable from a package or class name, so the tree has to exist first. Applying it in the launcher after discovery also makes it uniform across engines, and lets the launcher prune containers that end up with no remaining tests.
  • A test is tagged both `fast` and `slow`, and the request includes tag `fast` and excludes tag `slow`. Does it run?
    No. Exclusions take precedence over inclusions, so the test is filtered out. This is the usual reason a supposedly included test goes missing, and it is intentional: an exclusion is a hard statement that this test must not run in this job.

Selectors are the shelves you tell the librarian to search; filters are the rules for putting books back — no filter can ever fetch a book from a shelf you didn't name.

saying these in an interview costs you the question

  • Thinking a filter can add tests the selectors never reached
  • Believing an include-tag filter overrides an exclude-tag filter
  • Saying tag filtering happens inside each engine during discovery
  • Using class-name patterns where semantic tags are meant
  • Assuming an empty discovery result is always an error

context