How to migrate from JUnit 4 to JUnit 5: a step-by-step guide

Symflower's guide to migrating from JUnit 4 to JUnit 5

This step-by-step guide walks you through the JUnit 5 upgrade process to help you migrate from JUnit 4 to JUnit 5. For a fully automated solution, you can use Symflower to handle the entire conversion automatically, with no manual changes required.

💡 A series on JUnit testing

Table of contents:

Automatically migrate JUnit 4 to JUnit 5.

Introduction to JUnit

JUnit has been one of the most popular testing frameworks for a long time, with 85% of Java developers claiming to use it in JetBrains' 2021 survey. In addition, JUnit also plays a crucial role in the practice of TDD (Test-Driven Development), a trending strategy for dev teams all over the world.

As the first major update since JUnit 4’s 2006 release, JUnit 5 dropped in 2017 with a range of new features. While version 5 of JUnit promises numerous benefits, migration is far from straightforward if you’re not familiar with all that’s changed. This migration guide from JUnit 4 to 5 aims to help your team reduce its technical debt by making the transition smoother.

In this blog post, we’ll cover the basic reasons to upgrade to JUnit 5, as well as a guide to all the steps you have to take to migrate from JUnit 4 to 5. However, if you want to migrate your projects right away, you can use Symflower to handle the entire conversion automatically.

Differences between JUnit 4 vs JUnit 5: why upgrade?

JUnit 4 has certain limitations that become very apparent when compared to JUnit 5:

  • With JUnit 4, a single JAR library contains the framework, making it a little clunky to work with. Even if you’re only planning to use a single feature, you’ll need to import the entire library. In contrast, JUnit 5 offers more granularity that makes it easier to work with especially when using the Maven and Gradle build systems.
  • JUnit 4 only allows you to execute tests with one test runner at a time, while JUnit 5 allows more extensions at a time which lets you run multiple parallel tests. The Spring extension in JUnit 5 is easily combined with 3rd party or custom extensions.
    • With JUnit 5 supporting Java 8 features like lambda functions, you can create more powerful tests that are also simpler to maintain.
  • JUnit 4 lacks the useful features of JUnit 5 for describing, organizing, and executing tests. For example, to give a better overview, JUnit 5 lets you organize tests in hierarchies with better display names.

The great news is that you don’t have to migrate all at once to reap all those benefits, since JUnit 4 and 5 can coexist. JUnit 4-specific artifacts are located in the old org.junit base package, while JUnit 5 provides new annotations and classes that are stored in the new org.junit.jupiter. There is no conflict, letting you take a gradual hybrid approach to migrating over to JUnit 5.

JUnit 4 to JUnit 5 migration: a step-by-step migration guide

Before we dive in, you should know there’s a fundamental architecture change from JUnit 4 to 5 that impacts how migration should be done. Specifically, while JUnit 4 consisted of a single module, JUnit 5 has 3 separate sub-projects or modules:

  • JUnit Jupiter: Brings a new programming model and extension model that includes new assertions, new annotations, Java 8 Lambda Expressions, etc.
  • JUnit Platform: Serves as the foundation for launching test frameworks on the JVM and defines the TestEngine API for the testing framework.
  • JUnit Vintage is dedicated to backwards compatibility by supporting the execution of legacy JUnit 3 and 4-based tests on the new JUnit 5 framework. This provides a gentle migration path for all developers to update to JUnit 5.

Overall, the migration process will take you through the following four key steps:

  1. Replacing dependencies
  2. Updating testing classes and methods with new annotations
  3. Replacing rules and runners with JUnit 5’s new extension model

Let’s dive right in!

Automatically migrate JUnit 4 to JUnit 5.

1) Add JUnit 5 POM dependencies, remove JUnit 4 dependency

Because of its monolithic architecture, the running of JUnit 4 tests is enabled by a single dependency in the Maven configuration. Upon migrating to JUnit 5, you’ll be replacing that with the JUnit Vintage dependency. As defined above, this is what will ensure backwards compatibility with JUnit 4 tests and enables JUnit 4 and 5 tests to coexist while you go ahead and complete the migration process.

<dependency>
   <groupId>org.junit.jupiter</groupId>
   <artifactId>junit-jupiter-engine</artifactId>
   <version>${junit.version}</version>
</dependency>
<dependency>
   <groupId>org.junit.vintage</groupId>
   <artifactId>junit-vintage-engine</artifactId>
   <version>${junit.version}</version>
</dependency>
<dependency>
   <groupId>org.junit.platform</groupId>
   <artifactId>junit-platform-launcher</artifactId>
   <version>${junit.platform.version}</version>
</dependency>
<dependency>
   <groupId>org.junit.platform</groupId>
   <artifactId>junit-platform-runner</artifactId>
   <version>${junit.platform.version}</version>
</dependency>

Using JMock? Don’t forget that you’ll also need to upgrade the POM dependency to jmock-junit5:

<dependency>
  <groupId>org.jmock</groupId>
  <artifactId>jmock-junit5</artifactId>
  <version>${jmock.version}</version>
  <scope>test</scope>
</dependency>

2) Introduce JUnit 5 Jupiter annotations, assertions, and assumptions

JUnit 5 uses different annotations than JUnit 4. In order to be able to use common annotations, you’ll need to use different imports. See our mapping of packages and a description of JUnit 5 annotations below. Overall, a key change is that test classes and methods don’t necessarily have to be public in JUnit 5.

As an example for new packages: in the case of @Test, instead of the old org.junit.Test; you’ll be using org.junit.jupiter.api.Test;

All core annotations reside in the org.junit.jupiter.api package (in the junit-jupiter-api module), while assertions are found in org.junit.jupiter.api.Assertions.

Different annotations in JUnit 4 vs JUnit 5

JUnit 5 brings an update to some annotations. The following table from HowToDoInJava sums this up perfectly:

Feature JUnit 4 JUnit 5
Declare a test method @Test @Test
Execute before all test methods in the current class @BeforeClass @BeforeAll
Execute after all test methods in the current class @AfterClass @AfterAll
Execute before each test method @Before @BeforeEach
Execute after each test method @After @AfterEach
Disable a test method/class @Ignore @Disabled
Test factory for dynamic tests NA @TestFactory
Nested tests NA @Nested
Tagging and filtering @Category @Tag
Register custom extensions NA @ExtendWith
Automatically migrate JUnit 4 to JUnit 5.

Let’s take a deeper look at these JUnit 5 annotations:

  • @Test: You’ll be using this one (with no attributes – that’s a change from JUnit 4, see more on this in a second) simply to declare a test method. Note that since attributes can no longer be used, the expected attribute isn’t available – instead, use the assertThrows() method. Similarly, the timeout attribute is replaced by the assertTimeout() method.
  • @ParameterizedTest: Used to denote that the method is a parameterized test.
  • @RepeatedTest: The method annotated with @RepeatedTest is to be used as a test template for repeated tests.
  • @TestFactory: Denotes that the method is a test factory for dynamic tests.
  • @TestTemplate: Used to denote that a method is a template for test cases to be repeated. The number of times it is invoked will be dependent on the variety of invocation contexts returned by the registered providers.
  • @TestClassOrder: Used for @Nested test classes, this annotation lets you configure the execution order for your test class.
  • @TestMethodOrder: You may know this one from JUnit 4 as @FixMethodOrder, used to configure the order of test method execution for your test class.
  • @TestInstance: Use it to configure the test instance lifecycle for your test class.
  • @DisplayName: To ensure an easy overview of your code, this annotation declares a custom display name for your test class or method. Note that such annotations are not inherited!
  • @DisplayNameGeneration: Use it to declare a custom display name generator for your test class.
  • @BeforeEach: Like JUnit 4’s @Before, methods annotated with @BeforeEach will be executed before each @Test, @RepeatedTest, @ParameterizedTest, or @TestFactory methods in your class.
  • @AfterEach: Similar to the previous one but used to denote that the method should be executed after each @Test, @RepeatedTest, @ParameterizedTest, or @TestFactory methods in your class. You used this one as @After in JUnit 4.
  • @BeforeAll: Replacing JUnit 4’s @BeforeClass annotation. As the name suggests, the method annotated with this one will be executed before all @Test, @RepeatedTest, @ParameterizedTest, or @TestFactory methods in your class.
  • @AfterAll: This one replaces JUnit 4’s @AfterClass annotation. You guessed it: methods annotated with @AfterAll will be executed after all @Test, @RepeatedTest, @ParameterizedTest, or @TestFactory methods in your class.
  • @Nested: Use this new annotation to annotate a class that is a non-static nested test class. Keep in mind that between Java versions 8 to 15, you can’t use @BeforeAll and @AfterAll methods in a @Nested test class – that is, unless you’re using a per-class test instance lifecycle. These annotations are not inherited.
  • @Tag: Tags are used for filtering tests both at the class and method levels. @Tags are replacing @Categories in JUnit 4 and are only inherited at the class level (not at the method level).
  • @Disabled: This one replaces JUnit 4’s @Ignore and is used to disable a test class or method. This annotation is not inherited.
  • @Timeout: Use this to fail a test, test factory, test template, or lifecycle method in case its execution takes longer than a defined duration.
  • @ExtendWith: You’ll be using this to register extensions programmatically via fields.
  • @TempDir: Use this to supply a temporary directory by injecting fields or parameters in a lifecycle method or a test method. You’ll find this one in the org.junit.jupiter.api.io package!

Assertions in JUnit 5

The most notable change regarding assertions is the order of parameters in the assertion functions. Specifically, all assert methods in JUnit 4 (located in org.junit.Assert) accept parameters for error messages as the first argument.

public static void assertEquals(long expected, long actual)
public static void assertEquals(String message, long expected, long actual)

Source: junit.org

That changes in JUnit 5. Now, most assert() methods are found in org.junit.jupiter.Assertions and error messages are the last parameters. Assert methods in JUnit 5 have overloaded methods that support the display of parsing error messages upon test case failure. Here’s an example:

public static void assertEquals(long expected, long actual)
public static void assertEquals(long expected, long actual, String message)
public static void assertEquals(long expected, long actual, Supplier messageSupplier)

Source: junit.org

Assumptions in JUnit 5

To extend support for lambda expressions and method references in Java 8, JUnit 5 (specifically, JUnit Jupiter) brings changes in assumptions (as static methods in the org.junit.jupiter.api.Assumptions class, instead of the org.junit.Assume class that you’re used to).

JUnit 4 (org.junit.Assume) offers the following methods for stating assumptions on the conditions of a test being considered meaningful:

  • assumeFalse()
  • assumeNoException()
  • assumeNotNull()
  • assumeThat()
  • assumeTrue()

That changes in JUnit 5, where org.junit.jupiter.api.Assumptions contains the methods for stating assumptions. The methods are also different:

  • assumeFalse()
  • assumingThat()
  • assumeTrue()

With assumeNoException and assumeNotNull removed, to migrate your tests, you’ll have to rewrite those assumptions using JUnit 5 methods, e.g. assumeFalse and assumeTrue. For instance, JUnit 4’s assumeNotNull(object) should be updated to assumeTrue(object != null).

Automatically migrate JUnit 4 to JUnit 5.

3) Use JUnit 5’s new rules and runners

As briefly touched on before, the JUnit Jupiter extension model replaces the runners and rules extension points you’re used to in JUnit 4. From now on, you’ll be using the Extension API concept instead of rules.

Since @RunWith is no longer available, you’ll be using the @ExtendWith annotation and the org.junit.jupiter.api.extension package. That means that if you’ve used the Spring test runner in the past, you’ll have to replace that with the Spring extension.

For example, the Spring framework’s SpringJUnit4ClassRunner was used with @RunWith(SpringJUnit4ClassRunner.class) in JUnit 4. JUnit 5 replaces that with “extensions”, for which Spring provides the class SpringExtension, to be used with @ExtendWith(SpringExtension.class).

If you’re looking for a gradual migration path rather than switching at once, you’ll find it helpful that the junit-jupiter-migrationsupport module provides support for a subset of JUnit 4 rules and subclasses:

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

Using the class level annotation @EnableRuleMigrationSupport in the org.junit.jupiter.migrationsupport.rules package enables you to avoid refactoring existing code lines that use the above rules.

Where to go from here

That concludes our JUnit 4 to 5 migration guide. You should now have a better understanding of the differences between JUnit 4 and JUnit 5 and how to make the migration process work. For a fully automated solution, you can use Symflower to handle the entire conversion automatically, with no manual changes required.

Have we missed any steps? Get in touch to let us know!

Try Symflower in your IDE to generate unit test templates & test suites
| 2023-07-19