If you’ve ever written more than a few lines of Java code, odds are you’ve also written JUnit tests. JUnit is the testing framework most of us use.

I recently found myself tasked with migrating several hundreds of JUnit 4 tests to JUnit 5. Being a developer, I started looking for ways to automate this. To my surprise, I couldn’t easily find an existing tool to do this. In this post, I’ll show you what solution I came up with, and how to use it.

Please note that this post does not attempt to argue for or against migrating your suite of unittests to JUnit5, it assumes you’ve already made the choice to do so. It also assumes you use IntelliJ IDEA as development environment.

Under the hood, the solution this post describes uses a tool that is older than I am: Awk. If you’ve never heard of Awk before, do yourself a favor and read the excellent Awk in 20 minutes. If you know it, and you’re thinking “Is Awk the best tool for the job of modifying code?”, the answer I’d give is “Maybe not, but it’s simple, and it works well enough.”

With that out of the way, let’s dive into the solution I came up with!

Table of Contents

  • Where can I find it?
  • What does it do?
  • How do I run this?
    • Build the docker image
    • Use the docker image
  • Now that I’ve run it, what do I do?
    • Add dependencies to your pom.xml
    • In IntelliJ: format your code and optimize your imports
    • In IntelliJ: fix whatever new lambda’s we’ve created
  • Limitations
  • Alternatives

Where can I find it?

The source is available on Gitlab: https://gitlab.com/techforce1/migrate-to-junit5.

What does it do?

  •  Migrate your imports to the new JUnit5 package names
  • @Before and @After become @BeforeEach and @AfterEach, respectively
  • @BeforeClass and @AfterClass become @BeforeAll and @AfterAll, respectively
  • @RunWith in combination with either of the following runners is replaced by its JUnit5 counterpart:
    1. SpringRunner/SpringJUnit4ClassRunner becomes SpringExtension
    2. MockitoJUnitRunner becomes MockitoExtension
  • @Ignore becomes @Disabled
  • @Tests using the feature where you declare what exception the code under test should throw by using the expected property is converted into a form that uses assertThrows().
    Please be aware that the entirety of the test body is placed within the lambda parameter to assertThrows().

    @Test(expected = IllegalArgumentException.class)
    public void arrangementNumberNull() {
        builderValidFields().arrangementNumber(null).build();
    }

    gets turned into:

    @Test
    public void arrangementNumberNull() {
        assertThrows(IllegalArgumentException.class, () -> builderValidFields().arrangementNumber(null).build());
    }

How do I run this?

Build the docker image

From within the root of this repository:

docker build . -t migrate-to-junit5

Use the docker image

From within the root of your project’s repository:

docker run --rm -ti --mount type=bind,src=${PWD},dst=/src migrate-to-junit5

When it’s done, it’ll print a list of all files it modified.

Now that I’ve run it, what do I do?

Add dependencies to your pom.xml

You’re going to need to figure out on your own where you need to put what dependency, but here are the ones you’ll need most often:

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

Occasionally, when you use the MockitoJUnitRunner, you’ll also need to add the following dependency:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>

NB: On all projects I’ve handled thus far, the parent POM already has dependency management for these dependencies, so you don’t need to explicitly version these dependencies.

In IntelliJ: format your code and optimize your imports

What’s probably the best way to do this, is through the use of the Version Control view:

IntelliJ

Then hit whatever shortcut you have for formatting code (probably something like CTRL+ALT+L). You’ll be presented a dialog asking you what you want to do. Configure it like so:

configuration

In IntelliJ: fix whatever new lambda’s we’ve created

So, as part of the migration, any usage of  @Test(expected = SomeException.class) gets turned into a test that calls  assertThrows(SomeException.class, () -> {/*test body goes here*/};). Sometimes, the original test consists of a single expression, which means the curly braces introduced by the migration aren’t necessary.

Thankfully, IntelliJ can fix that for us.

Hit whatever keyboard shortcut you have for Run Inspection by Name, which is CTRL+ALT+SHIFT+I for me (or use CTRL+SHIFT+A to open the action popup, and search for Run Inspection by Name). In the dialog that pops up, type Statement lambda can be replaced with expression lambda:

inspection

Then press Enter, and run the inspection on all Uncommitted files:

Uncommitted files

Finally, in the Inspection results view, if the inspection found anything, select everything (CTRL+A) and click the button that says Replace with expression lambda:

expression lambda

Limitations

  • There’s no support for @Rules. If your tests depend on any  @Rule , you’re on your own.
  • The script is rather naive in the way it adds imports: it relies on your IDE to remove unused ones. This can lead to the unfortunate situation where your code doesn’t compile because of unused imports (mostly the Mockito extension stuff).
  • The following cases may confuse the script and lead to incorrect conversion of tests that use the  @Test(expected=...) feature:
    1. Curly braces at the end of lines that aren’t syntactically relevant (such as in comments)
    2. Multiple curly braces on a single line

Alternatives

Since creating this solution, I’ve become aware of a few alternative solutions. Here’s a summary of the most interesting ones:

  1. Refactoring from within IntelliJ: much like this solution, it doesn’t pretend to be a perfect solution. It doesn’t appear to try to migrate any @RunWith annotations, but it does understand Java’s syntax, so it’s unlikely to produce uncompilable code.
  2. Semantic code search and transformation with Rewrite: Rewrite looks like a nice tool to write automated refactorings of (among others) Java code. The authors have written the rewrite-testing-frameworks module. I haven’t looked very hard into this, but I like the concept, and might reach for this first next time I run into a situation where I want to automate some refactoring.

That’s it. Enjoy!