eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

Partner – LambdaTest – NPI EA (cat= Testing)
announcement - icon

Distributed systems often come with complex challenges such as service-to-service communication, state management, asynchronous messaging, security, and more.

Dapr (Distributed Application Runtime) provides a set of APIs and building blocks to address these challenges, abstracting away infrastructure so we can focus on business logic.

In this tutorial, we'll focus on Dapr's pub/sub API for message brokering. Using its Spring Boot integration, we'll simplify the creation of a loosely coupled, portable, and easily testable pub/sub messaging system:

>> Flexible Pub/Sub Messaging With Spring Boot and Dapr

Partner – Diagrid – NPI (cat= Testing)
announcement - icon

Distributed systems often come with complex challenges such as service-to-service communication, state management, asynchronous messaging, security, and more.

Dapr (Distributed Application Runtime) provides a set of APIs and building blocks to address these challenges, abstracting away infrastructure so we can focus on business logic.

In this tutorial, we'll focus on Dapr's pub/sub API for message brokering. Using its Spring Boot integration, we'll simplify the creation of a loosely coupled, portable, and easily testable pub/sub messaging system:

>> Flexible Pub/Sub Messaging With Spring Boot and Dapr

1. Overview

JUnit 5, the next generation of JUnit, facilitates writing developer tests with shiny new features. One such feature is parameterized tests. This feature enables us to execute a single test method multiple times with different parameters.

In this tutorial, we’re going to explore parameterized tests in-depth, so let’s get started.

2. Dependencies

In order to use JUnit 5 parameterized tests, we need to import the junit-jupiter-params artifact from the JUnit Platform. That means, when using Maven, we’ll add the following to our pom.xml:

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

Also, when using Gradle, we’ll specify it a little differently:

testCompile("org.junit.jupiter:junit-jupiter-params:5.10.0")

3. First Impression

Let’s say we have an existing utility function, and we’d like to be confident about its behavior:

public class Numbers {
    public static boolean isOdd(int number) {
        return number % 2 != 0;
    }
}

Parameterized tests are like other tests except that we add the @ParameterizedTest annotation:

@ParameterizedTest
@ValueSource(ints = {1, 3, 5, -3, 15, Integer.MAX_VALUE}) // six numbers
void isOdd_ShouldReturnTrueForOddNumbers(int number) {
    assertTrue(Numbers.isOdd(number));
}

JUnit 5 test runner executes this above test — and consequently, the isOdd method — six times. And each time, it assigns a different value from the @ValueSource array to the number method parameter.

So, this example shows us two things we need for a parameterized test:

  • a source of arguments, in this case, an int array
  • a way to access them, in this case, the number parameter

There is still another aspect not evident with this example, so we’ll keep looking.

4. Argument Sources

As we should know by now, a parameterized test executes the same test multiple times with different arguments.

And we can hopefully do more than just numbers, so let’s explore.

4.1. Simple Values

With the @ValueSource annotation, we can pass an array of literal values to the test method.

Suppose we’re going to test our simple isBlank method:

public class Strings {
    public static boolean isBlank(String input) {
        return input == null || input.trim().isEmpty();
    }
}

We expect this method to return true for null for blank strings. So, we can write a parameterized test to assert this behavior:

@ParameterizedTest
@ValueSource(strings = {"", "  "})
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input) {
    assertTrue(Strings.isBlank(input));
}

As we can see, JUnit will run this test two times and each time assigns one argument from the array to the method parameter.

One of the limitations of value sources is that they only support these types:

  • short (with the shorts attribute)
  • byte (bytes attribute)
  • int (ints attribute)
  • long (longs attribute)
  • float (floats attribute)
  • double (doubles attribute)
  • char (chars attribute)
  • java.lang.String (strings attribute)
  • java.lang.Class (classes attribute)

Also, we can only pass one argument to the test method each time.

Before going any further, note that we didn’t pass null as an argument. That’s another limitation — we can’t pass null through a @ValueSource, even for String and Class.

4.2. Null and Empty Values

As of JUnit 5.4, we can pass a single null value to a parameterized test method using @NullSource:

@ParameterizedTest
@NullSource
void isBlank_ShouldReturnTrueForNullInputs(String input) {
    assertTrue(Strings.isBlank(input));
}

Since primitive data types can’t accept null values, we can’t use the @NullSource for primitive arguments.

Quite similarly, we can pass empty values using the @EmptySource annotation:

@ParameterizedTest
@EmptySource
void isBlank_ShouldReturnTrueForEmptyStrings(String input) {
    assertTrue(Strings.isBlank(input));
}

@EmptySource passes a single empty argument to the annotated method.

For String arguments, the passed value would be as simple as an empty String. Moreover, this parameter source can provide empty values for Collection types and arrays.

To pass both null and empty values, we can use the composed @NullAndEmptySource annotation:

@ParameterizedTest
@NullAndEmptySource
void isBlank_ShouldReturnTrueForNullAndEmptyStrings(String input) {
    assertTrue(Strings.isBlank(input));
}

As with the @EmptySource, the composed annotation works for Strings, Collections, and arrays.

To pass a few more empty string variations to the parameterized test, we can combine @ValueSource, @NullSource, and @EmptySource:

@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {"  ", "\t", "\n"})
void isBlank_ShouldReturnTrueForAllTypesOfBlankStrings(String input) {
    assertTrue(Strings.isBlank(input));
}

4.3. Enum

To run a test with different values from an enumeration, we can use the @EnumSource annotation.

For example, we can assert that all month numbers are between 1 and 12:

@ParameterizedTest
@EnumSource(Month.class) // passing all 12 months
void getValueForAMonth_IsAlwaysBetweenOneAndTwelve(Month month) {
    int monthNumber = month.getValue();
    assertTrue(monthNumber >= 1 && monthNumber <= 12);
}

Or, we can filter out a few months by using the names attribute.

We could also assert the fact that April, September, June and November are 30 days long:

@ParameterizedTest
@EnumSource(value = Month.class, names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"})
void someMonths_Are30DaysLong(Month month) {
    final boolean isALeapYear = false;
    assertEquals(30, month.length(isALeapYear));
}

By default, the names will only keep the matched enum values.

We can turn this around by setting the mode attribute to EXCLUDE:

@ParameterizedTest
@EnumSource(
  value = Month.class,
  names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER", "FEBRUARY"},
  mode = EnumSource.Mode.EXCLUDE)
void exceptFourMonths_OthersAre31DaysLong(Month month) {
    final boolean isALeapYear = false;
    assertEquals(31, month.length(isALeapYear));
}

In addition to literal strings, we can pass a regular expression to the names attribute:

@ParameterizedTest
@EnumSource(value = Month.class, names = ".+BER", mode = EnumSource.Mode.MATCH_ANY)
void fourMonths_AreEndingWithBer(Month month) {
    EnumSet<Month> months =
      EnumSet.of(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER, Month.DECEMBER);
    assertTrue(months.contains(month));
}

Quite similar to @ValueSource, @EnumSource is only applicable when we’re going to pass just one argument per test execution.

4.4. CSV Literals

Suppose we’re going to make sure that the toUpperCase() method from String generates the expected uppercase value. @ValueSource won’t be enough.

To write a parameterized test for such scenarios, we have to

  • Pass an input value and an expected value to the test method
  • Compute the actual result with those input values
  • Assert the actual value with the expected value

So, we need argument sources capable of passing multiple arguments.

The @CsvSource is one of those sources:

@ParameterizedTest
@CsvSource({"test,TEST", "tEst,TEST", "Java,JAVA"})
void toUpperCase_ShouldGenerateTheExpectedUppercaseValue(String input, String expected) {
    String actualValue = input.toUpperCase();
    assertEquals(expected, actualValue);
}

The @CsvSource accepts an array of comma-separated values, and each array entry corresponds to a line in a CSV file.

This source takes one array entry each time, splits it by a comma, and passes each array to the annotated test method as separate parameters.

By default, the comma is the column separator, but we can customize it using the delimiter attribute:

@ParameterizedTest
@CsvSource(value = {"test:test", "tEst:test", "Java:java"}, delimiter = ':')
void toLowerCase_ShouldGenerateTheExpectedLowercaseValue(String input, String expected) {
    String actualValue = input.toLowerCase();
    assertEquals(expected, actualValue);
}

Now it’s a colon-separated value, so still a CSV.

4.5. CSV Files

Instead of passing the CSV values inside the code, we can refer to an actual CSV file.

For example, we could use a CSV file like this:

input,expected
test,TEST
tEst,TEST
Java,JAVA

We can load the CSV file and ignore the header column with @CsvFileSource:

@ParameterizedTest
@CsvFileSource(resources = "/data.csv", numLinesToSkip = 1)
void toUpperCase_ShouldGenerateTheExpectedUppercaseValueCSVFile(
  String input, String expected) {
    String actualValue = input.toUpperCase();
    assertEquals(expected, actualValue);
}

The resources attribute represents the CSV file resources on the classpath to read. And, we can pass multiple files to it.

The numLinesToSkip attribute represents the number of lines to skip when reading the CSV files. By default, @CsvFileSource does not skip any lines, but this feature is usually useful for skipping the header lines as we did here.

Just like the simple @CsvSource, the delimiter is customizable with the delimiter attribute.

In addition to the column separator, we have these capabilities:

  • The line separator can be customized using the lineSeparator attribute — a newline is the default value.
  • The file encoding is customizable using the encoding attribute — UTF-8 is the default value.

4.6. Method

The argument sources we’ve covered so far are somewhat simple and share one limitation. It’s hard or impossible to pass complex objects using them.

One approach to providing more complex arguments is to use a method as an argument source.

Let’s test the isBlank method with a @MethodSource:

@ParameterizedTest
@MethodSource("provideStringsForIsBlank")
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input, boolean expected) {
    assertEquals(expected, Strings.isBlank(input));
}

The name we supply to @MethodSource needs to match an existing method.

So, let’s next write provideStringsForIsBlank, static method that returns a Stream of Arguments:

private static Stream<Arguments> provideStringsForIsBlank() {
    return Stream.of(
      Arguments.of(null, true),
      Arguments.of("", true),
      Arguments.of("  ", true),
      Arguments.of("not blank", false)
    );
}

Here we’re returning a stream of arguments, but it’s not a strict requirement. For example, we can return any other collection-like interfaces like List. 

If we’re going to provide just one argument per test invocation, then it’s not necessary to use the Arguments abstraction:

@ParameterizedTest
@MethodSource // hmm, no method name ...
void isBlank_ShouldReturnTrueForNullOrBlankStringsOneArgument(String input) {
    assertTrue(Strings.isBlank(input));
}

private static Stream<String> isBlank_ShouldReturnTrueForNullOrBlankStringsOneArgument() {
    return Stream.of(null, "", "  ");
}

When we don’t provide a name for the @MethodSource, JUnit will search for a source method with the same name as the test method.

Sometimes, it’s useful to share arguments between different test classes. In these cases, we can refer to a source method outside of the current class by its fully qualified name:

class StringsUnitTest {
    @ParameterizedTest
    @MethodSource("com.baeldung.parameterized.StringParams#blankStrings")
    void isBlank_ShouldReturnTrueForNullOrBlankStringsExternalSource(String input) {
        assertTrue(Strings.isBlank(input));
    }
}

public class StringParams {

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

Using the FQN#methodName format, we can refer to an external static method.

4.7. Field

Using a method as the argument source proved to be a useful way to supply the test data. Consequently, starting with JUnit 5.11, we can now use a similar feature with static fields, through the experimental annotation @FieldSource:

static List<String> cities = Arrays.asList("Madrid", "Rome", "Paris", "London");

@ParameterizedTest
@FieldSource("cities")
void isBlank_ShouldReturnFalseWhenTheArgHasAtLEastOneCharacter(String arg) {
    assertFalse(Strings.isBlank(arg));
}

As we can see, the annotation points to a static field referencing the test data, which can be represented as a Collection, an Iterable, an object array, or a Supplier<Stream>. After that, the parameterized test will be executed for each test input. Similar to @MethodSource, if the name of the static field matches the name of the test, the value of the annotation can be omitted:

static String[] isEmpty_ShouldReturnFalseWhenTheArgHasAtLEastOneCharacter = { "Spain", "Italy", "France", "England" };

@ParameterizedTest
@FieldSource
void isEmpty_ShouldReturnFalseWhenTheArgHasAtLEastOneCharacter(String arg) {
    assertFalse(arg.isEmpty());
}

4.8. Custom Argument Provider

Another advanced approach to pass test arguments is to use a custom implementation of an interface called ArgumentsProvider:

class BlankStringsArgumentsProvider implements ArgumentsProvider {
    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Stream.of(
          Arguments.of((String) null), 
          Arguments.of(""), 
          Arguments.of("   ") 
        );
    }
}

Then we can annotate our test with the @ArgumentsSource annotation to use this custom provider:

@ParameterizedTest
@ArgumentsSource(BlankStringsArgumentsProvider.class)
void isBlank_ShouldReturnTrueForNullOrBlankStringsArgProvider(String input) {
    assertTrue(Strings.isBlank(input));
}

Let’s make the custom provider a more pleasant API to use with a custom annotation.

4.9. Custom Annotation

Suppose we want to load the test arguments from a static variable:

static Stream<Arguments> arguments = Stream.of(
  Arguments.of(null, true), // null strings should be considered blank
  Arguments.of("", true),
  Arguments.of("  ", true),
  Arguments.of("not blank", false)
);

@ParameterizedTest
@VariableSource("arguments")
void isBlank_ShouldReturnTrueForNullOrBlankStringsVariableSource(
  String input, boolean expected) {
    assertEquals(expected, Strings.isBlank(input));
}

Actually, JUnit 5 does not provide this. However, we can roll our own solution.

First, we can create an annotation:

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ArgumentsSource(VariableArgumentsProvider.class)
public @interface VariableSource {

    /**
     * The name of the static variable
     */
    String value();
}

Then we need to somehow consume the annotation details and provide test arguments. JUnit 5 provides two abstractions to achieve those:

  • AnnotationConsumer to consume the annotation details
  • ArgumentsProvider to provide test arguments

So, we next need to make the VariableArgumentsProvider class read from the specified static variable and return its value as test arguments:

class VariableArgumentsProvider 
  implements ArgumentsProvider, AnnotationConsumer<VariableSource> {

    private String variableName;

    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return context.getTestClass()
                .map(this::getField)
                .map(this::getValue)
                .orElseThrow(() -> 
                  new IllegalArgumentException("Failed to load test arguments"));
    }

    @Override
    public void accept(VariableSource variableSource) {
        variableName = variableSource.value();
    }

    private Field getField(Class<?> clazz) {
        try {
            return clazz.getDeclaredField(variableName);
        } catch (Exception e) {
            return null;
        }
    }

    @SuppressWarnings("unchecked")
    private Stream<Arguments> getValue(Field field) {
        Object value = null;
        try {
            value = field.get(null);
        } catch (Exception ignored) {}

        return value == null ? null : (Stream<Arguments>) value;
    }
}

And it works like a charm.

5. Repeatable Argument Source Annotations

In the previous section, we used various annotations to supply arguments for our parameterized tests. Starting with JUnit version 5.11, most of these annotations were improved and became repeatable. As a result, we can annotate a parameterized test multiple times with the same argument source annotation.

For example, we can use @MethodSource twice to execute the test with all the elements supplied by two different methods:

static List<String> asia() {
    return Arrays.asList("Japan", "India", "Thailand");
}

static List<String> europe() {
    return Arrays.asList("Spain", "Italy", "England");
}

@ParameterizedTest
@MethodSource("asia")
@MethodSource("europe")
void whenStringIsLargerThanThreeCharacters_thenReturnTrue(String country) {
    assertTrue(country.length() > 3);
}

As we can see, this can be a convenient way of running the test with data coming from different sources. Here are all the argument source annotations supporting this feature:

  • @ValueSource
  • @EnumSource
  • @MethodSource
  • @FieldSource
  • @CsvSource
  • @CsvFileSource
  • @ArgumentsSource

6. Argument Conversion

Now we know how to use various argument source annotations to supply primitive test data to our tests. Argument converters are a convenient way of mapping the primitive arguments of a parameterized test to more complex data structures.

6.1. Implicit Conversion

Let’s re-write one of those @EnumTests with a @CsvSource:

@ParameterizedTest
@CsvSource({"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"}) // Passing strings
void someMonths_Are30DaysLongCsv(Month month) {
    final boolean isALeapYear = false;
    assertEquals(30, month.length(isALeapYear));
}

This seems like it shouldn’t work, but it somehow does.

JUnit 5 converts the String arguments to the specified enum type. To support use cases like this, JUnit Jupiter provides several built-in implicit type converters.

The conversion process depends on the declared type of each method parameter. The implicit conversion can convert the String instances to types such as the following:

  • UUID 
  • Locale
  • LocalDate, LocalTime, LocalDateTime, Year, Month, etc.
  • File and Path
  • URL and URI
  • Enum subclasses

6.2. Explicit Conversion

We sometimes need to provide a custom and explicit converter for arguments.

Suppose we want to convert strings with the yyyy/mm/dd format to LocalDate instances.

First, we need to implement the ArgumentConverter interface:

class SlashyDateConverter implements ArgumentConverter {

    @Override
    public Object convert(Object source, ParameterContext context)
      throws ArgumentConversionException {
        if (!(source instanceof String)) {
            throw new IllegalArgumentException(
              "The argument should be a string: " + source);
        }
        try {
            String[] parts = ((String) source).split("/");
            int year = Integer.parseInt(parts[0]);
            int month = Integer.parseInt(parts[1]);
            int day = Integer.parseInt(parts[2]);

            return LocalDate.of(year, month, day);
        } catch (Exception e) {
            throw new IllegalArgumentException("Failed to convert", e);
        }
    }
}

Then we should refer to the converter via the @ConvertWith annotation:

@ParameterizedTest
@CsvSource({"2018/12/25,2018", "2019/02/11,2019"})
void getYear_ShouldWorkAsExpected(
  @ConvertWith(SlashyDateConverter.class) LocalDate date, int expected) {
    assertEquals(expected, date.getYear());
}

7. Argument Accessor

By default, each argument provided to a parameterized test corresponds to a single method parameter. Consequently, when passing a handful of arguments via an argument source, the test method signature gets very large and messy.

One approach to address this issue is to encapsulate all passed arguments into an instance of ArgumentsAccessor and retrieve arguments by index and type.

Let’s consider our Person class:

class Person {

    String firstName;
    String middleName;
    String lastName;
    
    // constructor

    public String fullName() {
        if (middleName == null || middleName.trim().isEmpty()) {
            return String.format("%s %s", firstName, lastName);
        }

        return String.format("%s %s %s", firstName, middleName, lastName);
    }
}

To test the fullName() method, we’ll pass four arguments: firstName, middleName, lastName, and the expected fullName. We can use the ArgumentsAccessor to retrieve the test arguments instead of declaring them as method parameters:

@ParameterizedTest
@CsvSource({"Isaac,,Newton,Isaac Newton", "Charles,Robert,Darwin,Charles Robert Darwin"})
void fullName_ShouldGenerateTheExpectedFullName(ArgumentsAccessor argumentsAccessor) {
    String firstName = argumentsAccessor.getString(0);
    String middleName = (String) argumentsAccessor.get(1);
    String lastName = argumentsAccessor.get(2, String.class);
    String expectedFullName = argumentsAccessor.getString(3);

    Person person = new Person(firstName, middleName, lastName);
    assertEquals(expectedFullName, person.fullName());
}

Here, we’re encapsulating all passed arguments into an ArgumentsAccessor instance and then, in the test method body, retrieving each passed argument with its index. In addition to just being an accessor, type conversion is supported through get* methods:

  • getString(index) retrieves an element at a specific index and converts it to String — the same is true for primitive types.
  • get(index) simply retrieves an element at a specific index as an Object.
  • get(index, type) retrieves an element at a specific index and converts it to the given type.

8. Argument Aggregator

Using the ArgumentsAccessor abstraction directly may make the test code less readable or reusable. In order to address these issues, we can write a custom and reusable aggregator.

To do that, we implement the ArgumentsAggregator interface:

class PersonAggregator implements ArgumentsAggregator {
    @Override
    public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context)
      throws ArgumentsAggregationException {
        return new Person(
          accessor.getString(1), accessor.getString(2), accessor.getString(3));
    }
}

And then we reference it via the @AggregateWith annotation:

@ParameterizedTest
@CsvSource({"Isaac Newton,Isaac,,Newton", "Charles Robert Darwin,Charles,Robert,Darwin"})
void fullName_ShouldGenerateTheExpectedFullName(
  String expectedFullName,
  @AggregateWith(PersonAggregator.class) Person person) {

    assertEquals(expectedFullName, person.fullName());
}

The PersonAggregator takes the last three arguments and instantiates a Person class out of them.

9. Customizing Display Names

By default, the display name for a parameterized test contains an invocation index along with a String representation of all passed arguments:

├─ someMonths_Are30DaysLongCsv(Month)
│     │  ├─ [1] APRIL
│     │  ├─ [2] JUNE
│     │  ├─ [3] SEPTEMBER
│     │  └─ [4] NOVEMBER

However, we can customize this display via the name attribute of the @ParameterizedTest annotation:

@ParameterizedTest(name = "{index} {0} is 30 days long")
@EnumSource(value = Month.class, names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"})
void someMonths_Are30DaysLong(Month month) {
    final boolean isALeapYear = false;
    assertEquals(30, month.length(isALeapYear));
}

April is 30 days long surely is a more readable display name:

├─ someMonths_Are30DaysLong(Month)
│     │  ├─ 1 APRIL is 30 days long
│     │  ├─ 2 JUNE is 30 days long
│     │  ├─ 3 SEPTEMBER is 30 days long
│     │  └─ 4 NOVEMBER is 30 days long

The following placeholders are available when customizing the display name:

  • {displayName} will be replaced with the display name of the method. In case the @Display annotation is provided, then the value is provided.
  • {index} will be replaced with the invocation index. Simply put, the invocation index for the first execution is 1, for the second is 2, and so on.
  • {arguments} is a placeholder for the complete, comma-separated list of arguments’ values.
  • {argumentsWithNames} is a placeholder for named argument. These arguments are built with the structure
    arguments(named(NAME, ARG), …). This prints the given name and the actual parameter name.
  • {argumentSetName} is a placeholder for the first parameter (name of the set) provided in the factory method argumentSet.
  • {argumentSetNameOrArgumentsWithNames} is a placeholder for the first parameter provided in the factory method argumentSet.
  • {0}, {1}, … are placeholders for individual arguments.

For our example, we’ll need some test classes. Let’s consider the Country and CountryUtil classes:

public record Country(String name, long population) {
}
public class CountryUtil {

    private static final long TRESHOLD = 100_000_000;

    public boolean isBigCountry(Country country) {
        return country.population() > TRESHOLD;
    }

}

9.1. Using the arguments(…ARGS) Factory

For the following example, we’ll display the arguments, then a phrase containing the name of the person and the domain they worked on:

@DisplayName("Big Countries - Simple")
@ParameterizedTest(name = "[{arguments}]: ''{0}'' with population ''{1}''")
@FieldSource("simpleArguments")
public void givenBigCountryData_usingCountryUtil_isBigCountry_shouldReturnTrue_simple(
  String name, long population) {
    Country country = new Country(name, population);

    boolean isBigCountry = CountryUtil.isBigCountry(country);

    assertTrue(isBigCountry, "The country provided is not considered big!");
}
private static List<Arguments> simpleArguments = Arrays.asList(
        arguments("India", 1_450_935_791),
        arguments("China", 1_419_321_278),
        arguments("United States", 345_426_571)
);

The above code will print:

├─ Big Countries - Simple
│     │  ├─ [India, 1450935791]: 'India' with population '1450935791' people
│     │  ├─ [China, 1419321278]: 'China' with population '1419321278' people
│     │  └─ [United States, 345426571]: 'United States' with population '345426571' people

It won’t display the index of the test anymore because we didn’t use the {index} in the format. However, the arguments are printed as a list, e.g., [India, 1450935791], and the arguments are enclosed only in one single quote because the single quote is part of the format and must be escaped.

9.2. Using the arguments(named(NAME, ARG), …ARGS) Factory

The following example is similar to the previous one. But in this case, each argument provided will be wrapped with named(NAME, ARG), which provides a custom name for each argument:

@DisplayName("Big Countries - With Named Arguments")
@ParameterizedTest(name = "{argumentsWithNames}")
@FieldSource("namedArguments")
public void givenBigCountryData_usingCountryUtil_isBigCountry_shouldReturnTrue_namedArguments(
  String countryName, long countryPopulation) {
    Country country = new Country(countryName, countryPopulation);

    boolean isBigCountry = CountryUtil.isBigCountry(country);

    assertTrue(isBigCountry, "The country provided is not considered big!");
}
private static List<Arguments> namedArguments = Arrays.asList(
        arguments(named("Most populated country in Asia", "India"), 1_450_935_791),
        arguments("China", 1_419_321_278),
        arguments(named("Biggest country in America", "United States"), 345_426_571)
);

The test display is slightly unexpected (it replaces the value with the name, a handy way to alias values). It replaces the argument’s value with the given name, which also happens if you access the named value with the index placeholder (e.g., {0}, {1}, …) :

├─ Big Countries - With Named Arguments
│ │ ├─ countryName=Most populated country in Asia, countryPopulation=1450935791
│ │ ├─ countryName=China, countryPopulation=1419321278
│ │ └─ countryName=Biggest country in America, countryPopulation=345426571

The display with named is useful for large objects with large toString() representation.

To use the argumentsWithNames placeholder, we must set the -parameters flag for the build. Here’s one way of doing that using Maven:

<build>
    ....
    <plugins>
        ....
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.13.0</version>
            <configuration>
                <parameters>true</parameters>
            </configuration>
        </plugin>
        ....
    </plugins>
    ...
</build>

9.3. Using argumentSet(NAME_OF_ARGS, ARGS) Factory

This type of naming gives us a name for the entire set of arguments, which is useful to detail a sequence of arguments:

@DisplayName("Big Countries - Named Set")
@ParameterizedTest(name = "''{0}'' is considered {argumentSetName} due to his population of ''{1}'' people")
@FieldSource("argumentSets")
public void notablePeople_withSetName(String name, long population) {
    Country country = new Country(name, population);

    boolean isBigCountry = CountryUtil.isBigCountry(country);

    assertTrue(isBigCountry, "The country provided is not considered big!");
}
private static List<Arguments> argumentSets = Arrays.asList(
        argumentSet("the most populated country in Asia", "India", 1_450_935_791),
        argumentSet("the second most populated country in Asia", "China", 1_419_321_278),
        argumentSet("biggest country in America", "United States", 345_426_571)
);

The test displays the name of the country, followed by the set name, and then the population:

├─ Big Countries - Named Set
│     │  ├─ 'India' is considered the most populated country in Asia due to his population of '1450935791' people
│     │  ├─ 'China' is considered the second most populated country in Asia due to his population of '1419321278' people
│     │  └─ 'United States' is considered biggest country in America due to his population of '345426571' people

10. Conclusion

In this article, we explored the nuts and bolts of parameterized tests in JUnit 5.

We learned that parameterized tests are different from normal tests in two aspects: they’re annotated with the @ParameterizedTest, and they need a source for their declared arguments.

Also, by now, we should know that JUnit provides some facilities to convert the arguments to custom target types or to customize the test names.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

eBook Jackson – NPI EA – 3 (cat = Jackson)