As a Java project grows, a flat test suite becomes unwieldy. You need to run a quick smoke test in 30 seconds before a commit, a full regression suite overnight, and a subset of integration tests on a staging branch. JUnit 6 Tags and Test Suites are the mechanism that makes all of this possible — without changing a single line of test logic.
This guide covers everything: the @Tag annotation, tag inheritance, filtering with Maven and Gradle, building explicit test suites with the Platform Suite Engine, and real-world tagging strategies used in production projects.
What Is a Tag in JUnit 6?
A tag is a string label attached to a test class or test method using the @Tag annotation. Tags are then used at build time to include or exclude groups of tests. They replace JUnit 4’s @Category annotation with a simpler, more flexible string-based system.
Applying @Tag to Tests
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import static org.junit.jupiter.api.Assertions.*;
// @Tag on the class applies to ALL tests in the class
@Tag("unit")
@Tag("order-service")
@DisplayName("OrderService — business logic tests")
class OrderServiceTest {
// Inherits class-level tags: "unit" and "order-service"
@Test
@DisplayName("Creating an order sets status to PENDING")
void creatingOrderSetsPendingStatus() {
OrderService service = new OrderService();
Order order = service.createOrder("[email protected]", 49.99);
assertEquals(OrderStatus.PENDING, order.getStatus());
}
// Additional method-level tag: this test has "unit", "order-service", AND "fast"
@Test
@Tag("fast")
@DisplayName("Order total calculation is accurate")
void orderTotalCalculationIsAccurate() {
OrderService service = new OrderService();
Order order = service.createOrder("[email protected]", 99.99);
assertEquals(99.99, order.getTotal(), 0.001);
}
// Tag a slow test so it can be excluded from quick builds
@Test
@Tag("slow")
@DisplayName("Processing 10000 orders completes within time limit")
void processingLargeVolumeOfOrdersCompletesInTime() {
// Simulate heavy processing
long startTime = System.currentTimeMillis();
for (int orderIndex = 0; orderIndex < 10_000; orderIndex++) {
new OrderService().createOrder("c" + orderIndex + "@test.com", 1.0);
}
long duration = System.currentTimeMillis() - startTime;
assertTrue(duration < 5000, "10,000 orders should process in under 5 seconds");
}
}
// Integration test class — tagged separately from unit tests
@Tag("integration")
@Tag("database")
@Tag("slow")
class OrderRepositoryIT {
@Test
@DisplayName("Saved order can be retrieved by ID")
void savedOrderCanBeRetrievedById() {
// hits a real database
}
}
Filtering Tests by Tag in Maven
Configure Maven Surefire Plugin to include or exclude tags in your pom.xml:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<!-- Only run tests tagged 'unit' -->
<groups>unit</groups>
<!-- Or: exclude slow tests from the default build -->
<!-- <excludedGroups>slow</excludedGroups> -->
<!-- Or: use tag expressions (AND, OR, NOT logic) -->
<!-- <groups>unit & !slow</groups> -->
</configuration>
</plugin>
Override at build time from the CLI:
# Run only fast unit tests
mvn test -Dgroups="fast"
# Run all tests EXCEPT slow ones
mvn test -DexcludedGroups="slow"
# Run unit tests that are NOT slow (tag expression)
mvn test -Dgroups="unit & !slow"
# Run either unit OR integration tests
mvn test -Dgroups="unit | integration"
Filtering Tests by Tag in Gradle
// build.gradle — configure default tag filter
test {
useJUnitPlatform {
// Include only tests tagged 'unit' in normal builds
includeTags 'unit'
// Exclude slow tests
// excludeTags 'slow'
}
testLogging {
events 'passed', 'failed', 'skipped'
}
}
// Create a separate Gradle task for integration tests
task integrationTest(type: Test) {
description = 'Runs integration tests tagged with @Tag("integration")'
group = 'verification'
useJUnitPlatform {
includeTags 'integration'
}
// Run integration tests after unit tests
shouldRunAfter test
}
# Run the integration test task
./gradlew integrationTest
# Override tag filter from CLI
./gradlew test -Pinclude.tags=fast
Tag Expressions: AND, OR, NOT
JUnit 6 supports boolean tag expressions for fine-grained filtering:
| Expression | Runs tests that are… |
|---|---|
unit | Tagged unit |
unit & fast | Tagged both unit AND fast |
unit | integration | Tagged unit OR integration |
!slow | NOT tagged slow |
unit & !slow | Tagged unit AND NOT slow |
(unit | integration) & !database | Unit or integration tests, but not ones hitting a DB |
Building a Test Suite with @Suite
For explicit test suites — running a specific combination of classes regardless of tags — use the JUnit Platform Suite Engine:
<!-- Add the suite engine dependency -->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<version>1.11.0</version>
<scope>test</scope>
</dependency>
import org.junit.platform.suite.api.*;
// Suite 1: Run all tests in a specific package
@Suite
@SelectPackages("com.example.order")
@DisplayName("Order Module — Full Test Suite")
class OrderModuleSuite {}
// Suite 2: Run tests by tag
@Suite
@IncludeTags("unit")
@ExcludeTags("slow")
@DisplayName("Fast Unit Tests Suite")
class FastUnitTestsSuite {}
// Suite 3: Run specific classes explicitly
@Suite
@SelectClasses({
OrderServiceTest.class,
OrderRepositoryIT.class,
OrderControllerTest.class
})
@DisplayName("Order End-to-End Suite")
class OrderE2ESuite {}
// Suite 4: Combine package selection with tag filtering
@Suite
@SelectPackages("com.example")
@IncludeTags("integration")
@ExcludeClassNamePatterns(".*Performance.*") // exclude perf test classes
@DisplayName("Integration Tests — Excluding Performance")
class IntegrationSuite {}
Recommended Tagging Strategy
| Tag | Applied to | When to run |
|---|---|---|
unit | Pure unit tests (no I/O) | Every commit, every build |
integration | Tests requiring DB, server, container | PR builds, nightly |
fast | Tests completing in <100ms | Pre-commit hook |
slow | Tests taking >1s | Nightly / scheduled CI |
smoke | Critical path happy-path tests | Post-deploy verification |
regression | Full coverage tests | Before release |
feature-x | Tests for a specific feature | While developing that feature |
Creating Custom Composed Annotations
Avoid repeating @Tag("unit") @Tag("fast") on every method by creating composed meta-annotations:
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import java.lang.annotation.*;
// Custom annotation: @FastUnitTest = @Test + @Tag("unit") + @Tag("fast")
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@Tag("unit")
@Tag("fast")
public @interface FastUnitTest {}
// Custom annotation: @SlowIntegrationTest = @Test + @Tag("integration") + @Tag("slow")
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Test
@Tag("integration")
@Tag("slow")
public @interface SlowIntegrationTest {}
// Usage — clean, no repetition
class PaymentServiceTest {
@FastUnitTest
@DisplayName("Payment request validation works correctly")
void paymentRequestValidationWorks() {
// runs as @Test + tagged "unit" + tagged "fast"
}
@SlowIntegrationTest
@DisplayName("Payment is processed through real payment gateway")
void paymentProcessedThroughRealGateway() {
// runs as @Test + tagged "integration" + tagged "slow"
}
}
Frequently Asked Questions (FAQs)
Q1: What characters are allowed in JUnit 6 tag names?
Tags must not contain whitespace, and should not contain ISO control characters. Alphanumeric characters, hyphens (-), and underscores (_) are always safe. Dots (.) are allowed but can conflict with package-style naming conventions. JUnit 6 validates tag syntax at runtime and will throw an exception if an invalid tag is used.
Q2: Do tags defined on a class apply to @Nested inner classes?
Yes. Tags on an outer class are inherited by all tests in that class, including those inside @Nested inner classes. Tags applied directly to a @Nested class apply only to tests within that nested class. You can combine outer and inner class tags for fine-grained control. See JUnit 6 Nested Tests for composition details.
Q3: Can I use @Tag with @ParameterizedTest?
Yes. @Tag works with any JUnit 6 test annotation including @ParameterizedTest, @TestFactory, and @RepeatedTest. The tag applies to the entire parameterized test (all invocations), not individual invocations. If one tag causes the entire method to be excluded, all its parameterized invocations are excluded too.
Q4: What is the difference between @Suite and just using tag filters?
@Suite creates an explicit, named collection of tests that you can run as a single unit. Tag filters are dynamic — any test with the matching tag is included. Use suites when you need a precisely defined, reproducible subset of tests that will not accidentally include new tests added later. Use tag filters when you want all tests matching a category to be included automatically as new tests are added.
Q5: How do I run a specific test suite from the Maven CLI?
Point Maven Surefire at the suite class using the -Dtest flag: mvn test -Dtest=FastUnitTestsSuite. The suite class itself has no @Test methods — it is discovered as a test container by the Platform Suite Engine, which then launches the selected tests. Make sure the junit-platform-suite dependency is on your classpath.
See Also
- JUnit 6 Tutorial: Complete Series Index
- JUnit 6 with Maven and Gradle: Complete Setup Guide
- Structuring Tests with JUnit 6 Nested Tests
- Running JUnit 6 Tests in CI/CD Pipelines
- Parallel Test Execution in JUnit 6
Conclusion
Tags and suites transform a monolithic test run into a strategic, flexible test execution system. Tag your tests consistently from day one — unit, integration, fast, slow — and you gain the ability to run exactly what you need, when you need it, without modifying build scripts or test code. Combined with custom composed annotations, the overhead of tagging becomes near-zero.
Next: JUnit 6 Extensions Model — build reusable extensions that inject behaviour before, after, and around your tests without touching test code.