skip to content

@Suite on the Platform

Declarative suites in JUnit 5 via junit-platform-suite — the modern answer to JUnit 4's Suite runner. Interviewers ask about selectors, filters, and per-suite configuration.

on this pageshow

questions

5

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?

level: juniorimportance: should knowfreq 38%

answer

  1. @Suite = class with no tests, only selectors
  2. @SelectClasses = explicit list; @SelectPackages = recursive scan
  3. Default name pattern ^(Test.*|.+[.$]Test.*|.*Tests?)$ bites @SelectPackages
  4. junit-platform-suite-engine runs it — nested Launcher
  5. Suite class: not private, not abstract, needs a selector

basics

~10 s

Put @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 s

A 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 lines
java
import 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

for a junior

Name @Suite plus @SelectClasses and @SelectPackages, and say the suite class holds no test methods.

for a middle

Add the filter annotations, the default class-name pattern gotcha, and that a separate suite engine artifact executes the class.

for a senior

Explain that the suite engine nests a Launcher run, what that buys (per-suite configuration, engine filtering), and the double-execution cost.

for a principal

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

context

open as a page

How do you make a JUnit 5 suite class run only the tests carrying certain @Tag values, and what expression syntax do the include and exclude filters accept?

level: middleimportance: should knowfreq 36%

basics

~20 s

Add @IncludeTags and/or @ExcludeTags to the @Suite class. Both accept tag expressions with ! (not), & (and), | (or), parentheses, and any()/none(). Exclusion wins: a test matching both an include and an exclude is not run.

open as a page

What does the JUnit 5 @ConfigurationParameter annotation on a suite class do, and how does it relate to a junit-platform.properties file and JVM system properties?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

@ConfigurationParameter sets a JUnit Platform configuration parameter (key/value) for that suite's nested run only — for example enabling parallel execution or per-class test instance lifecycle. It is supplied to the launcher, so it takes precedence over system properties and junit-platform.properties.

open as a page

Which artifact provides the engine that executes classes annotated with JUnit 5's @Suite, and what must such a class satisfy to be discovered and run?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

junit-platform-suite-engine provides the TestEngine that finds @Suite classes; junit-platform-suite-api provides the annotations (the junit-platform-suite aggregator pulls in both). The class must be non-private, non-abstract, have a no-arg constructor, be static if nested, and declare at least one selector.

open as a page

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?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Tags 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.

open as a page