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 ArgumentSource
s 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:
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}
}
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.
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:
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:
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 aComic
. JUnit will unwrap the payload from theNamed
object automatically for us.
The test report for this test will now look like this:
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
- you can find the code of this blog post in this GitHub repository
- want to learn more about the customization of your display name? have a look at JUnit’s Documentation