In JUnit 5, how do you declare a test suite that runs a chosen set of test classes, and which annotations decide what goes into it?
answer
- @Suite = class with no tests, only selectors
- @SelectClasses = explicit list; @SelectPackages = recursive scan
- Default name pattern ^(Test.*|.+[.$]Test.*|.*Tests?)$ bites @SelectPackages
- junit-platform-suite-engine runs it — nested Launcher
- Suite class: not private, not abstract, needs a selector
basics
~10 sPut @Suite on a plain class, then add selectors: @SelectClasses lists test classes explicitly, @SelectPackages scans whole packages. The junit-platform-suite-engine artifact must be on the test runtime classpath, otherwise the suite class runs nothing.
solid answer
~40 sA JUnit 5 suite is an ordinary class annotated with `@Suite` (from `junit-platform-suite-api`). It contains no test methods; it only carries **selector** annotations describing what to run: - `@SelectClasses(OrderServiceTest.class, PaymentTest.class)` — an explicit list. - `@SelectPackages("com.acme.api")` — everything discovered under those packages (recursively). - Less common selectors exist too: `@SelectMethod`, `@SelectClasspathResource`, `@SelectDirectories`, `@SelectFile`. Filters narrow the selection afterwards: `@IncludeTags` / `@ExcludeTags` for `@Tag` values, and `@IncludeClassNamePatterns` / `@ExcludeClassNamePatterns` for class-name regexes. With `@SelectPackages` the default class-name pattern (`^(Test.*|.+[.$]Test.*|.*Tests?)$`) already applies, so oddly named classes are silently skipped. The suite itself is executed by a dedicated engine shipped in `junit-platform-suite-engine`. Without that artifact on the test runtime classpath nothing discovers the `@Suite` class and it reports no tests. `@Suite` is meta-annotated `@Testable`, so IDEs offer a run gutter for it.
code
java · 13 linesimport org.junit.platform.suite.api.IncludeClassNamePatterns;
import org.junit.platform.suite.api.SelectClasses;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;
import org.junit.platform.suite.api.SuiteDisplayName;
@Suite
@SuiteDisplayName("API smoke suite")
@SelectPackages("com.acme.api")
@SelectClasses(LegacyOrderTest.class)
@IncludeClassNamePatterns(".*")
class ApiSmokeSuite {
}go deeper
Name @Suite plus @SelectClasses and @SelectPackages, and say the suite class holds no test methods.
Add the filter annotations, the default class-name pattern gotcha, and that a separate suite engine artifact executes the class.
Explain that the suite engine nests a Launcher run, what that buys (per-suite configuration, engine filtering), and the double-execution cost.
Frame it as a grouping strategy question: code-versioned curated groups versus launch-time tag/pattern filtering, and who owns the grouping over time.
## What a suite is A *suite* is a class whose only job is to describe a set of tests to run. It holds no test methods of its own. In JUnit 5 this lives on the **JUnit Platform** layer (the layer that discovers and launches tests), not in Jupiter (the programming model you write `@Test` methods against). That distinction matters: a suite can pull in tests written for any engine on the classpath, not just Jupiter ones. ## The annotations ```java @Suite @SuiteDisplayName("API smoke suite") @SelectPackages("com.acme.api") @SelectClasses(LegacyOrderTest.class) class ApiSmokeSuite {} ``` - **`@Suite`** marks the class. It comes from `org.junit.platform.suite.api`. - **`@SelectClasses`** takes `Class<?>` literals — an explicit, refactoring-safe list. Best when the suite is a short, curated set. - **`@SelectPackages`** takes package names as strings and scans them *recursively*. Best when the grouping is structural ("everything under `com.acme.api`"). - **`@SuiteDisplayName`** sets the human-readable name in reports and IDEs. - Additional selectors mirror what the Launcher API can select: `@SelectMethod`, `@SelectClasspathResource`, `@SelectFile`, `@SelectDirectories`, `@SelectModules`, `@SelectUris`. Selectors are additive: several annotations on one class union their results. ## Filters run after selection Selection answers "what is a candidate?"; filters answer "which candidates survive?". - `@IncludeTags("fast")` / `@ExcludeTags("slow")` filter on Jupiter's `@Tag` values and accept tag *expressions* (`"fast & !flaky"`). - `@IncludeClassNamePatterns` / `@ExcludeClassNamePatterns` filter on fully-qualified class names by regex. - `@IncludeEngines` / `@ExcludeEngines` restrict which test engines participate (for example `@IncludeEngines("junit-jupiter")`). A subtle default trips people up: when you select *packages* (or the classpath), the platform applies a default class-name filter of `^(Test.*|.+[.$]Test.*|.*Tests?)$`. A class named `OrderChecks` is discovered by neither the suite nor a normal run unless you widen the pattern with `@IncludeClassNamePatterns(".*")`. With `@SelectClasses` you name the class directly, so the pattern does not get in the way. ## What actually runs the suite The class is inert on its own. `junit-platform-suite-engine` provides a `TestEngine` implementation that discovers `@Suite` classes and, for each one, creates an inner `Launcher` run using the selectors and filters you declared. So a suite execution is literally *a test run nested inside a test run*. Practical consequences: - Reports show the suite as a container, with the selected classes nested beneath it. - Because the inner run is a fresh launch, the suite can set its own configuration parameters via `@ConfigurationParameter`. - The annotations themselves come from `junit-platform-suite-api`; the aggregator artifact `junit-platform-suite` pulls in both API and engine, which is why depending on the aggregator is the least surprising choice. ## Requirements on the class The suite class must not be `private` and must not be abstract, must have a usable no-arg constructor (the default one is fine), and must carry at least one selector — a `@Suite` class with no selectors selects nothing. Nested/inner suite classes must be `static`. ## When you actually need one In JUnit 4, suites were the main way to group tests. In JUnit 5 the platform can filter by tag, package, class-name pattern and engine at launch time, so most grouping needs are met without a suite class. Suites earn their keep when the grouping must be **expressed in code and version-controlled** — a curated smoke set referenced by name, a suite that also pins configuration parameters (parallelism, instance lifecycle) for just that group, or a suite that must be runnable identically from an IDE and from a pipeline. The main cost is duplication: the selected classes normally still run in the ordinary test run as well, so they execute twice unless you deliberately keep the suite and the normal run disjoint.
- Why might @SelectPackages find fewer classes than you expect?Package selection applies the platform's default class-name filter, `^(Test.*|.+[.$]Test.*|.*Tests?)$`. Classes like `OrderChecks` or `VerifyPayment` do not match and are silently skipped. Add `@IncludeClassNamePatterns(".*")` (or a pattern that matches your convention) to widen it, or select those classes explicitly with `@SelectClasses`.
- Does a suite class have to be public?No. JUnit 5 only requires that the class is not private and not abstract, and that it has a no-arg constructor — package-private works fine. A nested suite class must be static. What it must have is at least one selector annotation; a bare `@Suite` class selects nothing and reports zero tests.
saying these in an interview costs you the question
- Thinking @Suite comes from Jupiter — it is a platform-level artifact, junit-platform-suite-api
- Assuming a @Suite class runs without junit-platform-suite-engine on the test runtime classpath
- Putting @Test methods inside the suite class and expecting them to run as part of it
- Believing @SelectPackages picks up every class in the package regardless of its name
- Assuming selecting a class in a suite stops it running in the normal test run — it runs twice