Skip to main content
JUnit · lesson 5 of 7
JUnit intermediate Lesson 5 of 7

JUnit Tags, Filtering, Ordering and Suites

Run the right JUnit 6 tests from Maven: @Tag with Surefire groups and tag expressions, method ordering, junit-platform.properties and @Suite classes.

By SkillByExample · Published

junit java maven tags test-suites

A real project has fast unit tests, slow ones, and ones that need a database. Running all of them on every save is slow, and running the wrong subset in CI misses bugs. This lesson labels tests with tags, picks subsets from the Maven command line, controls the order tests run in, and groups them into suites. It uses the project and mvn alias from Your First JUnit 6 Test with Maven:

alias mvn='docker run --rm -v "$PWD":/w -v m2:/root/.m2 -w /w maven:3.10.0-eclipse-temurin-21-noble mvn'

Tagging tests

Three test classes; each test prints its name so the runs below show exactly what ran.

// File: src/test/java/PriceTest.java
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

@Tag("fast")
class PriceTest {
    @Test void addsTax()      { System.out.println("ran PriceTest.addsTax"); }
    @Test void roundsPennies() { System.out.println("ran PriceTest.roundsPennies"); }
}
// File: src/test/java/RepositoryTest.java
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

@Tag("db")
class RepositoryTest {
    @Test @Tag("fast") void mapsRow()     { System.out.println("ran RepositoryTest.mapsRow"); }
    @Test @Tag("slow") void savesOrder()  { System.out.println("ran RepositoryTest.savesOrder"); }
}
// File: src/test/java/ExportTest.java
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

class ExportTest {
    @Test @Tag("slow") void writesPdf() { System.out.println("ran ExportTest.writesPdf"); }
    @Test void namesFile()              { System.out.println("ran ExportTest.namesFile"); }
}
$ mvn -B -ntp test | grep -E "^ran|Tests run: .*Skipped: [0-9]+$" | sort
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0
ran ExportTest.namesFile
ran ExportTest.writesPdf
ran PriceTest.addsTax
ran PriceTest.roundsPennies
ran RepositoryTest.mapsRow
ran RepositoryTest.savesOrder
$ mvn -B -ntp test -Dgroups=fast | grep -E "^ran|Tests run: .*Skipped: [0-9]+$" | sort
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
ran PriceTest.addsTax
ran PriceTest.roundsPennies
ran RepositoryTest.mapsRow
$ mvn -B -ntp test -DexcludedGroups=slow | grep -E "^ran|Tests run: .*Skipped: [0-9]+$" | sort
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
ran ExportTest.namesFile
ran PriceTest.addsTax
ran PriceTest.roundsPennies
ran RepositoryTest.mapsRow
$ mvn -B -ntp test -Dgroups='fast & !db' | grep -E "^ran|Tests run: .*Skipped: [0-9]+$" | sort
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
ran PriceTest.addsTax
ran PriceTest.roundsPennies

A tag on a class applies to every test in it, and a method can add more: RepositoryTest.mapsRow carries both db and fast. Surefire hands two properties to the JUnit Platform:

  • -Dgroups=fast runs only tests with that tag: 3 of the 6.
  • -DexcludedGroups=slow runs everything except that tag: 4, including the untagged ExportTest.namesFile.
  • -Dgroups='fast & !db' is a tag expression, with &, |, ! and parentheses. It dropped mapsRow, which is fast but also db. Quote it so the shell leaves & and ! alone.

Tests not selected are not run and not counted, unlike the Skipped tests in the conditions lesson. The sort keeps the ran lines in a stable order for this page, because Surefire doesn’t promise an order for test classes. To make a filter permanent, put <groups> or <excludedGroups> in Surefire’s <configuration>; the command-line property overrides it for one run.

Ordering methods with @Order

A test class that walks through one flow, step by step.

// File: src/test/java/CheckoutFlowTest.java
import org.junit.jupiter.api.*;

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class CheckoutFlowTest {
    @Test @Order(1) void createCart() { System.out.println("1 createCart"); }
    @Test @Order(2) void addItem()    { System.out.println("2 addItem"); }
    @Test @Order(3) void pay()        { System.out.println("3 pay"); }
    @Test void receipt()              { System.out.println("? receipt"); }
}
$ mvn -B -ntp test -Dtest=CheckoutFlowTest | grep -E "^[0-9?] "
1 createCart
2 addItem
3 pay
? receipt

MethodOrderer.OrderAnnotation sorts by @Order value, lowest first. receipt has no @Order, so it gets the default value and runs after every annotated method. The other built-in orderers are MethodName, DisplayName and Random.

Ordered tests that pass state to each other (the cart from step 1 used in step 2) are a smell: one failure cascades and no step can run alone. Ordering earns its place for slow integration flows and for readable reports, not as a way to share data.

A project-wide default in junit-platform.properties

Without an annotation, JUnit picks its default order. ExportTest from above has none:

$ mvn -B -ntp test -Dtest=ExportTest | grep "^ran"
ran ExportTest.writesPdf
ran ExportTest.namesFile

writesPdf ran before namesFile. The default isn’t alphabetical: JUnit documents it as deterministic but intentionally non-obvious, so it is the same from run to run but not something to rely on. A junit-platform.properties file on the test class path sets configuration parameters for the whole project:

# File: src/test/resources/junit-platform.properties
junit.jupiter.testmethod.order.default = org.junit.jupiter.api.MethodOrderer$MethodName
$ mvn -B -ntp test -Dtest=ExportTest | grep "^ran"
ran ExportTest.namesFile
ran ExportTest.writesPdf

Now every class without its own @TestMethodOrder runs in method-name order. The same file takes other settings shown in later lessons, such as parallel execution and default timeouts, and junit.jupiter.testclass.order.default orders classes the same way. A -D property with the same key on the Maven command line overrides the file.

Suites: a named selection

A suite is a class that selects tests by class, package or tag, so a named selection can be run the same way everywhere. Suites need the junit-platform-suite module (its version comes from the BOM):

<!-- pom.xml: inside <dependencies> -->
    <dependency>
      <groupId>org.junit.platform</groupId>
      <artifactId>junit-platform-suite</artifactId>
      <scope>test</scope>
    </dependency>
// File: src/test/java/FastSuite.java
import org.junit.platform.suite.api.IncludeTags;
import org.junit.platform.suite.api.SelectClasses;
import org.junit.platform.suite.api.Suite;

@Suite
@SelectClasses({PriceTest.class, RepositoryTest.class, ExportTest.class})
@IncludeTags("fast")
class FastSuite {
}
$ mvn -B -ntp test -Dtest=FastSuite | grep -E "^ran|Tests run: .*Skipped: [0-9]+$"
ran PriceTest.addsTax
ran PriceTest.roundsPennies
ran RepositoryTest.mapsRow
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
$ mvn -B -ntp test | grep -E "Tests run:.*-- in" | sed -E 's/.*Tests run: ([0-9]+),.*-- in (.*)/\2: \1 tests/' | sort
CheckoutFlowTest: 4 tests
ExportTest: 2 tests
PriceTest: 2 tests
RepositoryTest: 2 tests

FastSuite selected three classes and kept only their fast tests, the same 3 tests that -Dgroups=fast ran, but now the selection is code: it has a name, lives in version control and runs the same from an IDE. Other selectors include @SelectPackages, @SelectClasspathResource and @IncludeClassNamePatterns; @ExcludeTags is the opposite of @IncludeTags.

The plain mvn test ran four classes, 10 tests (the 6 tagged tests plus CheckoutFlowTest’s 4; the sed boils each Surefire class line down to its name and count), and FastSuite isn’t among them. Surefire only picks up classes named *Test, Test*, *Tests or *TestCase by default, and FastSuite matches none of them. That’s deliberate: if it were called FastSuiteTest, a plain run would execute those three tests twice, once directly and once through the suite. Name suites *Suite and run them with -Dtest.

Practice

  1. Run only the database tests that are not slow, with one tag expression.
Solution
$ mvn -B -ntp test -Dgroups='db & !slow' | grep -E "^ran|Tests run: .*Skipped: [0-9]+$"
ran RepositoryTest.mapsRow
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

Only RepositoryTest.mapsRow has db (from its class) without slow.

  1. Write a SlowSuite that runs every slow test from the three tagged classes.
Solution
// File: src/test/java/SlowSuite.java
import org.junit.platform.suite.api.IncludeTags;
import org.junit.platform.suite.api.SelectClasses;
import org.junit.platform.suite.api.Suite;

@Suite
@SelectClasses({PriceTest.class, RepositoryTest.class, ExportTest.class})
@IncludeTags("slow")
class SlowSuite {
}
$ mvn -B -ntp test -Dtest=SlowSuite | grep -E "^ran|Tests run: .*Skipped: [0-9]+$" | sort
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
ran ExportTest.writesPdf
ran RepositoryTest.savesOrder
  1. Order a class by display name instead of @Order, so the report reads as numbered steps.
Solution
// File: src/test/java/SignupFlowTest.java
import org.junit.jupiter.api.*;

@TestMethodOrder(MethodOrderer.DisplayName.class)
class SignupFlowTest {
    @Test @DisplayName("3. confirm email") void confirm() { System.out.println("3 confirm"); }
    @Test @DisplayName("1. enter details") void enter()   { System.out.println("1 enter"); }
    @Test @DisplayName("2. accept terms")  void accept()  { System.out.println("2 accept"); }
}
$ mvn -B -ntp test -Dtest=SignupFlowTest | grep -E "^[0-9] "
1 enter
2 accept
3 confirm

MethodOrderer.DisplayName sorts by the display name string, so the 1., 2., 3. prefixes decide the order, whatever the method names are.

Frequently Asked Questions

How do I run only JUnit tests with a certain tag in Maven?
Annotate tests or classes with @Tag("fast") and run mvn test -Dgroups=fast. Use -DexcludedGroups=slow to run everything except a tag. Both accept tag expressions such as 'fast & !db' with &, |, ! and parentheses; quote them so the shell doesn't interpret the symbols.
How do I make JUnit run test methods in a specific order?
Annotate the class with @TestMethodOrder(MethodOrderer.OrderAnnotation.class) and give methods @Order(1), @Order(2) and so on; methods without @Order run after the numbered ones. MethodOrderer.MethodName, DisplayName and Random are also built in. Without an orderer, JUnit's order is deterministic but intentionally non-obvious.
What is junit-platform.properties used for?
It is a properties file on the test class path, usually src/test/resources/junit-platform.properties, that sets JUnit configuration parameters for the whole project. Examples are junit.jupiter.testmethod.order.default for the default method order, the parallel execution settings and default timeouts. Annotations on a class still take precedence over these defaults.
How do I create a test suite in JUnit 6?
Add the junit-platform-suite dependency, then write an empty class annotated with @Suite plus selectors such as @SelectClasses, @SelectPackages or @IncludeTags. Run it with mvn test -Dtest=YourSuite. Name it something like FastSuite rather than *Test, or Surefire will also run it on every plain build and execute the selected tests twice.