How To Improve JUnit DisplayNames With The New 'Named' API

With JUnit Jupiters @ParameterizedTest annotation, you can execute the same test multiple times, but with different parameters. This is useful if you want to test the same code with different data. For Example, to test for special cases or limits.
The annotation is used in combination with ArgumentSources like the @MethodSource. With this annotation, you can register a method as an argument provider which, like the name suggests, provides our test method with arguments. This allows extreme flexibility for our tests. However, the downside of this approach was the bad readability of the tests in your test report.
In this blog post, I will show you how to leverage the new Named API introduced in JUnit 5.8.0 to tackle this problem!

The Example

In the example of this blog post, we will write the software of a shop that purchases and resells comic books. The most important part of the software is the service that will calculate the purchase price. The calculation will consider two attributes of a comic, its condition and its print status. Here is the code:

Comic.java
public class Comic {

    private final String title;
    private final Condition condition;
    private final PrintStatus printStatus;

    //... all args constructor, getters

    public enum Condition {MINT, GOOD, MEDIOCRE, BAD}
    public enum PrintStatus {ORIGINAL, REPRINT}
}
PricingService.java
public class PricingService {
    public Double calculatePurchasePrice(Comic comic) {
        var purchasePrice = //complex calculation logic that considers the comics condition and print status 
        return purchasePrice;
    }
}
  • This is the method we want to test. The actual calculation logic is irrelevant for this blog post. All we need to know is that it considers the condition and print status of the comic.

Unreadable Test Reports

Before we start using the new Named API, we will write a test the old way.

PricingServiceTest.java
class PricingServiceTest {

    @ParameterizedTest
    @MethodSource("comics")
    void calculatePurchasePrice(Comic comic) {

        var cut = new PricingService();
        var price = cut.calculatePurchasePrice(comic);

        assertEquals(42.0, price);
    }

    static Stream<Arguments> comics() {
        return Stream.of(Arguments.of(new Comic("JVMan and Javagirl", Comic.Condition.MINT, Comic.PrintStatus.ORIGINAL)),
                         Arguments.of(new Comic("JVMan and Javagirl", Comic.Condition.GOOD, Comic.PrintStatus.ORIGINAL)),
                         // other combinations omited for brevity
        );
    }
}
  • We create a parameterized test that uses a method called “comics” as the source of its arguments. The argument is passed by JUnit as a normal method parameter.
  • The method we want to test is executed.
  • The argument source for our parameterized test.

When we run this test, we will get the following output:

Testoutput
PricingServiceTest
    calculatePurchasePrice(Comic)
        [1] dev.jschmitz.namedapi.Comic@4df828d7
        [2] dev.jschmitz.namedapi.Comic@6ab7a896
        // other tests omitted for brevity

  • As you can see, the report is far from optimal. If an error occurred, we would have no chance of identifying which exact test case fails. All we see is the object reference.

The new Named API

JUnit 5.8.0 introduced the new Named API, which is used to give parameters a meaningful name. It provides two ways of naming your parameters. You can either use Named.of() or named(). Both variants do the exact name thing. They wrap a payload and associate it with a name. Which one to use is totally up to you. Let’s have a look at how to use it:

NewPricingServiceTest.java
class NewPricingServiceTest {

    static Stream<Arguments> comics() {
        return Stream.of(
            Arguments.of(
                Named.of(
                    "a mint original", 
                    new Comic("JVMan and Javagirl", Comic.Condition.MINT, Comic.PrintStatus.ORIGINAL)
                )
            ),
            // other combinations omited for brevity
        );
    }

    @DisplayName("Calculates the price for:")
    @ParameterizedTest
    @MethodSource("comics")
    void calculatePurchasePrice(Comic comic) {

        var cut = new PricingService();
        var price = cut.calculatePurchasePrice(comic);

        assertEquals(42.0, price);
    }
}
  • We use the new Named API to associate a name to our payload.
  • The name of the parameter that will be used in the test report.
  • The actual parameter used in the test method
  • Although we use the new Named API in our argument source, the parameter of the test method is still a Comic. JUnit will unwrap the payload from the Named object automatically for us.

The test report for this test will now look like this:

Testoutput
NewPricingServiceTest
    Calculates the price for:
        [1] a mint original
        // other tests omitted for brevity
  • As you can see, the test report is far more readable than before. Due to the Named API, we can use our natural language to describe our Tests.

Conclusion

In my opinion, the Named API is a great way to improve the readability of your tests and the test report. Thanks to the backward-compatible design, you can add it to your existing tests, and it will simply work. No need for hacky workarounds like extra parameters, that solely exist to give the other parameters a context.

Resources