Tags and Test Suites in JUnit 6: Organizing Large Test Bases

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 &amp; !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:

ExpressionRuns tests that are…
unitTagged unit
unit & fastTagged both unit AND fast
unit | integrationTagged unit OR integration
!slowNOT tagged slow
unit & !slowTagged unit AND NOT slow
(unit | integration) & !databaseUnit 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

TagApplied toWhen to run
unitPure unit tests (no I/O)Every commit, every build
integrationTests requiring DB, server, containerPR builds, nightly
fastTests completing in <100msPre-commit hook
slowTests taking >1sNightly / scheduled CI
smokeCritical path happy-path testsPost-deploy verification
regressionFull coverage testsBefore release
feature-xTests for a specific featureWhile 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

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.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.