Migrating from JUnit 4 to JUnit 5

Posted on by Helen Scott

This post will help you take your project from previous versions of JUnit to JUnit5. IntelliJ IDEA has a number of tools to help facilitate the migration which you can perform in a series of steps that we talk about here.

This blog post covers the same material as the video. This provides an easy way for people to skim the content quickly if they prefer reading to watching, and to give the reader/watcher code samples and links to additional information.

Please note that JUnit 5 requires Java 8 (or higher) at runtime.

Migrating One Test to JUnit 5

JUnit has been around for a long time. Many applications will have a large number of JUnit tests written using JUnit 4. JUnit 5 was released in 2017, and provides a lot of features that weren’t in the previous version. This blog shows you how to migrate existing JUnit 4 tests to JUnit 5 using this java class initially. All the code before the migration we will go through is available here.

A typical JUnit 4 test contains sections:

  • code to run once at the start of all the tests
    @BeforeClass
    public static void beforeClass() throws Exception {
    System.out.println("JUnit4To5.beforeClass");
    }
  • code to run before each individual test
    @Before
    public void before() throws Exception {
    System.out.println("JUnit4To5.before");
    }
  • the tests themselves, some of which may be ignored
    @Test
    public void shouldMigrateASimpleTest() {
    Assert.assertEquals("expected", "expected");
    }
    @Test
    @Ignore
    public void shouldMigrateIgnoreTestToDisabledTest() {
    // This test is Ignored in JUnit 4 and should be Disabled in JUnit 5"
    }
  • code to run to clean up after each test
    @After
    public void after() throws Exception {
    System.out.println("JUnit4To5.after");
    }
  • code to run once to clean up after all tests
    @AfterClass
    public static void afterClass() throws Exception {
    System.out.println("JUnit4To5.afterClass");
    }

This project is currently using JUnit 4.13, but these guidelines apply to any version of JUnit 4. We also have a dependency on Hamcrest. We need to edit our Maven POM file.

<!-- start of pom.xml is above here -->
       <dependency>
           <groupId>junit</groupId>
           <artifactId>junit</artifactId>
           <version>4.13</version>
           <scope>test</scope>
       </dependency>
       <dependency>
           <groupId>org.hamcrest</groupId>
           <artifactId>hamcrest-library</artifactId>
           <version>2.2</version>
       </dependency>
<!-- rest of pom.xml below -->

The first thing we need to do in order to migrate to JUnit 5 is to add the JUnit 5 dependencies. We can get IntelliJ IDEA to do this for us, if we press ⌃⏎ or, Alt+Enter within the POM file, the IDE will offer the option of adding a new dependency.

Typing ‘JUnit’ will show junit-jupiter dependencies, which are the JUnit 5 dependencies. To start with let’s add a dependency on the basic junit-jupiter-api. IntelliJ IDEA has identified that 5.7.0-M1 (milestone one) is the latest dependency. Let’s use a release version rather than a milestone, so use the drop-down to select 5.6.2 as this is the current release version.

Junit Jupiter 5.6.2

Because we want to run existing JUnit 4 tests with JUnit 5, we need to add one more dependency. Use ⌃⏎, or , Alt+Enter again and this time look for the JUnit vintage engine to run older tests. Once again, IntelliJ IDEA is suggesting version 5.7.0-M1. Expand the menu item using the right arrow and select version 5.6.2 so we’re using the same version as the API.

Junit Vintage Engine 5.6.2

IntelliJ IDEA will update the Maven POM file with the new dependency version and imports. Click the Maven icon in the top-right to ensure that IntelliJ IDEA loads the changes. Alternatively you can use ⇧⌘I on macOS, or SHIFT+CTRL+O on Windows and Linux.

We only need these dependencies to run tests, so add the test scope to them as well.

<dependency>
   <groupId>org.junit.jupiter</groupId>
   <artifactId>junit-jupiter-api</artifactId>
   <version>5.6.2</version>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.junit.vintage</groupId>
   <artifactId>junit-vintage-engine</artifactId>
   <version>5.6.2</version>
   <scope>test</scope>
</dependency>

Now we should be able to safely remove the old JUnit 4 dependency from the Maven POM file, and the project will still compile. Click the Maven icon to make sure that IntelliJ IDEA removes the old dependency from the project.

<dependency>
   <groupId>org.junit.jupiter</groupId>
   <artifactId>junit-jupiter-api</artifactId>
   <version>5.6.2</version>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.junit.vintage</groupId>
   <artifactId>junit-vintage-engine</artifactId>
   <version>5.6.2</version>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.hamcrest</groupId>
   <artifactId>hamcrest-library</artifactId>
   <version>2.2</version>
</dependency>

Navigate back to the Junit4ToJUnit5 class using ⌘O, or Ctrl + N, to find a file and rebuild the whole project with the new dependencies using ⌘F9 or Ctrl+F9. If it all builds correctly, use ⌃⇧R, or CTRL+SHIFT+F10, to run the test class to check they work as you expect.

The results are shown in the Run window which you can access using ⌘4, or ALT+4.

Test Run Window IntelliJ

We have added the new dependencies to the Maven pom.xml file and removed the old JUnit 4 dependency from it. Use ⌘K, or CTRL+K to commit your changes since you know everything still works as expected.

Using Inspections to Migrate All Tests

Use ⌘,(comma), or ⌃⌥S to load the IntelliJ IDEA preferences and type in ‘inspections’. IntelliJ IDEA’s inspections IntelliJ IDEA’s inspections can be very helpful for migrating code. In particular there are a number of inspections for JUnit tests.

To help with the migration, turn on ‘JUnit 4 test can be JUnit 5’ inspection. You can only turn this on if you’re using at least Java 8 and have JUnit 5 dependencies set up.

Junit 4 can be 5

Let’s open the Junit4ToJUnit5 test class to see this in action:

public class JUnit4To5 {
   @BeforeClass
   public static void beforeClass() throws Exception {
         System.out.println("JUnit4To5.beforeClass");
   }

   @Before
   public void before() throws Exception {
       System.out.println("JUnit4To5.before");
   }

   @Test
   public void shouldMigrateASimpleTest() {
       Assert.assertEquals("expected", "expected");
   }

   @Test
   @Ignore
   public void shouldMigrateIgnoreTestToDisabledTest() {
   }

   @Test
   public void shouldStillSupportHamcrestMatchers() {
       assertThat(1, equalTo(1));
   }

   @Test
   public void shouldStillSupportAssume() {
       Assume.assumeTrue(javaVersion() > 8);
   }

   @After
   public void after() throws Exception {
       System.out.println("JUnit4To5.after");
   }

   @AfterClass
   public static void afterClass() throws Exception {
       System.out.println("JUnit4To5.afterClass");
   }

   private int javaVersion() {
       return 14;
   }
}

With this inspection turned on, JUnit 4 tests that are currently running using the JUnit 5 vintage engine are flagged to show you that they can be migrated. Press ⌥⏎, or Alt+Enter, in the class name and IntelliJ IDEA offers to migrate the test class to JUnit 5.

Migrate JUnit 4 to 5

The refactoring preview window will show the places where changes will be made, in this case a number of annotations will be changed to the new JUnit 5 annotations. You can click ‘Preview Usages’ on the bottom-right corner to see the changes it will make to your class.

Annotation Refactoring Preview

The second section in this case, Unclassified usage contains methods that will be changed because the assertions will be updated.

Unclassified Refactoring Preview

If you want to exclude any of the tests from the refactor you can click , or Delete. To reinclude them again click ⇧⌫, or Shift+Delete. Press Do Refactor and let’s see what the new code looks like in the Maven POM file.

public class JUnit4To5 {
   @BeforeAll
   public static void beforeClass() throws Exception {
       System.out.println("JUnit4To5.beforeClass");
   }

   @BeforeEach
   public void before() throws Exception {
       System.out.println("JUnit4To5.before");
   }

   @Test
   public void shouldMigrateASimpleTest() {
       Assertions.assertEquals("expected", "expected");
   }

   @Test
   @Disabled
   public void shouldMigrateIgnoreTestToDisabledTest() {
   }

   @Test
   public void shouldStillSupportHamcrestMatchers() {
       assertThat(1, equalTo(1));
   }

   @Test
   public void shouldStillSupportAssume() {
       Assumptions.assumeTrue(javaVersion() > 8);
       // then go ahead and run the rest of the test
   }

   @AfterEach
   public void after() throws Exception {
       System.out.println("JUnit4To5.after");
   }

   @AfterAll
   public static void afterClass() throws Exception {
       System.out.println("JUnit4To5.afterClass");
   }

   private int javaVersion() {
       // stub method. just used for the example
       return 14;
   }

}

Use Local History (VCS | Local History | Show History) to see the changes side-by-side. The section on the left shows you the old JUnit 4 file. The changes from the refactor are shown on the right. The main change is that instead of importing org.junit, which is the JUnit 4 package, the class will import org.junit.jupiter, which is the JUnit 5 package structure.

Junit Local History Input

We’ll see that the before and after annotation names have been changed to the new, more descriptive, ones: BeforeClass becomes BeforeAll, Before becomes BeforeEach.

Junit Local History Before

The test annotation doesn’t change, because the name is still the same. Changing the import means this will now use the new Test annotation. Assert statements now use methods on the Assertions class, instead of the old Assert.

Junit Local History Test

The JUnit 4 Ignore annotation has been replaced with the JUnit5 Disabled annotation.

JUnit Local History Test Ignore

Any tests that used Hamcrest matchers should still work as before.

JUnit Local History Hamcrest

JUnit4 introduced the idea of assumptions, if these assumptions are met the test is run. JUnit5 provides an Assumptions class instead of Assume.

Like the Before Annotations, After is replaced with AfterEach, and AfterClass is replaced with AfterAll.

JUnit Local History After

IntelliJ IDEA makes all of these changes automatically. Like any IntelliJ IDEA refactoring, you can see the class still compiles after you’ve done the refactoring by using ⌘F9, or CTRL+F9. Let’s use ⌃⇧R, or CTRL+SHIFT+F10 to run the tests to make sure everything behaves the way we expect.

JUnit Run Test Results

The beforeClass method is run once, each before and after method is run once per test, and the afterClass method is run once at the end, so the overall setup works as it did under JUnit 4. The disabled test is still not run, and there are three passing tests, the same as before.

Automatically Migrate All Tests

Now we understand the individual steps to migrate a test, we can be a bit more ambitious with our migration. Let’s try to automatically migrate all our tests. Use Find Action with ⌘⇧A, or Ctrl+Shift+A, and type Migrate, to see migration options for the code. IntelliJ IDEA offers the option to migrate the code from JUnit4 to JUnit5.

Junit Migrate 4 to 5

This migration is similar to what we did with the individual test class, but for all test classes. Press Run to see the refactoring preview. You can exclude specific classes or methods here using if you want to using ⇧⌫, or Shift+Delete. Press Do Refactor to do the refactoring.

Junit Refactored Unit Tests

We want to check all our tests still work as before, so run them using ⌃⇧R, or CTRL+SHIFT+F10 as before. You can also right-click on the ‘tests’ folder and select Run ‘All Tests’. The Test Results window shows us the outcome of running the tests.

Press the grey tick/check mark at the top-right to hide all the tests that passed and then press the icon next to that to hide all the tests that are disabled since these don’t need our attention or we don’t expect these to run respectively.

JUnit Hide Passed and Ignored Tests

After the migration, you can see that the test classes have been changed. We can go through each of the files to check the changes before we commit them. ⇧⌘], or Ctrl + Right Arrow, to move to the next file to compare so you can quickly check all the files. As these are mostly simple JUnit tests, most of the changes are a change to the imports to use the new JUnit Jupiter test annotation instead of the old JUnit test annotation.

Manual Migration for More Complex Cases

If the tests in the project are fairly straightforward ones, this automatic migration should take care of most of the tasks that need to happen. However, not all cases can be managed automatically. Migration from JUnit 4 to JUnit 5 could be a slow and steady process.

At this point in our project, we have some tests fully migrated to JUnit 5, some partially migrated but using some JUnit 4 features, and potentially there may be some classic JUnit 4 (or even JUnit 3) tests running as they are. We’re running them all using JUnit 5, so we can write new tests using the new JUnit 5 features but keep existing stable tests as they are.

However we might want to remove the dependency on the vintage engine to prevent ourselves from accidentally writing tests in the old style. If we remove this dependency, refresh the Maven changes and rebuild the project, we will be able to see which classes require the vintage engine, and therefore which tests need more help migrating to JUnit 5.

One example of a change that we might do manually is to use the correct import for Hamcrest’s assertThat in the ExceptionStackTraceTest class, for example. We could do this by removing the reference to the JUnit Assert class, and getting IntelliJ IDEA to static import the Hamcrest MatcherAssert.assertThat. We can use optimise imports to remove the unnecessary JUnit 4 Assert import.

AssertThat Refactor Example

You may discover that some tests are even JUnit 3 style tests, such as our NavigateToTestTest class. These tests extend a TestCase instead of using annotations.

Junit 3 Test Error

To address this, put the vintage dependency back in to the Maven POM file so that the project compiles again, and reload the maven changes. You can use ⌥⏎, or Alt+Enter, on the class that is using a JUnit 3 test case. IntelliJ IDEA will suggest migrating to JUnit as a good first step.

Junit 3 Migration in IntelliJ

The test now uses annotations, and doesn’t import anything from the old junit.framework package. Now you can take the next step and migrate this test to JUnit5.

Junit 4 to Migration IntelliJ

We might also want to do optimise imports to remove any old imports that aren’t needed. To do that, we can use a Find in Path search with ⌃⇧F, or CTRL+SHIFT+F, and type org.junit. This will show up everything, including our new Jupiter classes. We could use an expression to capture that the next character after the dot needs to be an upper case letter so we need to type ‘org.junit.[A-Z]’ and select Search with Regex and then match the case.

Junit Regex Search

Now we can see all the remaining files that import a class from org.junit. There are lots of ways we could fix this, one option is to simply remove the old import, use the new JUnit 5 Assertions class, and add a static import for the assertEquals method so it can be used by all tests in the class.

Manual JUnit Refactor

If your project has a number of these tests you can run this inspection alone on the whole codebase to automatically migrate all of those tests in one step.

Other Approaches to Consider

This specific problem, for example, can be fixed with an inspection. If we turn on ‘Obsolete assertions in JUnit 5 tests’ we can get IntelliJ IDEA to suggest using the new JUnit 5 assertions. Now, when we go back to the editor, there is a warning on the old assert statement, and we can get IntelliJ IDEA to automatically use the new JUnit 5 Assertions instead. IntelliJ IDEA does have inspections and automatic fixes to help migrate code from JUnit 4 to JUnit 5, but if there isn’t a specific fix, we might take another approach such as a find and replace, or a Structural Search, which might be more appropriate if there are a lot of files to change but the changes are all the same. We could use live templates for common code, and we could configure our auto import settings to make sure the new JUnit imports are suggested over the old ones. The right solution will depend upon what sort of changes need to be made and how many files are affected.

Final Steps

Now we’ve removed all references to JUnit 4 classes and functionality, we can remove the vintage engine and use just the modern JUnit 5 test engine. You can load the Maven changes and rebuild the project. You should check the tests still work and pass, which you can do by pressing CTRL twice to bring up the run anything window. If we type ‘All’ we can see the all tests run configuration and run all our project’s tests.

Summary

We have successfully migrated all the tests in this project to JUnit 5. The first step was to migrate the project to run our JUnit 4 tests on the JUnit 5 platform. For many projects it may be enough to do this, and there may be no need to go all the way to removing the vintage engine. The most important thing is to be able to write and run tests in the new JUnit 5 style. We’ll cover the advantages of using JUnit 5 in another video.

See also:

Subscribe

Subscribe for updates