1. Overview

JUnit 5 has good support for customizing test class and test method names. In this quick tutorial, we’ll see how we can use JUnit 5 custom display name generators via the @DisplayNameGeneration annotation.

2. Display Name Generation

We can configure custom display name generators via the @DisplayNameGeneration annotation. However, it’s good to be aware that the @DisplayName annotation always takes precedence over any display name generator.

To start with, JUnit 5 provides a DisplayNameGenerator.ReplaceUnderscores class that replaces any underscores in names with spaces. Let’s take a look at an example:

@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class ReplaceUnderscoresGeneratorUnitTest {

    @Nested
    class when_doing_something {

        @Test
        void then_something_should_happen() {
        }

        @Test
        @DisplayName("@DisplayName takes precedence over generation")
        void override_generator() {
        }
    }
}

Now, when we run the test we can see the display name generation made the test output more readable:

└─ ReplaceUnderscoresGeneratorUnitTest ✓
   └─ when doing something ✓
      ├─ then something should happen() ✓
      └─ @DisplayName takes precedence over generation ✓

3. Custom Display Name Generator

To write a custom display name generator, we have to write a class that implements the methods in the DisplayNameGenerator interface. The interface has methods for generating the name for a class, a nested class, and a method.

3.1. Camel Case Replacement

Let’s start with a simple display name generator that replaces camel case names with readable sentences. To begin with, we can extend the DisplayNameGenerator.Standard class:

    static class ReplaceCamelCase extends DisplayNameGenerator.Standard {
        @Override
        public String generateDisplayNameForClass(Class<?> testClass) {
            return replaceCamelCase(super.generateDisplayNameForClass(testClass));
        }

        @Override
        public String generateDisplayNameForNestedClass(Class<?> nestedClass) {
            return replaceCamelCase(super.generateDisplayNameForNestedClass(nestedClass));
        }

        @Override
        public String generateDisplayNameForMethod(Class<?> testClass, Method testMethod) {
            return this.replaceCamelCase(testMethod.getName()) + 
              DisplayNameGenerator.parameterTypesAsString(testMethod);
        }

        String replaceCamelCase(String camelCase) {
            StringBuilder result = new StringBuilder();
            result.append(camelCase.charAt(0));
            for (int i=1; i<camelCase.length(); i++) {
                if (Character.isUpperCase(camelCase.charAt(i))) {
                    result.append(' ');
                    result.append(Character.toLowerCase(camelCase.charAt(i)));
                } else {
                    result.append(camelCase.charAt(i));
                }
            }
            return result.toString();
        }
    }

In the above example, we can see the methods that generate different parts of the display name.

Let’s write a test for our generator:

@DisplayNameGeneration(DisplayNameGeneratorUnitTest.ReplaceCamelCase.class)
class DisplayNameGeneratorUnitTest {

    @Test
    void camelCaseName() {
    }
}

Next, when running the test, we can see that the camel case names have been replaced with readable sentences:

└─ Display name generator unit test ✓
   └─ camel case name() ✓

3.2. Indicative Sentences

So far, we’ve discussed very simple use cases. However, we can get more creative:

    static class IndicativeSentences extends ReplaceCamelCase {
        @Override
        public String generateDisplayNameForNestedClass(Class<?> nestedClass) {
            return super.generateDisplayNameForNestedClass(nestedClass) + "...";
        }

        @Override
        public String generateDisplayNameForMethod(Class<?> testClass, Method testMethod) {
            return replaceCamelCase(testClass.getSimpleName() + " " + testMethod.getName()) + ".";
        }
    }

The idea here is to create indicative sentences from the nested class and test method. In other words, the nested class name will be prepended to the test method name:

class DisplayNameGeneratorUnitTest {

    @Nested
    @DisplayNameGeneration(DisplayNameGeneratorUnitTest.IndicativeSentences.class)
    class ANumberIsFizz {
        @Test
        void ifItIsDivisibleByThree() {
        }

        @ParameterizedTest(name = "Number {0} is fizz.")
        @ValueSource(ints = { 3, 12, 18 })
        void ifItIsOneOfTheFollowingNumbers(int number) {
        }
    }

    @Nested
    @DisplayNameGeneration(DisplayNameGeneratorUnitTest.IndicativeSentences.class)
    class ANumberIsBuzz {
        @Test
        void ifItIsDivisibleByFive() {
        }

        @ParameterizedTest(name = "Number {0} is buzz.")
        @ValueSource(ints = { 5, 10, 20 })
        void ifItIsOneOfTheFollowingNumbers(int number) {
        }
    }
}

Looking at the example, we use the nested class as a context for the test method. To better illustrate the results, let’s run the test:

└─ Display name generator unit test ✓
   ├─ A number is buzz... ✓
   │  ├─ A number is buzz if it is one of the following numbers. ✓
   │  │  ├─ Number 5 is buzz. ✓
   │  │  ├─ Number 10 is buzz. ✓
   │  │  └─ Number 20 is buzz. ✓
   │  └─ A number is buzz if it is divisible by five. ✓
   └─ A number is fizz... ✓
      ├─ A number is fizz if it is one of the following numbers. ✓
      │  ├─ Number 3 is fizz. ✓
      │  ├─ Number 12 is fizz. ✓
      │  └─ Number 18 is fizz. ✓
      └─ A number is fizz if it is divisible by three. ✓

As we can see, the generator combined the nested class and test method names to create indicative sentences.

4. Custom Names for Arguments in Parameterized Tests

Before diving deep into the details, let’s see how JUnit 5 generates, by default, the display names for parameterized test arguments. For instance, let’s consider the whenUsingDefaultAttributes_thenGenerateDefaultDisplayNames() parameterized test:

@ParameterizedTest
@MethodSource("argumentsProvider")
void whenUsingDefaultAttributes_thenGenerateDefaultDisplayNames(String givenArg) {
    // Test
}

private static Stream<Arguments> argumentsProvider() {
    return Stream.of(Arguments.of("City: Madrid"), Arguments.of("Country: Spain"), Arguments.of("Continent: Europe"));
}

Notably, we used the @MethodSource annotation to bind the parameterized test to a factory method that returns a Stream of arguments. Each argument will be used in each invocation of the parameterized test.

Now, if we run this test, we’ll get:

└─ whenUsingDefaultAttributes_thenGenerateDefaultDisplayNames
    ├─ [1] City: Madrid
    ├─ [2] Country: Spain
    ├─ [3] Continent: Europe

As we can see, JUnit 5 generates a name containing an index followed by a string representation of each passed argument.

4.1. Using the name Attribute

Typically, we can override the default behavior and customize the display name of each argument by using the attribute name of the @ParameterizedTest annotation itself. So, let’s see it in action:

@ParameterizedTest(name = "Parameter with index {index} => {0}")
@MethodSource("argumentsProvider")
void whenUsingNameAttribute_thenGenerateCustomDisplayNames(String givenArg) {
    // Test
}

Here, we want to generate a custom display name that follows the pattern “Parameter with index {index} => {0}”. Next, let’s run the test and see what happens:

└─ whenUsingNameAttribute_thenGenerateCustomDisplayNames
    ├─ Parameter with index 1 => City: Madrid
    ├─ Parameter with index 2 => Country: Spain
    ├─ Parameter with index 3 => Continent: Europe

Unsurprisingly, JUnit 5 used the custom pattern specified in the name attribute to generate the display name for each argument.

4.2. Using the Named Interface

The Named interface offers another way to achieve the same objective. It allows associating a custom name with a particular argument.

First, we’re going to create another parameterized test to highlight the use of the Named interface:

@ParameterizedTest
@MethodSource("namedArguments")
void whenUsingNamedInterface_thenGenerateCustomDisplayNames(String givenArg) {
    // Test
}

private static Stream<Arguments> namedArguments() {
    return Stream.of(Arguments.of(Named.of("Testing with a city", "Tokyo")), Arguments.of(Named.of("Testing with a country", "Japan")), Arguments.of(Named.of("Testing with a continent", "Asia")));
}

In a nutshell, we used the factory method of() to create an instance of Named based on the specified name and value.

Lastly, let’s run the test and see how the Named interface overrides the default display name generation mechanism:

└─ whenUsingNamedInterface_thenGenerateCustomDisplayNames
    ├─ [1] Testing with a city
    ├─ [2] Testing with a country
    ├─ [3] Testing with a continent

As shown above, the specified custom names are used to generate the display names instead of the actual values.

5. Conclusion

In this article, we saw how to use the @DisplayNameGeneration annotation to generate display names for our tests. Then, we wrote our own DisplayNameGenerator to customize the display name generation. Furthermore, we explained how to generate custom names for each argument in parameterized tests.

As usual, the examples used in this article can be found over on GitHub.