Parameterized unit tests with JUnit5

JUnit5 brings joy to the world of testing, and the category of parameterized tests is no exception.
All the limited and somewhat cumbersome way of developing unit tests with the previous version, has gone. Kudos for the JUnit team for a great framework, and kudos for an excellent documentation that you can find here.

Having said that, let’s take a quick look at the main differences provided by JUnit 5, in the area of parameterized tests :

  • We can run parameterized tests mixed with regular ones in the same class.
  • We can have several completely different parameterized tests in the same class, receiving different datasets.
  • Annotations indicating that we will run a parameterized test is now at method level and not at class level.
  • A number of pre-prepared annotations are available so that we do not have to write code.
  • Arguments from the dataset are injected as method parameters and not as instance variables.
  • If we only have one parameter, we can use Streams.

The possibilities open by JUnit 5, when compared with the previous version, are so many, that reading the documentation is essential. This post’s purpose is not to make an exhaustive analysis, but to introduce some of the new available possibilities.

We will use a very similar example as the one use for parameterized tests with JUnit 4.

We have a Purchase

Purchase.java
1
2
3
4
5
6
7
8
9
@AllArgsConstructor
public class Purchase {

private final long id;
@Getter
private PurchaseStatus status;
@Getter
private TransactionStatus transactionStatus;
}

The purchase can have 4 different Purchase statuses that are enumerated

PurchaseStatus.java
1
2
3
4
5
6
public enum PurchaseStatus {
CONFIRMED,
COMPLETED,
FAILED,
PENDING;
}

and also seven Transaction status, also enumerated

TransactionStatus.java
1
2
3
4
5
6
7
8
9
public enum TransactionStatus {
NEW,
PENDINGCHARGE,
FAILEDCHARGE,
CHARGED,
PENDINGCREDIT,
FAILEDCREDIT,
CREDITED
}

We need to process the purchases. For the purpose we have a purchaseJob that will do the work.
As long as the purchase has a COMPLETED status it will be processed and for all the other statuses it will not. In our example, we just return true or false, depending on the Purchase status. if a null purchase is supplied to the process() method, an exception will be thrown.

We also have a getPaidPurchases() which will act as an imaginary DAO, returning a collection of COMPLETED purchases.

PurchaseJob.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class PurchaseJob {

public boolean process(final Purchase purchase) {

if (purchase == null) throw new IllegalArgumentException("Purchase to process cannot be null");

return PurchaseStatus.COMPLETED.equals(purchase.getStatus());
}

public List<Purchase> getPaidPurchases(){
return Arrays.asList(
new Purchase(1L, PurchaseStatus.COMPLETED, TransactionStatus.CHARGED),
new Purchase(2L, PurchaseStatus.COMPLETED, TransactionStatus.CHARGED),
new Purchase(3L, PurchaseStatus.COMPLETED, TransactionStatus.CHARGED)
);
}
}

with this little example in place let’s head for the test

PurchaseJobTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class PurchaseJobTest {

private static PurchaseJob purchaseJob = new PurchaseJob();

static Stream<Purchase> purchaseStatusNegativeDataSet() {

return Stream.of(
new Purchase(1L, PurchaseStatus.CONFIRMED, TransactionStatus.NEW),
new Purchase(2L, PurchaseStatus.FAILED, TransactionStatus.FAILEDCHARGE),
new Purchase(3L, PurchaseStatus.PENDING, TransactionStatus.PENDINGCHARGE)
);
}

@ParameterizedTest
@MethodSource("purchaseStatusNegativeDataSet")
void processPurchase_returnsFalse(final Purchase purchase) {

Assertions.assertFalse(purchaseJob.process(purchase));
}

@ParameterizedTest
@NullSource
void processPurchase_withNullPurchase_throwsIllegalArgumentException(Purchase purchase) {

Assertions.assertThrows(IllegalArgumentException.class, () -> purchaseJob.process(purchase));
}

@Test
void processPurchase_returnsTrue() {

final Purchase purchase = new Purchase(1L, PurchaseStatus.COMPLETED, TransactionStatus.CHARGED);

Assertions.assertTrue(purchaseJob.process(purchase));
}

@ParameterizedTest
@EnumSource(value = TransactionStatus.class, mode = EXCLUDE, names = {"CHARGED"})
void paidPurchases_haveAllTransactionStatusAsCharged(final TransactionStatus transactionStatus) {

List<Purchase> purchases = purchaseJob.getPaidPurchases()
.stream()
.filter(p -> (p.getTransactionStatus().equals(transactionStatus)))
.collect(Collectors.toList());

Assertions.assertTrue(purchases.isEmpty());
}
}

Let’s dissect what we have here. There is a lot going on.

The first thing that jumps to the eye, is that there is no public access modifier in test methods. You can use it if you want. But there is no need for it.

There is no class level annotation indicating any specific runner.

The purchaseStatusNegativeDataSet() static method, responsible for supplying a collection of purchases, has no annotation.
So how does a parameterized test knows where to get the information from ?

Easy. By annotating a method with @ParameterizedTest we are saying that we want a parameterized test, and with the @MethodSource("purchaseStatusNegativeDataSet") we indicate where the dataset is coming from.
With those 2 annotations in place, we just need to add a parameter to the method, and JUnit 5 will take care of injecting every item from the collection and run it as many times as the number of items in the collection. We can see it in processPurchase_returnsFalse().

Lets look now at the processPurchase_withNullPurchase_throwsIllegalArgumentException method.

This is also considered a parameterized test, so annotation @ParameterizedTest is present and also @NullSource. The later indicates that we want to run the method with a null argument. So whenever, in the method, we use the argument JUnit 5 will replace it by a null.

The processPurchase_returnsTrue(), is a standard @Test method. It lives happily amongst the parameterized ones.

To finalize we have the paidPurchases_haveAllTransactionStatusAsCharged. A parameterized method that receives an enum as an argument, hence the @EnumSource annotation that will specify the Enum to be supplied. As you can see we can exclude as many constants as we want from the test (separating them by comma). In this case we are just excluding the CHARGED one.

You will find much more in JUnit 5 excellent user guide section devoted to parameterized tests

I hope this little post drives your curiosity to explore and start adding JUnit5 to your projects.

An example for the code in this post can be found in git-hub

happy coding :)

Share