Migrating From JUnit 4 to JUnit 5: A Step-By-Step Guide
Migrating from JUnit 4 to 5 can be a challenge if you’re not familiar with JUnit 5. Here’s a step-by-step guide that walks you through the process!
Join the DZone community and get the full member experience.
Join For FreeJUnit 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.
Developers use JUnit to implement unit testing, which in turn helps them improve code quality and accelerate programming. JUnit’s popularity is no surprise, considering that it contributes so much to enhanced productivity in delivering quality code. That said, some teams are still using JUnit 4 and are worried about the disruption that migrating to JUnit 5 could cause.
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 article, we’ll cover the basic reasons to make the switch, as well as the steps you have to take to migrate from JUnit 4 to 5.
Why Migrate From JUnit 4 to 5?
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.
Step-By-Step Migration Guide From JUnit 4 to 5
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 three 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
.
Annotations in 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 see a quick rundown of 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)
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)
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.
Closing Thoughts
And that should be it! Now, you should have a better understanding of the differences between JUnit 4 and JUnit 5, and the above information should help you manage a smooth migration process. For a definitive guide with extended examples on GitHub, we recommend checking out Arho Huttunen’s excellent guide!
Have we missed any steps? Visit to give feedback so we can update this article!
Published at DZone with permission of Kristof Horvath. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments