44

I have a bunch of @ParameterizedTests that receive parameters from a @MethodSource with quite verbose toString() results (e.g. Selenium's WebDriver). These are used per default to compose the corresponding display names. From the JUnit 5 user guide:

By default, the display name of a parameterized test invocation contains the invocation index and the String representation of all arguments for that specific invocation. However, you can customize invocation display names via the name attribute of the @ParameterizedTest annotation […]

While this allows customizing the display names to a certain degree, it seems like I cannot adapt the string representation of the individual parameters. Unfortunately, specifying a generator via @DisplayNameGeneration can only be applied at the class level and doesn't affect the display name of the parameterized test invocations.

Is there a way to use a DisplayNameGenerator for @ParameterizedTest or to customize the string representation of the given parameters?

SDJ
  • 4,083
  • 1
  • 17
  • 35
beatngu13
  • 7,201
  • 6
  • 37
  • 66

4 Answers4

70

As of JUnit 5.8.0, there is a Named<T> interface as part of the JUnit Jupiter API with "automatic support for injecting the contained payload [the arguments] into parameterized methods directly" (see issue #2301). Example:

@DisplayName("A parameterized test with named arguments")
@ParameterizedTest
@MethodSource("namedArguments")
void testWithNamedArguments(File file) {}

static Stream<Arguments> namedArguments() {
    return Stream.of(
        Arguments.of(Named.of("An important file", new File("path1"))),
        Arguments.of(Named.of("Another file", new File("path2")))
    );
}

If you prefer static imports, you can also go for the corresponding aliases from Arguments and Named:

arguments(named("An important file", new File("path1")))

For more information, please refer to the corresponding docs.

beatngu13
  • 7,201
  • 6
  • 37
  • 66
  • `Named.of()` is a great way to customise the display name. Much cleaner than adding an additional parameter that will not be used. – armandino Mar 08 '22 at 23:08
  • 1
    `(name = "{index}: {0}")` can be skipped when using `Named` – Thánh Ma Mar 10 '22 at 07:58
  • @ThánhMa thank you, updated the answer accordingly. Output is slightly different (e.g. "[1] An important file" instead of "1: An important file") but no information loss. – beatngu13 May 21 '22 at 17:06
  • 1
    For multiple inputs, use `Stream.of(arguments(named("cool name", "first input"), "second input"));` and `@ParameterizedTest(name = "{0}")` – Domadin Apr 21 '23 at 16:48
36

To indirectly achieve your goal until this is directly supported by JUnit, one can always add an argument to the test that's the desired string. The customization then needs to be done in the argument provider method.

@ParameterizedTest(name = "{index} {1}")
@MethodSource("argumentProvider")
public void testSomething(Object someObject, String niceRepresentation) {
    // Test something
}

private static Stream<Arguments> argumentProvider() {
    return Stream.of(
            Arguments.of(new Object(), "Nice 1"),
            Arguments.of(new Object(), "Nice 2")
    );
}

The drawback is that the unit test is supplied arguments that are not used in the test's implementation, which can harm clarity, but then it becomes a trade-off with the verbose name in the test report.

SDJ
  • 4,083
  • 1
  • 17
  • 35
  • I already thought about something similar. One could also extend the parameter type and overwrite `toString()`, but I was hoping for a nicer solution. Nonetheless, valid solution—thanks! – beatngu13 Sep 11 '19 at 18:09
  • 1
    This is not type safe, since the actual parameter could change leaving the strings mismatched, but seems like the only way for now. – Abhijit Sarkar Aug 11 '20 at 07:40
1

just adding a similar problem with surefire reports.

Wasn't getting the correct test name in the reports after using @DisplayName and @ParameterizedTest(name = "{0}").

This issue solved my problem #1255

       <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.0.0-M4</version>
            <configuration>
                <statelessTestsetReporter
                        implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5Xml30StatelessReporter">
                    <disable>false</disable>
                    <version>3.0</version>
                    <usePhrasedFileName>false</usePhrasedFileName>
                    <usePhrasedTestSuiteClassName>true</usePhrasedTestSuiteClassName>
                    <usePhrasedTestCaseClassName>true</usePhrasedTestCaseClassName>
                    <usePhrasedTestCaseMethodName>true</usePhrasedTestCaseMethodName>
                </statelessTestsetReporter>
            </configuration>
        </plugin>
Corgan
  • 669
  • 8
  • 11
0

For bigger units (that for example require more parameters, interacts with a few services etc.) I usually use TestCase record and override the toString() method to get a nicely displayed name.

class Test {
    
    private record TestCase(
        String testName,
        // params, expected outputs, validators, etc
    ) {
        @Override
        public String toString() {
            return testName;
        }
    }

    @ParameterizedTest(name = "{index}. {0}")
    @MethodSource("testCases_source")
    void checkTestCase(TestCase testCase) {
        // test method impl
    }

    public static Stream<Arguments> testCases_source() {
        return TestCases.testCasesStream(
            new TestCase(
                "should return value and invoke the method", // testName
                InputDto.of("v1"), // input 
                (service) -> { verify(mockk, times(1)).method() }, // verifier
                true // output       
            )
            new TestCase(
                "should return value and invoke the service twice",
                // ...
            )
            // other test cases 
        )
    }

    // helper from other class to apply some syntax sugar:
    public Stream<Arguments> testCasesStream(TestCase... testCases) {
        return Arrays.stream(testCases).map(Arguments::of); 
    }
}

Unfortunately, Java doesn't have any creation pattern for record so usually I replace the record with Java Class + Lombok (or use Kotlin instead).

Cililing
  • 4,303
  • 1
  • 17
  • 35