One CI job excludes JUnit 5 tests tagged "slow" and another includes only tests tagged "fast". A newly written test carrying no tag at all runs in the first job but not the second — explain why, and describe what happens when both an include and an exclude filter are configured at once.
answer
- include = keep if matches; untagged matches nothing
- exclude = drop if matches; untagged survives
- both configured → exclusion wins
- include-style pipelines silently lose new tests
- audit with none() or an unfiltered nightly
basics
~20 sAn include filter keeps only tests whose tags satisfy it, and an untagged test satisfies no tag name, so it is dropped. An exclude filter drops only tests that match, so an untagged test survives. With both, exclusion wins: a test tagged fast and slow is excluded.
solid answer
~50 sTag filters are two separate post-discovery filters with different defaults. **Include** (`includeTags`, Maven `groups`) means "keep a test only if its tags satisfy this expression". An untagged test satisfies no positive tag name, so it is dropped — which is why a freshly written test silently disappears from a `fast`-only job. **Exclude** (`excludeTags`, Maven `excludedGroups`) means "drop a test if its tags satisfy this expression". An untagged test satisfies nothing, so it is kept. With both configured, a test must pass the include filter **and** not match the exclude filter; exclusion effectively wins, so a test tagged both `fast` and `slow` under include `fast` + exclude `slow` does not run. Multiple include entries OR together, as do multiple exclude entries. Operationally: prefer exclusion-based defaults so new tests are visible by default, and if you must use inclusion, run a scheduled `none()` job (or a full unfiltered nightly) so untagged tests cannot rot unnoticed.
go deeper
Just be able to say that including a tag runs only tagged tests, while excluding a tag still runs untagged ones.
State the rule precisely — keep if include matches and exclude does not — and give the both-tags example.
Lead with the operational risk: include-style pipelines silently lose new tests, so default to exclusion and audit with none() or an unfiltered nightly.
Frame it as a safety property of the pipeline — failures should be loud and in the safe direction — plus enforcement (a convention test) and a metric (executed-test count) rather than relying on discipline.
## Two filters, two defaults The JUnit Platform applies tag filtering after discovery, via two independent post-discovery filters: - **include**: a test is kept only if its tag set satisfies the include expression; - **exclude**: a test is dropped if its tag set satisfies the exclude expression. The asymmetry that catches teams out is what each one does with a test that has *no tags*. An expression like `fast` is a predicate over the tags the test actually has. An untagged test has none, so `fast` evaluates false: an include filter drops it. Symmetrically, `slow` is also false for it, so an exclude filter keeps it. Nothing is broken — the semantics are consistent — but the operational consequences are opposite. ## Combining them When both are configured, both must be satisfied: keep if `include(tags) && !exclude(tags)`. So exclusion is the stronger force. A test tagged `fast` *and* `slow`, run with include `fast` and exclude `slow`, does not execute. That is usually what you want (`slow` is a hard cost statement) but it surprises people who read the include as an override. Multiple entries on the same side combine with OR: two include expressions mean "matches either", two exclude expressions mean "drop if either matches". So `includeTags("fast", "smoke")` is the same as `includeTags("fast | smoke")`. ## Why the untagged case is a production problem An include-style pipeline is a trap with a long fuse. On day one every test is tagged. Then someone adds `OrderServiceTest` and forgets the annotation. Nothing fails: the pull-request job stays green because the test never runs, the nightly job is also filtered, and the test is invisible for months. There is no skipped count to notice, because filtered tests are pruned from the plan and produce no report entry at all. The suite silently develops holes. Exclusion-style pipelines fail in the safe direction: the forgotten test runs everywhere, so the worst outcome is a slower pull-request build — loud and self-correcting. ## Practical strategies 1. **Default to exclusion.** Make the pull-request job `excludeTags("slow | browser | flaky")` rather than `includeTags("fast")`. New tests run until someone deliberately opts them out. 2. **Audit the gap.** If inclusion is unavoidable (a huge suite where the fast subset is the exception), add a scheduled job that runs the include expression `none()` and fails if it finds anything, or simply run the whole suite unfiltered nightly. Either way, untagged tests cannot hide. 3. **Tag on an objective axis.** "Needs Postgres" is checkable; "slow" is a judgement that decays as tests change. Objective tags survive because anyone can apply them correctly. 4. **Enforce, don't hope.** A convention test (reflection over the test classpath, or an ArchUnit-style rule) that asserts every test class carries one annotation from a closed set turns an invisible omission into a red build. 5. **Watch the totals.** Track the executed-test count per job. A drop with no deletions in the diff is the signature of a filtering mistake, and it is the only signal a filtered-out test ever produces. ## Related gotchas Remember tags accumulate from enclosing classes and superclasses, so a test can be excluded because of a tag it never declares — check the class, its `@Nested` parents and its superclass before concluding the filter is broken. And in shells and some build DSLs, `!` and `|` need quoting; an unquoted `!slow` can arrive mangled, producing an empty or unexpectedly full run that looks like a JUnit bug and is not.
- A test tagged both "fast" and "slow" runs in a job with include "fast" and exclude "slow". Does it execute?No. Both filters apply, and the test must satisfy the include expression while not satisfying the exclude expression. Since it carries `slow`, the exclude filter drops it — exclusion is effectively the stronger force. This is usually desirable, because the slow tag represents a cost you have deliberately deferred.
- How would you detect that a pull-request job has silently stopped running some tests because of tag filtering?Watch the executed-test count per job over time: filtered tests produce no report entry at all, so a count that drops without any deleted tests in the diff is the only signal. Backing that up, a scheduled run of the include expression `none()` lists untagged tests, and an unfiltered nightly full suite guarantees nothing is permanently invisible.
An allow-list bouncer turns away anyone without a wristband; a deny-list bouncer only turns away the names on his card. Same door, opposite treatment of the guest nobody registered.
saying these in an interview costs you the question
- Assuming untagged tests always run regardless of the filter
- Thinking an include filter overrides an exclude filter for a test that matches both
- Expecting a skipped/disabled entry in the report to reveal filtered-out tests
- Believing multiple includeTags entries are ANDed together
- Treating include-style filtering as equally safe as exclude-style for a growing suite