Migrating from JUnit 4 to JUnit 5: A Definitive Guide

In this article, we will take a look at the steps required for migrating from JUnit 4 to JUnit 5. We will see how to run existing tests along with the new version, and what changes we have to do to migrate the code.

This post is part of the JUnit 5 Tutorial.

JUnit 5 Advantages

JUnit 5 has been designed to be modular unlike the previous versions. The key point of the new architecture is to separate concerns between writing tests, extensions and tools.

JUnit has been split into three different sub-projects:

  • The basis, JUnit Platform provides build plugins and an API for writing test engines
  • JUnit Jupiter is the new API for writing tests and extensions in JUnit 5
  • Finally, JUnit Vintage allows us to run JUnit 4 tests with JUnit 5

One of the biggest flaws of JUnit 4 is that it does not support multiple runners (so you cannot use e.g. SpringJUnit4ClassRunner and Parameterized at the same time). In JUnit 5 this is finally possible by registering multiple extensions.

Furthermore, JUnit 5 utilizes Java 8 features like lambdas for lazy evaluation. JUnit 4 never advanced beyond Java 7 missing out on Java 8 features.

Also, JUnit 4 has shortcomings in parameterized tests and lacks nested tests. This has inspired third-party developers to create specialized runners for these situations. JUnit 5 adds better support for parameterized tests and native support for nested tests along with some other new features.

Running Tests

Let's see what we need to do to run existing test on the new platform. In order to run both JUnit 4 and JUnit 5 tests we need:

  • Jupiter test engine to run JUnit 5 tests
  • Vintage test engine to run JUnit 4 tests
  • Jupiter API to write JUnit 5 tests

In addition to this, to run the tests with Maven we also need the Surefire provider from JUnit Platform. We have to add all the dependencies to pom.xml:

Likewise, to run the tests with Gradle we also need the Gradle plugin from JUnit Platform. Again, we have to add all the dependencies to build.gradle:

Migrating Tests

Migration of existing tests mostly involves finding and replacing some package and class names. Let's see what changes there are.

Annotations

Annotations reside in the org.junit.jupiter.api package instead of org.junit package.

Some of the annotations are also different:

JUnit 4

JUnit 5

@Before

@BeforeEach

@After

@AfterEach

@BeforeClass

@BeforeAll

@AfterClass

@AfterAll

@Ignore

@Disable

In most cases, we can just find and replace the package and class names.

However, we can not use expected or timeout attributes with the @Test annotation anymore.

The expected attribute in JUnit 4 can be replaced with the assertThrows() method in Junit 5:

Similarly, the timeout attribute can be replaced with the assertTimeout() method:

We can also see that neither test classes nor test methods need to be public in JUnit 5. We might actually get a IDE warning that they can be made package-private.

Assertions

Methods for asserting reside in the org.junit.jupiter.api.Assertions class instead of org.junit.Assert class.

In most cases, we can just find and replace the package names.

However, if we have provided the assertion with a custom message we will get compiler errors. The optional assertion message is now the last parameter. This order of parameters feels more natural:

It is also possible to lazily evaluate assertion messages like in the example. This avoids constructing complex messages unnecessarily.

There is also another issue when asserting String objects with a custom assertion message. The order of the parameters is different but we won't get a compiler error because all the parameters are String type. We can easily spot these cases because the tests will fail when we run them.

Assumptions

Assumption methods reside in org.junit.jupiter.Assumptions class instead of org.junit.Assume class.

These methods have been changed in a similar way to assertions. The assumption message is now the last parameter:

Categories

The @Category annotation from JUnit 4 has been replaced with a @Tag annotation in JUnit 5. Also, we no longer use marker interfaces but instead pass the annotation a string parameter.

In JUnit 4 we use categories whereas in JUnit 5 we use tags:

We can configure filtering of tests by tags in Maven pom.xml:

Correspondingly, we can configure filtering in Gradle build.gradle:

Runners

The @RunWith annotation from JUnit 4 does not exist in JUnit 5. We can implement the same functionality by using the new extension model in the org.junit.jupiter.api.extension package and the @ExtendWith annotation.

For example, we might be using the Spring Test runner in JUnit 4. We have to replace the runner with a Spring extension in JUnit 5. If we are using Spring 5 the extension comes bundled with Spring Test:

However, if we are using Spring 4 it does not come bundled with SpringExtension. We can still use it but it requires an extra dependency from the JitPack repository.

To use SpringExtension with Spring 4 we have to add the dependency in Maven pom.xml:

Same way, we have to add the dependency in build.gradle when using Gradle:

Rules

The @Rule and @ClassRule annotations from JUnit 4 do not exist in JUnit 5. We can implement the same functionality by using the new extension model in the org.junit.jupiter.api.extension package and the @ExtendWith annotation.

Migration Support

However, to provide a gradual migration path there is support for a subset of JUnit 4 rules and their subclasses in junit-jupiter-migrationsupport module:

  • ExternalResource (including e.g. TemporaryFolder)
  • Verifier (including e.g. ErrorCollector)
  • ExpectedException

Existing code using these rules can be left unchanged by using the class level annotation @EnableRuleMigrationSupport in the org.junit.jupiter.migrationsupport.rules package.

To enable the support in Maven we have to add the dependency in pom.xml:

To enable the support in Gradle we have to add the dependency in build.gradle:

Custom Rules

Migrating custom JUnit 4 rules requires re-writing the code as a JUnit 5 extension.

The rule logic applied as a @Rule can be reproduced by implementing the BeforeEachCallback and AfterEachCallback interfaces.

Respectively, we can reproduce rule logic applied as a @ClassRule by implementing the BeforeAllCallback and AfterAllCallback interfaces.

For example, if we have a JUnit 4 rule that does performance logging:

In turn, we can write the same rule as a JUnit 5 extension:

Summary

Migrating from JUnit 4 to JUnit 5 requires some work depending on how the existing tests have been written.

  • We can run JUnit 4 tests along with the JUnit 5 tests to allow for gradual migration.
  • In a lot of cases, we only have to find and replace package and class names.
  • We might have to convert custom runners and rules to extensions.

The example code for this guide can be found on GitHub.

About the Author

Arho is a software craftsman who wants to learn new things by teaching others. He helps people deliver more valuable software sooner, while maintaining a sustainable development pace.

>