skip to content

In JUnit 5, how do you label a subset of tests with the @Tag annotation so that a build can run only that subset, and what does @Tag actually do at discovery time?

level: juniorimportance: must knowfreq 50%

answer

  1. @Tag = label, zero behaviour
  2. post-discovery filter, not skipped
  3. class tag reaches methods, @Nested, subclasses
  4. repeatable, tags accumulate
  5. includeTags / excludeTags / groups / -t

basics

~20 s

@Tag("slow") labels a test class or method; it is metadata and changes nothing by itself. The runner passes a tag expression (Gradle includeTags/excludeTags, Maven groups/excludedGroups) and the JUnit Platform filters non-matching tests out of the run.

solid answer

~50 s

`@Tag("slow")` attaches a string label to a test class or a test method. It is repeatable, and a class-level tag applies to every test method in that class, to `@Nested` classes and to subclasses. On its own it does nothing: it does not skip, reorder or isolate anything. Selection happens in the JUnit Platform launcher. Tests are discovered first, then **post-discovery tag filters** drop the ones that do not match the expression, so a filtered-out test is simply absent from the execution plan. It is not reported as skipped or disabled the way `@Disabled` is, so "the count went down" is the only visible sign. How the expression reaches the launcher depends on who runs the tests: Gradle's `useJUnitPlatform { includeTags("fast"); excludeTags("slow") }`, Maven Surefire's `groups`/`excludedGroups`, the ConsoleLauncher's `-t`/`-T`, or an IDE run configuration. The usual purpose is CI staging: a fast label for the pull-request gate, heavier labels for a nightly job.

go deeper

for a junior

Know the syntax, that it goes on classes and methods, and that the build decides what actually runs.

for a middle

Add the inheritance rules (class → methods, @Nested, subclasses), repeatability, and that filtering is a post-discovery step rather than a skip.

for a senior

Frame it as CI staging: which axis you tag on, and the trap that include-style filters silently drop untagged tests.

for a principal

Talk about tags as a suite-partitioning contract — a small closed vocabulary, enforced by convention or a check, versus alternatives like timing-based sharding.

## What a tag is `@Tag` is a JUnit 5 (Jupiter) annotation that attaches an arbitrary text label to a test: `@Tag("slow")`, `@Tag("db")`, `@Tag("smoke")`. The string is yours to invent; there is no registry, enum or config file where tags must be declared first. The annotation is pure declarative metadata. Adding it changes no behaviour whatsoever unless something outside the test asks to filter by it. ## Where a tag can go Tags may be placed on a test class or on an individual `@Test`/`@ParameterizedTest`/`@TestFactory` method. `@Tag` is repeatable, so a method can carry several. Tags accumulate downwards: a class-level tag applies to every test in the class, to its `@Nested` inner classes and to subclasses that inherit the tests. A method-level tag adds to, rather than replaces, the class-level ones — a class tagged `db` with a method tagged `slow` yields a test carrying both. ## What performs the filtering A JUnit Platform run has two phases. **Discovery** builds a tree of test descriptors from your selectors (classes, packages, methods). **Execution** walks that tree. Tag filtering sits between the two as a *post-discovery filter*: the engine finds every test, then `TagFilter.includeTags(...)` / `excludeTags(...)` prune the descriptors whose tags do not satisfy the expression. The consequence matters in interviews: a test removed by a tag filter is not "skipped", it is *not in the plan*. Reports show no entry for it — no `skipped` line, no disabled reason. That is different from `@Disabled` and from a failed `assumeTrue(...)`, both of which execute far enough to be reported as skipped or aborted. So if someone says "our nightly suite reports 400 skipped tests because they're tagged", the tags are not what is skipping them. ## Getting the expression to the launcher You never write the filter in Java for a normal build; the runner supplies it. Gradle exposes `includeTags` / `excludeTags` inside `useJUnitPlatform`, Maven Surefire exposes `groups` / `excludedGroups`, the ConsoleLauncher takes `-t` / `--include-tag` and `-T` / `--exclude-tag`, and IDEs offer a "Tags" run-configuration option. All of them accept a full tag *expression*, not just a bare name, so `fast & !flaky` is legal wherever a tag goes. ## What people tag in practice Useful taxonomies are small and mechanical: a speed axis (`fast`/`slow`), a dependency axis (`db`, `kafka`, `browser`, `network`), or a purpose axis (`smoke`, `contract`, `security`). The point is to make one CI stage cheap: run everything that needs no external process on every push, and defer the rest. Avoid subjective labels nobody can apply consistently. ## Common mistakes Expecting `@Tag` alone to change something; assuming the label must be predeclared; forgetting that a class tag reaches nested and inherited tests; and — the big one — assuming a newly written, untagged test will still run in a job configured to *include* a tag. It will not: including `fast` means "tests whose tags satisfy `fast`", and an untagged test satisfies nothing.

  • Does a test removed by a tag filter appear as skipped in the test report?
    No. Tag filters are post-discovery filters, so the test is pruned from the execution plan and never reported at all. Only `@Disabled`, a failed `assumeTrue(...)` or a disabling condition produce a skipped/aborted entry. The practical tell is that the total test count drops rather than the skipped count rising.
  • A class is annotated @Tag("db") and one method inside it is annotated @Tag("slow"). Which tags does that method have?
    Both: `db` and `slow`. Class-level tags apply to every contained test and method-level tags add to them rather than overriding. So the method matches the expression `db`, the expression `slow`, and `db & slow`, and it is excluded by an exclusion of either one.
  • Can you put @Tag on a @Nested inner class?
    Yes, and it behaves like any class-level tag for the tests inside it, on top of whatever the enclosing class declares. That makes `@Nested` a convenient way to tag a coherent group of scenarios without repeating the annotation on each method.

A tag is a luggage label, not a gate. The label alone never keeps a bag off the plane; the sorting machine that reads labels does.

saying these in an interview costs you the question

  • Saying @Tag skips or disables tests by itself
  • Thinking tag names must be predeclared in an enum or config file
  • Claiming filtered-out tests show up as skipped in the report
  • Believing a method-level tag replaces the class-level tag
  • Confusing @Tag with @Disabled or with JUnit 4 @Category semantics being enforced at runtime

context