Are you still using JUnit 4? If so, your code will thank you for migrating to JUnit 5.

JUnit 5 is the latest stable release of JUnit, and it comes with a lot of great features. Some of them are there to make your tests more readable. Some were added so you can enhance your written tests and add new scenarios. JUnit 5 also allows you to get the most out of Java 8 language features, such as lambda expressions. In general, JUnit 5 is trying to make the framework more robust and flexible than it was in its previous version.

Annotation Changes

The first changes that we will be discussing are annotation changes. JUnit 5 tried to increase readability for all annotations so that they are easier to understand at first glance. The new framework also has some new annotations that we can use to increase our scenarios. Let's take a look at the major annotation changes first:

@Before → @BeforeEach
@BeforeClass → @BeforeAll
@After → @AfterEach
@AfterClass → @AfterAll

First of all, let's talk about the simplest change. Before was renamed to BeforeEach, BeforeClass was renamed to BeforeAll, After was renamed to AfterEach, and AfterClass was renamed to AfterAll. All of these changes were made to enable cleaner code and better readability.

@RunWith → @ExtendWith

RunWith was renamed to ExtendWith. But this one wasn't just a simple rename. ExtendWith is an annotation with a lot of features. The best one is that you can now use multiple Extensions at the same time. For example, you can do something like this:

@ExtendWith(MockitoExtension.class)
@ExtendWith(MyCustomExtension.class)
public class FeatureTest {
}

Alternatively, you can just use a list inside the @ExtendWith value, as shown below:

@ExtendWith({MockitoExtension.class, MyCustomExtension.class})
public class FeatureTest {
}

Note that the execution order will follow the execution declared.

@Suite → @SelectPackages or @SelectClasses

Another change was made to improve the readability and to give each annotation single responsibility. So if you want to do a test suite, it's easier with the above annotations. Let's see how this can be done:

@SelectClasses({PostgreConnector.class, PostgreRepository.class, PostgreDriver.class})
public class MyPostgreTests {
}

In the test above, we pointed to each class that we wanted to cover in our test suite. We can do this by using packages, as shown below:

@SelectPackages("com.avenuecode.examples.junit5.database")
@IncludePackages("com.avenuecode.examples.junit5.database.pgsql")
public class MyPostgreTests {
}

In the test above, I'm using a new annotation too: @IncludePackages. @SelectPackages will make sure that all classes and subpackages under that package will be tested. If you also annotate with @IncludePackages, you will make sure that only that subpackage and its classes will be in that suite. You can also use @ExcludePackages to point a package and its subpackages to be excluded from a test suite. In addition, you can use @IncludeClassNamePatterns or @ExcludeClassNamePatterns to make sure that some classes following a regex indicated will be excluded or included in your suite.

@Ignore → @Disabled

This was a rename that also aims for better readability and an easier understanding of what the method does. Beyond this, if you annotate @Disabled at class level, you will get the number of tests skipped in your execution log. If you used @Ignore with JUnit 4, you would get: 1/1 class skipped, but now you will get: number of tests/number of tests skipped, which makes a slight improvement in understanding what was skipped.

New Annotations

As stated before, JUnit 5 added some new annotations that can help us increase our test readability and make them more powerful. We will be focusing on two annotations in this article to showcase an example of what the new version is trying to achieve: @DisplayName and @ParameterizedTest.

@DisplayName

This annotation is one of those changes that you look at and think: "Why wasn't this implemented before?". It's so simple and yet so functional. All this change does is display a text before your test runs. It's that simple. Here's the code:

@Test
@DisplayName("The method withdraw should reduce the amount of money in the bank account by the amount passed")
public void withdrawShouldReduceAmount() {
    var bankAccount = new BankAccount(2000);
    bankAccount.withdraw(100);
    assertEquals(1900, bankAccount.getBalance());
}

@ParameterizedTest

This annotation is one of the most powerful ones. Why? It allows you to reutilize the same test method with multiple parameter variations. Yes, this was a feature only achieved by adding external libraries; now it's on JUnit source too. Below, I'll show how to write a simple test that can use this feature very well. 

Let's say we want to check whether or not our customer who has a bank account is eligible for a credit card. Our method would look something like this:

public enum AccountType {
    BLACK(1000), PLATINUM(1500), GOLD(2000);
    private double minEligibility;
    AccountType(int minEligibility) {
        this.minEligibility = minEligibility;
    }
    public double getMinEligibility() {
        return this.minEligibility;
    }
}

public boolean isEligibleForCreditCard() {
  return this.balance >= this.accountType.getMinEligibility();
}

So now, we can write our tests like this:

@ParameterizedTest
@MethodSource("accountTypes")
@DisplayName("Verify if the account is eligible for a credit card based on its type and balance")
public void verifyCreditCardEligibility(BankAccount bankAccount, boolean expected) {
    assertEquals(expected, bankAccount.isEligibleForCreditCard());
}

public static Stream<Arguments> accountTypes() {
    return Stream.of(
            Arguments.of(new BankAccount(AccountType.GOLD, 3000.5), true),
            Arguments.of(new BankAccount(AccountType.GOLD, 1000.25), false),
            Arguments.of(new BankAccount(AccountType.BLACK, 1100), true),
            Arguments.of(new BankAccount(AccountType.BLACK, 980), false),
            Arguments.of(new BankAccount(AccountType.PLATINUM, 1600), true),
            Arguments.of(new BankAccount(AccountType.PLATINUM, 1200), false)
    );
}

What we're doing above is providing a data source method. ParameterizedTest will use our MethodSource AccountTypes to supply the test parameters. So, for each of our Arguments of the Stream we're creating in our MethodSource, the test method will be run passing the Arguments as parameters to verifyCreditCardEligibility(). Pretty simple, yet powerful, right?

You can provide other sources of data, such as @ValueSorce, @EmptySource, @EnumSource, and a lot of others. You can find a full guide here.

New Assertions and Changes

The new assertions are located in a new package. Most of them are located in org.junit.jupiter.Assertions.

Like the changes shown above, some assertions were changed to achieve better readability. New assertions were added so we can achieve new scenarios or make some old cases more powerful. Let's take a loot at each:

Message is now the last parameter of assert[...]()

Did you also hate it when you wrote some assertion like assertEquals("Some string", "Another string"), and then you wanted it to have a custom message like "2 is not the same as 2" when it failed, but to achieve that you needed to change your assertEquals parameter order? That was all because the overloaded methods were written like this:

assertEquals(String expected, String actual)
assertEquals(String message, String expected, String actual)

It is kind of counterintuitive, don't you think? So, in JUnit 5, this was changed so the method signatures look like this:

assertEquals(String expected, String actual)
assertEquals(String expected, String actual, String message)

This was not only changed for assertEquals(String, String), but also for all assertions that accept a custom message.

New assertions

There are a lot of new assertions in JUnit 5, such as assertThrows(), assertDoesNotThrow(), assertAll(), assertTimeout(), assertIterableEquals(), assertLinesMatch(), assertNotEquals(), and  assertTimeoutPreemptively().

In this post, we will be covering the ones that I think you'll use the most frequently: assertThrows() and assertAll().

assertThrows()

I'm pretty confident in saying that this is the best change made to Assertions in JUnit 5. This replaces the @Test(expected = SomeException.class) and the @Rule ExpectedException from JUnit 4. Let's see some code so we can understand it better:

Imagine that we have the following method:

public void withdraw(double amount) {
    if (amount < 0) {
        throw new IllegalArgumentException("amount to be withdrawn must be greater than 0");
    }
    this.balance - amount;
}

So let's write a test that validates that our exception was thrown. In JUnit 4, we would write something like this:

@Test(expected = IllegalArgumentException.class)
public void withdrawShouldThrowExceptionWhenAmountIsLessThanZero() {
    var bankAccount = new BankAccount(2000);
    bankAccount.withdraw(-1);
}

Or, if we wanted to validate our exception message, we could write something like this:

@Rule
public ExpectedException expectedException = ExpectedException.none();

@Test
public void withdrawShouldThrowExceptionWhenAmountIsLessThanZero() {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage("amount to be withdrawn must be greater than 0");
    var bankAccount = new BankAccount(2000);
    bankAccount.withdraw(-1);
}

Now that our assertions are more powerful, we could write the same code below but enjoy greater readability and some new features. Let's take a look:

@Test
public void withdrawShouldThrowExceptionWhenAmountIsLessThanZero() {
    IllegalArgumentException exception = assertThrows(
        IllegalArgumentException.class,
        () -> {
            var bankAccount = new BankAccount(2000);
            bankAccount.withdraw(-1);
        },
        "The IllegalArgumentException was not thrown, check your test."
    );
  assertEquals("amount to be withdrawn must be greater than 0", exception.getMessage(), "The exception message did not match the expected message")
}

Can you see how much easier it is now to understand what the test is doing? It also makes the most out of lambda and Java 8. Beyond this, the assertThrows make your code more flexible. You can read more about it in its documentation.

assertAll()

This is a new assertion that was added so that you can create a block of assertions. Now you could be asking: why would I want that if I can simply assert multiple times in my test method? Well, if you do that, the test will stop at the first failed assertion and you will not know the result of the next ones unless you make sure that the first assertion passes. With assertAll(), you make sure that every assertion runs, whether or not they pass, and you get a detailed explanation for each of the failures. Let's see an example:

@Test
public void validatePerson() {
    var person  = myClass.getTheLastJedi();
    assertAll("Should return Luke Skywalker",
        () -> assertEquals("Lucas", person.getFirstName()),
        () -> assertEquals("Skywalkar", person.getLastName()),
        () -> assertEquals(35, person.getAge()),
        () -> assertEquals(170, person.getHeightInCentimeters());
}

Now, even though two tests fail, all of the other ones will be executed and you will get something like the code below:

org.opentest4j.MultipleFailuresError:
    Should return Luke Skywalker (2 failures)
    expected: <Luke> but was: <Lucas>
    expected: <Skywalker> but was: <Skywalkar>

Conclusion

At first glance, JUnit 5 might look like it didn't change enough to justify migrating from the most used version, JUnit 4. But its changes do improve your test quality significantly, so you should consider migrating if you haven't already done so.


Author

Lucas Moraes

Lucas Moraes is a Java Engineer at Avenue Code. He loves to automate everything he can. He also enjoys using the latest technologies that are available. In his free time, Lucas really likes to watch any kind of sports and is a huge Star Wars fan.


How to Build Unit Tests Using Jest

READ MORE

How Heuristics Can Improve Your Tests

READ MORE

How to Use WireMock for Integration Testing

READ MORE

The Best Way to Use Request Validation in Laravel REST API

READ MORE