Your team wants named groupings of a large test corpus — a smoke set, a slow set, an integration set. When would you express those as JUnit 5 @Suite classes rather than relying on @Tag values filtered at launch time, and what does each cost over years?
answer
- Tags = property, lives with the test; suite = list, lives elsewhere
- Tags: no second list, 2^N subsets, locally readable
- Suite: curated identity, scoped @ConfigurationParameter, cross-engine
- Costs: suite rot + double execution vs tag-vocabulary sprawl
- Failure mode of both is silence — add a guard
basics
~20 sTags are the default: each test declares what it is once, and any run can select combinations. Add a @Suite class only when the grouping needs a code-versioned identity, its own configuration parameters, or must select across engines — and accept double execution and drift as its cost.
solid answer
~1 minDefault to **tags**. A `@Tag` is a property of the test, declared once at the source of truth, and any launcher can combine them with tag expressions. Nothing needs editing when a new run wants a new combination, and there is no second list to keep in sync. Reach for a **`@Suite` class** when the grouping needs something a filter cannot express: - a **named, code-versioned identity** — a curated smoke set whose membership is a reviewable diff, not a string in someone's launch options; - **its own configuration parameters** (`@ConfigurationParameter`) — scoped parallelism or instance lifecycle for that group only; - **cross-engine selection** or engine restriction via `@IncludeEngines`; - an entry point that behaves identically in an IDE and anywhere else. Costs: a suite is a second list that rots (the classic "someone added a test and forgot the suite"); selected classes still run in the ordinary run, so they execute twice unless the two are kept disjoint; and suite membership is invisible from the test file itself, so a reader cannot tell which groups a test belongs to. Tags cost a vocabulary that must be governed or it sprawls. A common resolution: tags for membership, a thin suite only where scoped configuration is genuinely needed.
go deeper
Say tags label tests and suites list them, and that JUnit 5 usually prefers tags.
Contrast where the information lives, and name the one thing only a suite can do — carry its own configuration parameters.
Argue the maintenance economics: staleness of curated lists, double execution, and how you would guard against both.
Pick a primary mechanism for the organisation, justify each exception, and design the guard against the shared failure mode — tests silently belonging to nothing.
## The two control surfaces **Tags** attach a property to the test: `@Tag("slow")` on a class or method. Selection happens later, by expression (`fast & !flaky`), wherever the run is launched. The information lives *with* the test. **Suites** attach a membership list to a group: a `@Suite` class with `@SelectClasses` / `@SelectPackages`, optional filters, and optional `@ConfigurationParameter`s. The information lives *away from* the test, in a class of its own. That single difference — where the fact lives — drives almost every long-term consequence. ## Where tags win 1. **No second list.** A new test is in the group the moment it is tagged. Nobody has to remember to update anything else. 2. **Combinatorics for free.** N tags give you 2^N selectable subsets with no new files. A suite class gives you exactly one grouping. 3. **Local readability.** Opening a test file tells you what the test is (`@SlowTest`, `@IntegrationTest`). Suite membership is invisible from the test. 4. **Refactoring-safe with composed annotations.** Wrapping `@Tag("slow")` in a `@SlowTest` annotation makes the label a compile-checked symbol, with find-usages and rename support. ## Where suites win 1. **Curated membership is the point.** A smoke set is deliberately *not* "everything with a property" — it is a chosen, small, reviewed list. Expressing it as `@SelectClasses` makes each addition a diff someone approves. Expressing it as a tag makes it easy for anyone to bolt their favourite test onto the critical path. 2. **Scoped configuration.** Only a suite can carry `@ConfigurationParameter` for its own nested run — enabling parallel execution or a `per_class` lifecycle for one group without touching global defaults. This is the strongest technical reason to keep suite classes. 3. **Cross-engine or engine-restricted selection.** `@IncludeEngines("junit-jupiter")` or selecting Cucumber plus Jupiter tests as one unit is natural in a suite. 4. **Identity in reports and IDEs.** `@SuiteDisplayName` gives the group a stable name in reports; `@Testable` gives it a run gutter. A tag expression has no such identity. ## The costs, stated honestly **Suites rot.** The single most common failure is a `@SelectClasses` list that no longer reflects reality — a new critical test was written and never added, so the smoke suite is green while the product is broken. `@SelectPackages` mitigates this by making membership structural, at the price of coupling grouping to package layout and inheriting the default class-name pattern (`^(Test.*|.+[.$]Test.*|.*Tests?)$`), which silently excludes oddly named classes. **Double execution.** A suite does not remove its selected classes from ordinary discovery. If a suite and a normal run happen together, the tests run twice — wasted time and, worse, doubled flakiness exposure and confusing report counts. Keeping them disjoint requires a deliberate convention. **Two ways to say the same thing.** A codebase with both a rich tag vocabulary and a pile of suites forces every reader to check both. Pick a primary mechanism and let the other be the documented exception. **Tags sprawl.** Left ungoverned, tag vocabularies grow synonyms (`slow`, `Slow`, `long-running`) that nobody prunes because removing one might break an unseen run. Tags need an owner and a short, documented list, ideally enforced by making composed annotations the only sanctioned way to tag. ## How I would decide - Is the grouping a **property** of each test (fast/slow, needs-database, flaky)? → tag. - Is the grouping a **curated set** with an owner and a review gate (release smoke, contract set)? → suite, with `@SelectClasses` so additions are visible. - Does the group need **different execution semantics** from everything else? → suite, because only it can carry configuration parameters. - Is it just "I want to run these right now"? → neither; that is an ad-hoc selection at launch time and should not become a committed artifact. And whatever you choose, add one guard: something that fails loudly when the grouping is wrong. For a tag vocabulary, a check that rejects unknown tag strings. For a curated suite, a periodic run of everything so a forgotten test is not invisible forever. The failure mode of both mechanisms is silence — tests that quietly stop being part of anything — and silence is the thing to engineer against.
- How would you stop a curated @SelectClasses suite from silently going stale?Keep the full corpus running somewhere so nothing is invisible, and make suite membership a reviewed diff by convention. Where the grouping is genuinely structural, prefer @SelectPackages so new classes join automatically. A check that asserts the suite is non-empty and, for critical suites, that specific classes are present prevents the worst case of an accidentally empty group.
- When is double execution from a suite actually acceptable?When the suite exists for its configuration rather than its membership — for example a suite that reruns a subset under parallel execution to smoke out thread-safety problems, where running those tests twice under different settings is the intent. It is not acceptable when it merely doubles wall-clock time and inflates report counts for no signal.
saying these in an interview costs you the question
- Treating suites as the default grouping mechanism in JUnit 5, as they were in JUnit 4
- Claiming a suite removes its selected classes from the ordinary test run
- Maintaining both a large tag vocabulary and many suites without stating which is primary
- Ignoring that @SelectPackages is subject to the default class-name pattern
- Assuming @ConfigurationParameter on a suite affects the whole test JVM