How do I use tags to group JUnit tests?

In JUnit 5, you can use the @Tag annotation to group tests into categories such as:

  • fast
  • slow
  • unit
  • integration
  • database
  • api
  • smoke

Tags are useful when you want to run only certain groups of tests, for example only fast unit tests during development, or only integration tests in a CI pipeline.


1. Basic Example

Use @Tag on a test method:

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class CalculatorTest {

    @Test
    @Tag("fast")
    void shouldAddTwoNumbers() {
        Calculator calculator = new Calculator();

        int result = calculator.add(2, 3);

        assertEquals(5, result);
    }

    @Test
    @Tag("slow")
    void shouldCalculateLargeDataSet() {
        Calculator calculator = new Calculator();

        int result = calculator.processLargeDataSet();

        assertEquals(1000, result);
    }
}

In this example:

  • shouldAddTwoNumbers() belongs to the fast group.
  • shouldCalculateLargeDataSet() belongs to the slow group.

2. Tag an Entire Test Class

You can place @Tag on a class to apply the tag to all tests inside it.

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertTrue;

@Tag("unit")
class StringUtilsTest {

    @Test
    void shouldReturnTrueForBlankString() {
        assertTrue(StringUtils.isBlank(""));
    }

    @Test
    void shouldReturnTrueForWhitespaceString() {
        assertTrue(StringUtils.isBlank("   "));
    }
}

All tests in StringUtilsTest are now tagged as unit.


3. Use Multiple Tags

A test can have more than one tag.

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertNotNull;

class UserServiceTest {

    @Test
    @Tag("unit")
    @Tag("fast")
    void shouldCreateUser() {
        UserService userService = new UserService();

        User user = userService.createUser("[email protected]");

        assertNotNull(user);
    }

    @Test
    @Tag("integration")
    @Tag("database")
    void shouldSaveUserToDatabase() {
        UserRepository userRepository = new UserRepository();

        User user = userRepository.save(new User("[email protected]"));

        assertNotNull(user.getId());
    }
}

This allows you to run tests by different categories.

For example:

  • Run all unit tests.
  • Run all fast tests.
  • Run all integration tests.
  • Exclude all database tests.

4. Run Tagged Tests with Maven

If you are using Maven Surefire, you can run tests with a specific tag like this:

mvn test -Dgroups=unit

To run multiple tags:

mvn test -Dgroups=unit,fast

To exclude a tag:

mvn test -DexcludedGroups=slow

Example:

mvn test -Dgroups=integration -DexcludedGroups=slow

This runs tests tagged with integration, but excludes tests tagged with slow.


5. Configure Tags in pom.xml

You can also configure Maven to include or exclude tags permanently.

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.5.2</version>
            <configuration>
                <groups>unit</groups>
                <excludedGroups>slow</excludedGroups>
            </configuration>
        </plugin>
    </plugins>
</build>

This configuration runs tests tagged unit and excludes tests tagged slow.


6. Run Tagged Tests with Gradle

If you are using Gradle, configure the test task:

test {
    useJUnitPlatform {
        includeTags 'unit'
        excludeTags 'slow'
    }
}

Then run:

gradle test

You can also create separate test tasks:

tasks.register('integrationTest', Test) {
    useJUnitPlatform {
        includeTags 'integration'
    }
}

Run it with:

gradle integrationTest

7. Tag Integration Tests

A common use case is separating unit tests from integration tests.

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertTrue;

@Tag("integration")
class UserRepositoryTest {

    @Test
    void shouldConnectToDatabase() {
        boolean connected = true;

        assertTrue(connected);
    }
}

Then you can run only integration tests:

mvn test -Dgroups=integration

Or exclude them during regular builds:

mvn test -DexcludedGroups=integration

8. Tag Nested Tests

Tags also work with nested test classes.

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

class OrderServiceTest {

    @Nested
    @Tag("validation")
    class ValidationTests {

        @Test
        void shouldRejectInvalidOrder() {
            // validation test
        }

        @Test
        void shouldAcceptValidOrder() {
            // validation test
        }
    }

    @Nested
    @Tag("pricing")
    class PricingTests {

        @Test
        void shouldCalculateDiscount() {
            // pricing test
        }

        @Test
        void shouldApplyTax() {
            // pricing test
        }
    }
}

Here:

  • All tests in ValidationTests are tagged validation.
  • All tests in PricingTests are tagged pricing.

9. Tag Naming Rules

JUnit tag names must follow a few rules:

  • A tag must not be blank.
  • A tag must not contain whitespace.
  • A tag must not contain ISO control characters.
  • A tag must not contain reserved characters:
    • ,
    • (
    • )
    • &
    • |
    • !

Good examples:

@Tag("unit")
@Tag("integration")
@Tag("fast")
@Tag("database")
@Tag("smoke")

Avoid:

@Tag("unit test")
@Tag("fast,unit")
@Tag("integration&database")

10. Best Practices

Use tags for broad execution groups, not for every small detail.

Good tag categories:

  • unit
  • integration
  • slow
  • fast
  • database
  • api
  • smoke

Avoid over-tagging tests with too many labels.

For example, this is usually enough:

@Test
@Tag("integration")
@Tag("database")
void shouldSaveCustomer() {
}

But this is probably too much:

@Test
@Tag("integration")
@Tag("database")
@Tag("repository")
@Tag("customer")
@Tag("save")
@Tag("positive")
void shouldSaveCustomer() {
}

Summary

Use JUnit 5 @Tag to group and filter tests.

@Test
@Tag("fast")
void shouldRunQuickly() {
}

You can apply tags to:

  • Individual test methods
  • Entire test classes
  • Nested test classes

Then run selected groups using Maven or Gradle:

mvn test -Dgroups=fast
test {
    useJUnitPlatform {
        includeTags 'fast'
    }
}

Tags are especially useful for separating unit tests, integration tests, slow tests, and database-dependent tests.

How do I use @EnumSource to test enum values?

@EnumSource is a JUnit 5 parameterized-test source that runs the same test once for each selected enum constant.

Basic usage

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

import static org.junit.jupiter.api.Assertions.assertNotNull;

class DirectionTest {

    enum Direction {
        NORTH, SOUTH, EAST, WEST
    }

    @ParameterizedTest
    @EnumSource(Direction.class)
    void shouldTestAllDirections(Direction direction) {
        assertNotNull(direction);
    }
}

This runs the test 4 times: once with NORTH, SOUTH, EAST, and WEST.

Test only specific enum values

Use names to include selected constants:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

import static org.junit.jupiter.api.Assertions.assertTrue;

class DirectionTest {

    enum Direction {
        NORTH, SOUTH, EAST, WEST
    }

    @ParameterizedTest
    @EnumSource(value = Direction.class, names = {"NORTH", "EAST"})
    void shouldTestOnlyNorthAndEast(Direction direction) {
        assertTrue(direction == Direction.NORTH || direction == Direction.EAST);
    }
}

Exclude specific enum values

Use mode = EnumSource.Mode.EXCLUDE:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

import static org.junit.jupiter.api.Assertions.assertNotEquals;

class DirectionTest {

    enum Direction {
        NORTH, SOUTH, EAST, WEST
    }

    @ParameterizedTest
    @EnumSource(
        value = Direction.class,
        names = {"WEST"},
        mode = EnumSource.Mode.EXCLUDE
    )
    void shouldTestAllExceptWest(Direction direction) {
        assertNotEquals(Direction.WEST, direction);
    }
}

This runs for NORTH, SOUTH, and EAST.

Match enum names with a regex

Use mode = EnumSource.Mode.MATCH_ANY:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

import static org.junit.jupiter.api.Assertions.assertTrue;

class DirectionTest {

    enum Direction {
        NORTH, SOUTH, EAST, WEST
    }

    @ParameterizedTest
    @EnumSource(
        value = Direction.class,
        names = {"N.*", "E.*"},
        mode = EnumSource.Mode.MATCH_ANY
    )
    void shouldTestNamesStartingWithNOrE(Direction direction) {
        assertTrue(direction.name().startsWith("N") || direction.name().startsWith("E"));
    }
}

This runs for NORTH and EAST.

Common modes

EnumSource.Mode.INCLUDE     // default; include listed names
EnumSource.Mode.EXCLUDE     // exclude listed names
EnumSource.Mode.MATCH_ANY   // include names matching any regex
EnumSource.Mode.MATCH_ALL   // include names matching all regexes

Typical real-world example

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

import static org.junit.jupiter.api.Assertions.assertTrue;

class OrderStatusTest {

    enum OrderStatus {
        NEW, PAID, SHIPPED, CANCELLED
    }

    @ParameterizedTest
    @EnumSource(value = OrderStatus.class, names = {"PAID", "SHIPPED"})
    void completedStatusesShouldBeProcessed(OrderStatus status) {
        assertTrue(status == OrderStatus.PAID || status == OrderStatus.SHIPPED);
    }
}

In short:

@ParameterizedTest
@EnumSource(MyEnum.class)
void test(MyEnum value) {
    // test each enum value
}

How do I use @MethodSource for dynamic test data?

In JUnit 5, @MethodSource lets you supply test arguments from one or more factory methods. It is commonly used with @ParameterizedTest when your test data is too complex for @ValueSource or @CsvSource.

Basic example

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertTrue;

class StringTest {

    @ParameterizedTest
    @MethodSource("blankStrings")
    void shouldDetectBlankStrings(String input) {
        assertTrue(input == null || input.isBlank());
    }

    static Stream<String> blankStrings() {
        return Stream.of(null, "", " ", "\t", "\n");
    }
}

The method referenced by @MethodSource("blankStrings") provides the test data.

Supplying multiple arguments

If your test method has multiple parameters, return Stream<Arguments>.

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;

class CalculatorTest {

    @ParameterizedTest
    @MethodSource("additionCases")
    void shouldAddNumbers(int a, int b, int expected) {
        assertEquals(expected, a + b);
    }

    static Stream<Arguments> additionCases() {
        return Stream.of(
                Arguments.of(1, 2, 3),
                Arguments.of(5, 7, 12),
                Arguments.of(-1, 1, 0)
        );
    }
}

Using a method source without naming it

If the source method has the same name as the test method, you can omit the method name.

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertTrue;

class NumberTest {

    @ParameterizedTest
    @MethodSource
    void isEven(int number) {
        assertTrue(number % 2 == 0);
    }

    static Stream<Integer> isEven() {
        return Stream.of(2, 4, 6, 8);
    }
}

Dynamic test data

You can generate test data dynamically inside the provider method.

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.IntStream;

import static org.junit.jupiter.api.Assertions.assertTrue;

class RangeTest {

    @ParameterizedTest
    @MethodSource("numbersFromOneToTen")
    void shouldBePositive(int number) {
        assertTrue(number > 0);
    }

    static IntStream numbersFromOneToTen() {
        return IntStream.rangeClosed(1, 10);
    }
}

Using objects as test data

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertFalse;

class UserValidationTest {

    @ParameterizedTest
    @MethodSource("invalidUsers")
    void shouldRejectInvalidUsers(User user) {
        assertFalse(user.isValid());
    }

    static Stream<Arguments> invalidUsers() {
        return Stream.of(
                Arguments.of(new User("", "[email protected]")),
                Arguments.of(new User("Alice", "")),
                Arguments.of(new User(null, "[email protected]"))
        );
    }

    static class User {
        private final String name;
        private final String email;

        User(String name, String email) {
            this.name = name;
            this.email = email;
        }

        boolean isValid() {
            return name != null && !name.isBlank()
                    && email != null && !email.isBlank();
        }
    }
}

Referencing an external method source

You can also put test data providers in another class.

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import static org.junit.jupiter.api.Assertions.assertEquals;

class MathTest {

    @ParameterizedTest
    @MethodSource("com.example.TestData#additionCases")
    void shouldAddNumbers(int a, int b, int expected) {
        assertEquals(expected, a + b);
    }
}
package com.example;

import org.junit.jupiter.params.provider.Arguments;

import java.util.stream.Stream;

public class TestData {

    static Stream<Arguments> additionCases() {
        return Stream.of(
                Arguments.of(1, 2, 3),
                Arguments.of(10, 20, 30)
        );
    }
}

Depending on your setup, make the provider method public static if it is in a different package or class.

Common rules

For standard JUnit 5 usage:

  • The test must use @ParameterizedTest.
  • The provider method usually must be static.
  • The provider method can return:
    • Stream<T>
    • Stream<Arguments>
    • Collection<T>
    • Iterable<T>
    • arrays
    • primitive streams like IntStream, LongStream, or DoubleStream
  • Use Arguments.of(...) when passing multiple values.
  • The number and types of provided arguments must match the test method parameters.

Example with readable test names

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;

class DiscountTest {

    @ParameterizedTest(name = "{index}: price={0}, discount={1}, expected={2}")
    @MethodSource("discountCases")
    void shouldCalculateDiscount(double price, double discount, double expected) {
        assertEquals(expected, price * (1 - discount));
    }

    static Stream<Arguments> discountCases() {
        return Stream.of(
                Arguments.of(100.0, 0.10, 90.0),
                Arguments.of(200.0, 0.25, 150.0),
                Arguments.of(50.0, 0.00, 50.0)
        );
    }
}

In short, use @MethodSource when you want flexible, reusable, or dynamically generated test data for parameterized tests.

How do I use @CsvFileSource to load test data from a file?

Use JUnit 5’s @CsvFileSource with a parameterized test to load rows from a CSV file and pass each row into your test method.

1. Add the CSV file

Place the CSV file under src/test/resources, for example:

src/test/resources/test-data/users.csv

Example CSV:

username,age,active
alice,30,true
bob,25,false
charlie,40,true

2. Use @CsvFileSource

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

class UserCsvTest {

    @ParameterizedTest
    @CsvFileSource(
        resources = "/test-data/users.csv",
        numLinesToSkip = 1
    )
    void loadsUsersFromCsv(String username, int age, boolean active) {
        assertNotNull(username);
        assertTrue(age > 0);

        System.out.println(username + " " + age + " " + active);
    }
}

Key points

  • resources = "/test-data/users.csv" loads the file from the test classpath, usually src/test/resources.
  • The leading / means the path is absolute from the classpath root.
  • numLinesToSkip = 1 skips the header row.
  • Each CSV column maps to a test method parameter.
  • JUnit automatically converts common types like String, int, boolean, double, enums, etc.

CSV with custom delimiter

If your file uses semicolons:

username;age;active
alice;30;true
bob;25;false

Use:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;

class UserCsvTest {

    @ParameterizedTest
    @CsvFileSource(
        resources = "/test-data/users.csv",
        numLinesToSkip = 1,
        delimiter = ';'
    )
    void loadsUsersFromCsv(String username, int age, boolean active) {
        // test logic
    }
}

Loading from a filesystem path

If the file is not on the classpath, use files instead of resources:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;

class ExternalCsvTest {

    @ParameterizedTest
    @CsvFileSource(
        files = "src/test/resources/test-data/users.csv",
        numLinesToSkip = 1
    )
    void loadsUsersFromFile(String username, int age, boolean active) {
        // test logic
    }
}

Handling empty and null values

Example CSV:

name,email
Alice,[email protected]
Bob,
Charlie,NIL

Example test:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;

import static org.junit.jupiter.api.Assertions.assertNull;

class NullCsvTest {

    @ParameterizedTest
    @CsvFileSource(
        resources = "/test-data/users.csv",
        numLinesToSkip = 1,
        nullValues = "NIL"
    )
    void handlesNullValues(String name, String email) {
        if ("Charlie".equals(name)) {
            assertNull(email);
        }
    }
}

In this example:

  • Empty value after Bob, is treated as an empty string by default in many CSV cases.
  • NIL is explicitly converted to null.

Required dependency

For Maven, make sure you have junit-jupiter-params:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <scope>test</scope>
</dependency>

With Spring Boot tests, this is often already included through spring-boot-starter-test.

How do I use @CsvSource in JUnit parameterized tests?

To use @CsvSource in JUnit 5 parameterized tests, you define multiple sets of comma-separated input values directly inside the annotation. Each CSV row becomes one test invocation.

1. Add the Required Imports

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.assertEquals;

@CsvSource is part of JUnit Jupiter Params, so make sure your project includes the parameterized test dependency.

For Maven:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.11.0</version>
    <scope>test</scope>
</dependency>

For Gradle:

testImplementation 'org.junit.jupiter:junit-jupiter-params:5.11.0'

2. Basic @CsvSource Example

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.assertEquals;

class CalculatorTest {

    @ParameterizedTest
    @CsvSource({
        "1, 2, 3",
        "5, 7, 12",
        "10, 20, 30"
    })
    void shouldAddTwoNumbers(int a, int b, int expected) {
        int result = a + b;

        assertEquals(expected, result);
    }
}

Each line in @CsvSource maps to the method parameters:

"1, 2, 3"  -> a = 1, b = 2, expected = 3
"5, 7, 12" -> a = 5, b = 7, expected = 12

3. Using Strings with @CsvSource

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.assertEquals;

class StringTest {

    @ParameterizedTest
    @CsvSource({
        "java, JAVA",
        "junit, JUNIT",
        "test, TEST"
    })
    void shouldConvertTextToUppercase(String input, String expected) {
        assertEquals(expected, input.toUpperCase());
    }
}

JUnit automatically converts CSV values to the method parameter types when possible.

4. Handling Commas in Values

If a value contains a comma, wrap it in single quotes:

@ParameterizedTest
@CsvSource({
    "'Smith, John', John",
    "'Doe, Jane', Jane"
})
void shouldExtractFirstName(String fullName, String firstName) {
    String result = fullName.substring(fullName.indexOf(",") + 2);

    assertEquals(firstName, result);
}

Here, 'Smith, John' is treated as one argument instead of two.

5. Using Custom Delimiters

By default, @CsvSource uses a comma. You can change the delimiter:

@ParameterizedTest
@CsvSource(
    value = {
        "1|2|3",
        "4|5|9",
        "10|20|30"
    },
    delimiter = '|'
)
void shouldAddNumbersWithCustomDelimiter(int a, int b, int expected) {
    assertEquals(expected, a + b);
}

6. Null and Empty Values

JUnit treats an unquoted empty value as null:

@ParameterizedTest
@CsvSource({
    "apple, APPLE",
    ", UNKNOWN"
})
void shouldHandleNullValues(String input, String expected) {
    String result = input == null ? "UNKNOWN" : input.toUpperCase();

    assertEquals(expected, result);
}

You can use an empty string by quoting it:

@ParameterizedTest
@CsvSource({
    "'', empty"
})
void shouldHandleEmptyString(String input, String expected) {
    String result = input.isEmpty() ? "empty" : input;

    assertEquals(expected, result);
}

7. Adding Display Names

You can make test output easier to read using the name attribute:

@ParameterizedTest(name = "{index} => input={0}, expected={1}")
@CsvSource({
    "java, JAVA",
    "junit, JUNIT"
})
void shouldConvertToUppercase(String input, String expected) {
    assertEquals(expected, input.toUpperCase());
}

Example display names:

1 => input=java, expected=JAVA
2 => input=junit, expected=JUNIT

Key Points

  • Use @ParameterizedTest, not @Test.
  • Use @CsvSource to provide multiple comma-separated argument sets.
  • Each CSV row must match the number of method parameters.
  • JUnit automatically converts values to types like int, double, boolean, String, and enums.
  • Use single quotes for values containing commas.
  • Use quoted empty strings for ""; unquoted empty values are treated as null.
  • Use delimiter if comma-separated data is hard to read.