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
- Part 1: How to write JUnit test cases? A step-by-step guide with examples covers the basics: why and how to write Java unit test cases with JUnit 5, as well as a step-by-step Java unit test tutorial and key best practices.
- Part 2: Advanced techniques takes a deeper dive into some of the more advanced techniques of JUnit such as parameterized tests, assumptions, and timeouts.
Table of contents:
- Introduction to JUnit
- Differences between JUnit 4 vs JUnit 5: why upgrade?
- JUnit 4 to JUnit 5 migration: a step-by-step migration guide
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:
- Replacing dependencies
- Updating testing classes and methods with new annotations
- Replacing rules and runners with JUnit 5’s new extension model
Let’s dive right in!
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 |
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, theexpected
attribute isn’t available – instead, use theassertThrows()
method. Similarly, thetimeout
attribute is replaced by theassertTimeout()
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 theorg.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)
.
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!
🤓 Check out these Java resources
- The best IntelliJ IDEA productivity plugins for Java developers
- What are the top Java unit testing frameworks & tools in 2024?
- What are Java modules and how to use them?
- The best static analysis tools and linters for Java
- How to write reusable code? Guide & best practices for reusability in Java
- Mocking frameworks for Java: Mockito vs EasyMock vs JMockit
- Java Unit Testing Frameworks Compared: JUnit vs TestNG differences and similarities