return 42;

by Jan Wedel

Why you should start using JUnit 5

My favorite features of JUnit 5

What is JUnit 5?

According to its website,

JUnit 5 is the next generation of JUnit. The goal is to create an up-to-date foundation for developer-side testing on the JVM. This includes focusing on Java 8 and above, as well as enabling many different styles of testing.

This article is for everyone that did or did not hear about JUnit and wonders why they would want to use it.

I don't want to go into details on all the features or how it works internally, I just want to show some of the new features that are most valuable to me so far.

So let's get right into it...

Display Name

Previously in JUnit, tests got their names from their test methods.

E.g.:

public class AccountTest {

    @Test
    public void testMethod() {
        // do some tests here
    }
}

When you run it, it will print out testMethod as the name for the test which is problably not very useful and most importantly not readable. Test shoud be human readable because tests are the only true code documentation that does not get deprecated. So people started trying to come up with some more descriptive test names.

public class AccountTest {

    @Test
    public void withdrawSomeAmountGreaterThanCurrentBalanceFailsWithExcepetion() {
        // do some tests here
    }
}

Better? Not so much. At least it gives you some idea on what is going to happen here. Something about withdrawing some amount from an account that is greater than the current balance. That should fail. But how to make it more readable? Some developers (including myself) started breaking with the strict Java convention of camelCase and started using underlines to separate aspects of the test, for instance method__scenario__expectedResult.

public class AccountTest {

    @Test
    public void withdraw__amountGreaterThanCurrentBalance__failsWithExcepetion() {
        // do some tests here
    }
}

This is a bit more readable although it hurts the eyes of every Java developer that is used to camelCase. What you actually want is using any arbitrary string as the test name.

Kotlin allow you to use string literals like e.g.

internal class AccountTest {

    @Test
    fun `should thrown an exception when an amount is withdrawn greater that the current balance`() {
        // do some tests here
    }

JUnit 5 to the rescue. Although not as simple as in Kotlin, JUnit 5 comes with an @DisplayName annotation that can be used for both test classes and methods. The downside is, you still need to make a method name:

@DisplayName("An account")
class AccountTest {

    @Test
    @DisplayName("should throw an exception when an amount is withdrawn greater that the current balance")
    void withdrawBalanceExceeded() {
        // do some tests here
    }
}

So although its the typical Java annotation cluttering going on, the result (as in what the IDE shows) is much more readable.

Note that you do not need to use public anymore with methods and classes.

Nested Tests

While pair-programming with a colleague, I was copy and pasting tests and descriptions and just change parts of it to tests different edge cases. She told me I should use nested tests. We tried it and I immediately fell in love with the idea. Instead of having test like this:

  • An account
    • should throw an exception when an amount is withdrawn greater that the current balance
    • should reduce the balance by the amount that is withdrawn

You could do something like that

  • An account
    • withdrawal
      • should throw an exception when the amount greater that the current balance
      • should reduce the balance by the amount

With JUnit 5, it looks like this:

@DisplayName("An account")
public class AccountTest {

    @Nested
    @DisplayName("withdrawal")
    class Withdrawal {

        @Test
        @DisplayName("should throw an exception when the amount greater that the current balance")
        public void failureBalanceExceeded() {
            // do some tests here
        }

        @Test
        @DisplayName("should reduce the balance by the amount")
        public void success() {
            // do some tests here
        }
    }
}

Again, the gross number of lines increase by using @Nested as it is necessary to wrap tests within a nested class but it keeps the tests its self cleaner.

Extensions

JUnit 5 is built to be very extensible. Custom or third-party extensions can be added by using the @ExtendWith annotation at class level.

Spring Boot

Spring boot integration tests for example are now run by using @ExtendWith(SpringExtension.class). Note that this only works from spring-boot 2.x and higher.

Mockito Extension

There will be an updated MockitoExtension that could do something like that:

@ExtendWith(MockitoExtension.class)
class MyMockitoTest {

    @BeforeEach
    void init(@Mock Person person) {
        when(person.getName()).thenReturn("Dilbert");
    }

    @Test
    void simpleTestWithInjectedMock(@Mock Person person) {
        assertEquals("Dilbert", person.getName());
    }
}

Migration from 4 to 5

Those new features in JUnit 5 come at a price: They are not compatible with JUnit 4 tests. This is called JUnit Jupiter.

"But wait, what about my existing 500 test cases suite?" you might ask. It not as bad as you might think. The JUnit team did a smart thing, they moved all JUnit Jupiter code and annotations in a separate package so can have both JUnit 4 and 5 in the same code base. You would still need to add the new JUnit platform plus JUnit 4 test support which is called JUnit Vintage. There is a nice article about the migration you can find here. So in a nutshell, JUnit 5 = Platform + Jupiter + Vintage.

We actually have a project with hundreds of tests and we step-by-step migrate them. All new tests are written with JUnit 5 and once we need to touch existing tests, we migrate them. It basically delete all JUnit imports, and replace @Before by @BeforeEach as well as @BeforeClass by @BeforeAll.

Conclusion

Although Java itself could be improved a lot by e.g. introducing meta programming (which has its disadvantages, too) or at least allow string literals as method names to make tests even more readable, but JUnit 5 is way better than JUnit 4 and I would assume that there will be a lot of extensions to come in the future.

Moreover, there are a lot of improvements to JUnits assertion library but since I use AssertJ, I didn't cover that part. See the JUnit 5 documentation for reference.


Jan Wedel's DEV Profile