Migrating from JUnit 4 to JUnit 5

1. Overview

In this article, we’ll see how we can migrate from JUnit 4 to the latest JUnit 5 release – with an overview of the differences between the two versions of the library.

For the general guidelines on using JUnit 5, see our article here.

2. JUnit 5 Advantages

Let’s start with the previous version – JUnit 4 has some clear limitations:

  • The entire framework was contained in a single jar library. The whole library needs to be imported even when only a particular feature is required. In JUnit 5, we get more granularity and can import only what is necessary
  • One test runner can only execute tests in JUnit 4 at a time (e.g. SpringJUnit4ClassRunner or Parameterized ). JUnit 5 allows multiple runners to work simultaneously
  • JUnit 4 never advanced beyond Java 7, missing out on a lot of features from Java 8. JUnit 5 makes good use of Java 8 features

The idea behind JUnit 5 was to completely rewrite JUnit 4 to solve most of these drawbacks.

3. Differences

JUnit 4 was divided into modules that comprise JUnit 5:

  • JUnit Platform – this module scopes all the extension frameworks we might be interested in test execution, discovery, and reporting
  • JUnit Vintage – this module allows backward compatibility with JUnit 4 or even JUnit 3

3.1. Annotations

JUnit 5 comes with important changes within its annotations. The most important one is that we can no longer use @Test annotation for specifying expectations.

The expected parameter in JUnit 4:

@Test(expected = Exception.class)
public void shouldRaiseAnException() throws Exception {
    // ...
}

Now, we can use a method assertThrows:

public void shouldRaiseAnException() throws Exception {
    Assertions.assertThrows(Exception.class, () -> {
        //...
    });
}

The timeout attribute in JUnit 4:

@Test(timeout = 1)
public void shouldFailBecauseTimeout() throws InterruptedException {
    Thread.sleep(10);
}

Now, the assertTimeout method in JUnit 5:

@Test
public void shouldFailBecauseTimeout() throws InterruptedException {
    Assertions.assertTimeout(Duration.ofMillis(1), () -> Thread.sleep(10));
}

Other annotations that were changed within JUnit 5:

  • @Before annotation is renamed to @BeforeEach
  • @After annotation is renamed to @AfterEach
  • @BeforeClass annotation is renamed to @BeforeAll
  • @AfterClass annotation is renamed to @AfterAll
  • @Ignore annotation is renamed to @Disabled

3.2. Assertions

We can now write assertion messages in a lambda in JUnit 5, allowing the lazy evaluation to skip complex message construction until needed:

@Test
public void shouldFailBecauseTheNumbersAreNotEqual_lazyEvaluation() {
    Assertions.assertTrue(
      2 == 3, 
      () -> "Numbers " + 2 + " and " + 3 + " are not equal!");
}

We can also group assertions in JUnit 5:

@Test
public void shouldAssertAllTheGroup() {
    List<Integer> list = Arrays.asList(1, 2, 4);
    Assertions.assertAll("List is not incremental",
        () -> Assertions.assertEquals(list.get(0).intValue(), 1),
        () -> Assertions.assertEquals(list.get(1).intValue(), 2),
        () -> Assertions.assertEquals(list.get(2).intValue(), 3));
}

3.3. Assumptions

The new Assumptions class is now in org.junit.jupiter.api.Assumptions. JUnit 5 fully supports the existing assumptions methods in JUnit 4 and also adds a set of new methods to allow running some assertions only under specific scenarios only:

@Test
public void whenEnvironmentIsWeb_thenUrlsShouldStartWithHttp() {
    assumingThat("WEB".equals(System.getenv("ENV")),
      () -> {
          assertTrue("http".startsWith(address));
      });
}

3.4. Tagging and Filtering

In JUnit 4 we could group tests by using the @Category annotation. With JUnit 5, the @Category annotation gets replaced with the @Tag annotation:

@Tag("annotations")
@Tag("junit5")
@RunWith(JUnitPlatform.class)
public class AnnotationTestExampleTest {
    /*...*/
}

We can include/exclude particular tags using the maven-surefire-plugin:

<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <configuration>
                <properties>
                    <includeTags>junit5</includeTags>
                </properties>
            </configuration>
        </plugin>
    </plugins>
</build>

3.5. New Annotations for Running Tests

The @RunWith was used to integrate the test context with other frameworks or to change the overall execution flow in the test cases in JUnit 4.

With JUnit 5, we can now use the @ExtendWith annotation to provide similar functionality.

As an example, to use the Spring features in JUnit 4:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  {"/app-config.xml", "/test-data-access-config.xml"})
public class SpringExtensionTest {
    /*...*/
}

Now, in JUnit 5 it is a simple extension:

@ExtendWith(SpringExtension.class)
@ContextConfiguration(
  { "/app-config.xml", "/test-data-access-config.xml" })
public class SpringExtensionTest {
    /*...*/
}

3.6. New Test Rules Annotations

In JUnit 4, the @Rule and @ClassRule annotations were used to add special functionality to tests.

In JUnit 5. we can reproduce the same logic using the @ExtendWith annotation.

For example, say we have a custom rule in JUnit 4 to write log traces before and after a test:

public class TraceUnitTestRule implements TestRule {
 
    @Override
    public Statement apply(Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                // Before and after an evaluation tracing here 
                ...
            }
        };
    }
}

And we implement it in a test suite:

@Rule
public TraceUnitTestRule traceRuleTests = new TraceUnitTestRule();

In JUnit 5, we can write the same in a much more intuitive manner:

public class TraceUnitExtension implements AfterEachCallback, BeforeEachCallback {

    @Override
    public void beforeEach(TestExtensionContext context) throws Exception {
        // ...
    }

    @Override
    public void afterEach(TestExtensionContext context) throws Exception {
        // ...
    }
}

Using JUnit 5’s AfterEachCallback and BeforeEachCallback interfaces available in the package org.junit.jupiter.api.extension, we easily implement this rule in the test suite:

@RunWith(JUnitPlatform.class)
@ExtendWith(TraceUnitExtension.class)
public class RuleExampleTest {
 
    @Test
    public void whenTracingTests() {
        /*...*/
    }
}

3.7. JUnit 5 Vintage

JUnit Vintage aids in the migration of JUnit tests by running JUnit 3 or JUnit 4 tests within the JUnit 5 context.

We can use it by importing the JUnit Vintage Engine:

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

4. Conclusion

As we’ve seen in this article, JUnit 5 is a modular and modern take on the JUnit 4 framework. We have introduced the major differences between these two versions and hinted how to migrate from one to another.

The full implementation of this tutorial can be found in over on GitHub.