Power Your Software Testing with AI Agents and Cloud
The Native AI-Agentic Cloud Platform to Supercharge Quality Engineering. Test Intelligently and Ship Faster.
- TestMu AI (Formerly LambdaTest)
- /
- Learning Hub
- /
- JUnit Test Suites: Group and Run Tests With @Suite
JUnit Test Suites: Group and Run Tests With @Suite
Build a JUnit test suite with @Suite, @SelectClasses, @SelectPackages, and @IncludeTags, run it from Maven, Gradle, and the IDE, and migrate JUnit 4 suites.
Published on:
A release branch is cut on Friday afternoon and the team wants the twelve checkout tests to run before the deploy, not the whole 900-test build. In JUnit that request is a JUnit test suite: one class that selects a set of test classes and runs them as a unit. This chapter builds one with @Suite, shows every selector and filter the JUnit Platform offers, runs the suite from Maven, Gradle, and the IDE, and explains the two reasons a suite silently runs nothing or runs everything twice.
The examples assume you can write a test class; the What Is JUnit chapter covers the framework and its Platform, Jupiter, and Vintage modules.
TL;DR
A JUnit test suite is a class annotated with @Suite that selects other test classes by name, package, pattern, or tag and runs them together. It needs the junit-platform-suite dependency, runs from Maven with mvn test -Dtest=SuiteName, and replaces the JUnit 4 @RunWith(Suite.class) runner.
- @SelectClasses and @SelectPackages - the two selectors you will use most: an explicit list of classes, or every test under a package tree.
- @IncludeTags and @IncludeClassNamePatterns - filters that turn a package selection into a smoke suite or a regression suite without listing classes.
- @BeforeSuite and @AfterSuite - static methods that run once around the whole suite, for setup that is too expensive to repeat per class.
- Surefire's naming rule - Maven ignores a class named AllTestsSuite because it matches none of the default include patterns, so pass it with -Dtest or add an includes block.
- Scaling out - a suite groups tests but does not distribute them; TestMu AI HyperExecute runs each suite class on its own virtual machine and merges the reports.
What Is a JUnit Test Suite?
A JUnit test suite is a class that declares which tests to run instead of containing tests itself. On the JUnit Platform the JUnit Platform Suite Engine turns that class into a test plan: it reads the selector annotations, discovers the matching tests through the other engines, and executes them as children of the suite. The suite engine is itself a TestEngine, which is why a suite can combine Jupiter tests, JUnit 4 tests running on Vintage, and tests from any third-party engine in one run.
A suite earns its place over a plain mvn test in these situations:
- A named subset - a smoke suite for every commit, a checkout suite before a payments deploy, a slow suite for the nightly build.
- One-time setup around many classes - start a database container or a WireMock server once in @BeforeSuite instead of once per class.
- A fixed entry point for tooling - a CI job, an IDE run configuration, or a HyperExecute task that points at one class and always gets the same set of tests.
If the only goal is to run a subset, tags alone often do it with less code; the Suites vs Tags section below draws the line. The JUnit Test Cases chapter covers how to structure the test classes a suite selects.
Add the Suite Dependency
The junit-jupiter artifact does not include suites. The user guide names three artifacts: junit-platform-suite-api holds the annotations, junit-platform-suite-engine runs them, and junit-platform-suite aggregates both. With the junit-bom import from the JUnit Maven Dependency chapter in place, one extra dependency is enough:
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<scope>test</scope>
</dependency>
</dependencies>Gradle users add testImplementation(platform("org.junit:junit-bom:6.1.3")) once, then testImplementation("org.junit.platform:junit-platform-suite") next to the Jupiter dependency, and keep useJUnitPlatform() in the test task. Both modules are 6.1.3 at the time of writing, and because JUnit 6 requires Java 17, the project needs a JDK that most teams already run: the JetBrains State of Java 2025 report puts 40% of Java developers on Java 21 and 39% on Java 17. On an older JDK, stay on the JUnit 5.x line of the same artifacts.
Create a Suite With @Suite
The suite below is the one I ran for this chapter. It selects every class under the demo package whose name ends in Test, gives the suite a readable name, and prints a line before and after the whole run:
package suites;
import org.junit.platform.suite.api.AfterSuite;
import org.junit.platform.suite.api.BeforeSuite;
import org.junit.platform.suite.api.IncludeClassNamePatterns;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;
import org.junit.platform.suite.api.SuiteDisplayName;
@Suite
@SuiteDisplayName("All demo tests")
@SelectPackages("demo")
@IncludeClassNamePatterns(".*Test")
class AllTestsSuite {
@BeforeSuite
static void beforeSuite() {
System.out.println("[suite] starting all demo tests");
}
@AfterSuite
static void afterSuite() {
System.out.println("[suite] finished all demo tests");
}
}These details decide whether the class behaves:
- The body stays empty of @Test methods - a suite is a declaration; tests live in the selected classes.
- @BeforeSuite and @AfterSuite are static - the user guide requires it, the same rule as @BeforeAll and @AfterAll on a test class.
- @IncludeClassNamePatterns takes a regular expression - .*Test matches CalculatorTest but not AllTestsSuite, which keeps the suite from selecting itself.
- The suite lives in its own package - putting it in suites rather than demo means @SelectPackages("demo") never discovers the suite class, which is the simplest guard against recursion.
Select and Filter Tests
The junit-platform-suite-api package ships 30 annotations. Selectors say where to look; filters narrow what was found; configuration annotations tune the run. These are the ones that matter in practice:
| Annotation | What it does | Typical use |
|---|---|---|
| @SelectClasses | Selects the listed test classes. | A fixed checkout suite: @SelectClasses({CartTest.class, PaymentTest.class}) |
| @SelectPackages | Selects every test class in the named packages and their subpackages. | Everything under com.shop.api |
| @SelectMethod | Selects a single test method; repeatable. | Pin one known-flaky method into a diagnostic suite |
| @IncludePackages / @ExcludePackages | Keeps or drops subpackages of a selection. | Select com.shop but exclude com.shop.legacy |
| @IncludeClassNamePatterns / @ExcludeClassNamePatterns | Regex filters on class names. | ".*IT" for integration tests, ".*Test" for unit tests |
| @IncludeTags / @ExcludeTags | Filters by @Tag values; accepts tag expressions. | @IncludeTags("smoke"), @ExcludeTags("slow | flaky") |
| @IncludeEngines / @ExcludeEngines | Limits the run to specific engine IDs. | @ExcludeEngines("junit-vintage") during a migration |
| @ConfigurationParameter | Sets a Platform configuration key for this suite only. | Turn on parallel execution for one suite |
| @SuiteDisplayName | Names the suite in reports. | "Smoke tests" instead of SmokeSuite |
| @BeforeSuite / @AfterSuite | Static methods run once before and after all selected tests. | Start and stop a shared container |
A tag filter is the shortest route to a smoke suite. StringUtilsTest in the demo project carries @Tag("smoke"), so this suite runs it and skips CalculatorTest, which has no tag:
@Suite
@SuiteDisplayName("Smoke tests")
@SelectPackages("demo")
@IncludeTags("smoke")
class SmokeSuite {
}Tags combine with the rest of the tutorial: the JUnit Ignore Test chapter shows how @Disabled and the conditional annotations interact with a selected class, and the JUnit Nested Tests chapter explains how @Nested classes appear inside a suite's tree.
Run a Suite From Maven, Gradle, and the IDE
In IntelliJ IDEA or Eclipse, right-click the suite class and run it; the IDE passes the class to the Platform launcher and shows the suite as a tree. Maven is where people get stuck, for a reason the Surefire documentation states plainly: by default the plugin includes only classes matching **/Test*.java, **/*Test.java, **/*Tests.java, and **/*TestCase.java. AllTestsSuite and SmokeSuite match none of them, so a plain mvn test runs the two test classes directly and never touches the suites.
Any of these fixes works. Name the run explicitly with mvn test -Dtest=AllTestsSuite; rename the class to AllTests so the default pattern matches; or add an includes block to the Surefire configuration. I use the first in CI because it keeps the choice visible in the pipeline definition:
# run one suite
mvn test -Dtest=AllTestsSuite
# run only the tagged smoke tests through the suite
mvn test -Dtest=SmokeSuite
# Gradle: filter the test task to the suite class
./gradlew test --tests "suites.AllTestsSuite"I ran all three commands against the demo project on a TestMu AI HyperExecute Linux VM (job 21564636, Java 17, Surefire 3.6.0). The output confirms the naming rule: the plain run never mentions a suite, the suite run wraps the same classes in the @BeforeSuite and @AfterSuite lines, and the smoke run picks only the tagged class.
$ mvn -B test
[INFO] Running demo.StringUtilsTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.079 s -- in demo.StringUtilsTest
[INFO] Running demo.CalculatorTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.013 s -- in demo.CalculatorTest
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
$ mvn -B test -Dtest=AllTestsSuite
[INFO] Running suites.AllTestsSuite
[suite] starting all demo tests
[INFO] Running demo.CalculatorTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.056 s -- in demo.CalculatorTest
[INFO] Running demo.StringUtilsTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.008 s -- in demo.StringUtilsTest
[suite] finished all demo tests
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.110 s -- in suites.AllTestsSuite
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
$ mvn -B test -Dtest=SmokeSuite
[INFO] Running suites.SmokeSuite
[INFO] Running demo.StringUtilsTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.049 s -- in demo.StringUtilsTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0One line surprises people: Tests run: 0 ... in suites.AllTestsSuite. Surefire attributes each test to the class that declares it, so the suite's own row is always zero and the real counts sit on the selected classes and in the final total. A suite that reports zero everywhere, not just on its own row, selected nothing, which usually means a package name or class-name pattern that matches no class.
The opposite problem is duplicate execution, which the user guide calls out: if a suite selects classes that Surefire also matches on its own, a plain build runs those tests twice, once directly and once inside the suite. Keeping suite classes outside the default patterns and launching them with -Dtest avoids both problems at once. The Run JUnit from Command Line chapter covers the console launcher and the Gradle flags in more depth.
JUnit 4 Suites: @RunWith(Suite.class) and @SuiteClasses
JUnit 4 built suites with a runner. The JUnit 4 Suite javadoc describes it as the way to manually build a suite containing tests from many classes, the JUnit 4 equivalent of the JUnit 3 static suite() method:
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses({ CalculatorTest.class, StringUtilsTest.class })
public class AllTestsSuite {
}The mapping to JUnit 5 and 6 is direct: @RunWith(Suite.class) becomes @Suite, @SuiteClasses becomes @SelectClasses, and the package, pattern, and tag selectors are new. An old suite keeps running on the deprecated Vintage engine until it is converted; the JUnit 4 vs JUnit 5 chapter has the full annotation map, and JUnit 6 Migration covers what Vintage's deprecation means for the timeline.
Suites vs Tags vs testng.xml
Each of these mechanisms answers "run this subset", and picking the wrong one adds a class nobody maintains.
| Mechanism | Where the selection lives | Pick it when |
|---|---|---|
| @Tag plus -Dgroups=smoke | On the test method or class; the build command chooses the tag. | The subset is a property of each test and no shared setup is needed. |
| @Suite class | In a compiled Java class with selectors and filters. | You need @BeforeSuite setup, a fixed entry point for tooling, or a selection that mixes packages, patterns, and tags. |
| testng.xml (TestNG) | In an XML file read at run time. | Non-developers change what runs, or the project already standardized on TestNG. |
JUnit has no run-time file equivalent to testng.xml, and that is a deliberate trade: the suite is type-checked and refactors with the code. The JUnit vs TestNG chapter compares suite handling alongside the other differences, and the TestNG tutorial's testng.xml chapter shows the file-based approach for comparison.
Run Suites in Parallel and at Scale
A suite selects tests; it does not make them faster. Inside one JVM, junit.jupiter.execution.parallel.enabled=true in junit-platform.properties runs the selected classes concurrently, and @ConfigurationParameter sets the same key on a single suite. The JUnit Parallel Testing chapter covers the thread-safety rules that come with it.
Across machines, suite classes are the natural unit of distribution. HyperExecute discovers the suite classes with a one-line command, splits them across fresh Linux VMs with Auto-Split, runs each with mvn test -Dtest=$test, restores the .m2 cache keyed on the pom.xml checksum, and merges every task's Surefire reports into one job. TestMu AI documents that model as up to 70% faster than a hub-and-node grid, because each VM holds the code, the dependencies, and the runtime with no network hop between them. The YAML is the same one the HyperExecute getting started guide shows for Maven, with the discovery command pointed at the suites package.
Note: Run the smoke suite on every commit and the full suite on a schedule, on 3,000+ browser and OS combinations when the tests drive a browser. Try TestMu AI free!
Best Practices for JUnit Test Suites
- Prefer @SelectPackages with a filter over @SelectClasses - a package selection picks up new test classes automatically; an explicit list goes stale the week after it is written.
- Keep suites in their own package - it prevents a suite from selecting itself and keeps Surefire's default scan from running the tests twice.
- Name suites by intent, not by size - SmokeSuite, CheckoutSuite, and NightlySuite tell the pipeline reader what runs; AllTestsSuite is fine only when it really is everything.
- Use @BeforeSuite for shared infrastructure only - a container or a mock server belongs there; per-test state belongs in @BeforeEach so tests stay independent.
- Launch suites explicitly in CI - mvn test -Dtest=SmokeSuite in the pipeline file is easier to audit than a Surefire includes block buried in the pom.
- Tag first, suite second - if a tag and -Dgroups solve the problem, skip the suite class.
Conclusion
Add junit-platform-suite, create a class annotated with @Suite and @SelectPackages, put it in a package Surefire does not scan, and run it with mvn test -Dtest=SuiteName. That is the whole mechanism of a JUnit test suite; everything else in this chapter is choosing the right selector and avoiding a double run.
The next chapter, JUnit Jupiter, looks at the module whose tests a suite selects. If your suites already take longer than a coffee break, the HyperExecute guide linked above shows how to run each one on its own machine.
Author
Himanshu Sheth is the Director of Marketing (Technical Content) at TestMu AI, with over 8 years of hands-on experience in Selenium, Cypress, and other test automation frameworks. He has authored more than 130 technical blogs for TestMu AI, covering software testing, automation strategy, and CI/CD. At TestMu AI, he leads the technical content efforts across blogs, YouTube, and social media, while closely collaborating with contributors to enhance content quality and product feedback loops. He has done his graduation with a B.E. in Computer Engineering from Mumbai University. Before TestMu AI, Himanshu led engineering teams in embedded software domains at companies like Samsung Research, Motorola, and NXP Semiconductors. He is a core member of DZone and has been a speaker at several unconferences focused on technical writing and software quality.
Reviewer
Srinivasan Sekar is Director of Engineering at TestMu AI (formerly LambdaTest), where he leads engineering and open-source initiatives behind the Selenium and Appium automation grid and owns TestMu AI's MCP Server. A committer to Appium and a contributor to Selenium, WebdriverIO, Taiko, and AppiumTestDistribution, he brings over 15 years of experience in quality engineering and open-source technologies. He is the author of the Apress book 'The MCP Standard: A Developer's Guide to Building Universal AI Tools with the Model Context Protocol,' a Certified Kubernetes and Cloud Native Associate, and an international conference speaker. Before TestMu AI he spent over eight years at Thoughtworks as a Principal Consultant and Quality Architect. Srinivasan holds a B.Tech in Information Technology from Anna University.
JUnit Test Suites FAQs
Did you find this page helpful?
More Related Learning Hubs
TestMu AI forEnterprise
Get access to solutions built on Enterprise
grade security, privacy, & compliance
- Advanced access controls
- Advanced data retention rules
- Advanced Local Testing
- Premium Support options
- Early access to beta features
- Private Slack Channel
- Unlimited Manual Accessibility DevTools Tests


