Migration from Junit 4 to Junit 5

vga

Victor Gallet

Posted on April 23, 2018

Migration from Junit 4 to Junit 5

While working on my current project, I got some time to migrate from JUnit 4 to JUnit 5.
Since JUnit 5 was released in September 2017, it's the right time to take a look at it.

My application is a java 8 maven project divided into 7 maven modules and each module has it owns integration and unit tests. However, one of these modules is dedicated to tests. It contains all the test needed dependencies and it's injected as scope test into others modules.
Our tests dependencies are the most common in a Java project. We use JUnit 4, AssertJ, Mockito, DbUnit and Spring Test.

At last, we also have a dedicated project to run end-to-end testings based on Selenium, Fluentlenium and JGiven.
Unfortunately, JGiven does not fully support JUnit 5. It's currently in an experimental state, so I haven't started this migration.

Dependencies

Let's start by adding the new JUnit dependencies :

<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>

Enter fullscreen mode Exit fullscreen mode

The important to take note of is the import of junit-vintage-engine. It provides the ability to run JUnit 4 tests and JUnit 5 tests simultaneously without difficulty.

Unit Tests

The next step is to replace all imports of old JUnit annotations by the newest.

import org.junit.Test
Enter fullscreen mode Exit fullscreen mode

become

import org.junit.jupiter.api.Test;
Enter fullscreen mode Exit fullscreen mode

Here's the mapping of each annotation:

JUnit 4 Junit 5
org.junit.Before org.junit.jupiter.api.BeforeEach
org.junit.After org.junit.jupiter.api.After
org.junit.BeforeClass org.junit.jupiter.api.BeforeAll
org.junit.AfterClass org.junit.jupiter.api.AfterAll
org.junit.Ignore org.junit.jupiter.api.Disabled

As we use AssertJ for all our assertions, I didn't need to migrate JUnit 4 assertions.

Rules

One the biggest change is the removal of the concept of rules, that has been replaced by extension model. The purpose of extension is to extend the behavior of test classes or methods and it replaces JUnit runner and Junit Rules.

One rule that we all have used is ExpectedException and it can be easily replaced by JUnit assertThrows :

   @Test
    void exceptionTesting() {
        Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
            throw new IllegalArgumentException("a message");
        });
        assertEquals("a message", exception.getMessage());
    }
Enter fullscreen mode Exit fullscreen mode

Another well-known rule to migrate is TemporaryFolder. Unfortunately, JUnit 5 does not provide a replacement yet. There is an open issue in Github.

Introduce a TemporaryFolder extension #1247

Overview

See discussion at https://github.com/junit-team/junit5-samples/issues/4.

Related Issues

  • #219

Deliverables

  • [X] Introduce an official TemporaryFolder extension for JUnit Jupiter analogous to the rule support in JUnit 4.

So what can we do to make it work?

First of all, it's possible to keep tests using those rule in JUnit 4 thanks to junit-vintage-engine.

Another solution is to continue to use JUnit 4 TemporaryFolder rule by adding the dependency junit-jupiter-migrationsupport.

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-migrationsupport</artifactId>
    <version>${junit.version}</version>
</dependency>

Enter fullscreen mode Exit fullscreen mode

This module enables to run JUnit 5 tests with rules. For example :

@EnableRuleMigrationSupport
public class JUnit4TemporaryFolderTest {

    @Rule
    public TemporaryFolder temporaryFolder = new TemporaryFolder();

    @Test
    public void test() throws IOException {
        temporaryFolder.newFile("new_file");
    }
}

Enter fullscreen mode Exit fullscreen mode

However, this feature only supports :

  • rules that extend org.junit.rules.ExternalResource
  • rules that extend org.junit.rules.Verifier
  • rule ExpectedException and it's currently marked as experimental so use it at your own risk.

Finally, one solution is to create our own TemporaryFolderExtension based on Junit 4 implementation.

public class TemporaryFolderExtension implements BeforeEachCallback, AfterEachCallback {

   private final File parentFolder;
   private File folder;

   public TemporaryFolderExtension() {
       this(null);
   }

   public TemporaryFolderExtension(File parentFolder) {
       this.parentFolder = parentFolder;
   }

   @Override
   public void afterEach(ExtensionContext extensionContext) {
       if (folder != null) {
           recursiveDelete(folder);
       }
   }

   @Override
   public void beforeEach(ExtensionContext extensionContext) throws IOException {
       folder = File.createTempFile("junit", "", parentFolder);
       folder.delete();
       folder.mkdir();
   }

   public File newFile(String fileName) throws IOException {
       File file = new File(getRoot(), fileName);
       if (!file.createNewFile()) {
           throw new IOException("a file with the name \'" + fileName + "\' already exists in the test folder");
       }
       return file;
   }

   public File newFolder(String folderName) {
       File file = getRoot();
       file = new File(file, folderName);
       file.mkdir();
       return file;
   }

   private void recursiveDelete(File file) {
       File[] files = file.listFiles();
       if (files != null) {
           for (File each : files) {
               recursiveDelete(each);
           }
       }
       file.delete();
   }

   public File getRoot() {
       if (folder == null) {
           throw new IllegalStateException("the temporary folder has not yet been created");
       }
       return folder;
   }

}
Enter fullscreen mode Exit fullscreen mode

This implementation does not fully support all extension features like Parameter Resolution but at least, it allows us to fully migrate our tests to JUnit 5.
In addition, it's possible to inject extensions as rule by using @RegisterExtension

@RegisterExtension
public TemporaryFolderExtension temporaryFolder = new TemporaryFolderExtension();

Enter fullscreen mode Exit fullscreen mode

This annotation enables us to build an extension with parameters and to access is during test execution.

Custom Rules

In my case, I had only one custom rule to migrate. Its goal is to create an in-memory SMTP server for asserting sending emails.

public class SMTPServerRule extends ExternalResource {

   private GreenMail smtpServer;
   private String hostname;
   private int port;

   public SMTPServerRule() {
       this(25);
   }

   public SMTPServerRule(int port) {
       this("localhost", port);
   }

   public SMTPServerRule(String hostname, int port) {
       this.hostname = hostname;
       this.port = port;
   }


   @Override
   protected void before() throws Throwable {
       super.before();

       smtpServer = new GreenMail(new ServerSetup(port, hostname, "smtp"));
       smtpServer.start();
   }

   public List<ExpectedMail> getMessages() {
       return Lists.newArrayList(smtpServer.getReceivedMessages()).stream()
           .parallel()
           .map(mimeMessage -> ExpectedMail.transformMimeMessage(mimeMessage)).collect(Collectors.toList());
   }

   @Override
   protected void after() {
       super.after();
       smtpServer.stop();
   }
}

Enter fullscreen mode Exit fullscreen mode

To make it work as a JUnit extension, it only needs to implement BeforeEachCallback and AfterEachCallback interfaces instead of inheriting from ExternalResource. The main implementation is still the same.

public class SMTPServerExtension implements BeforeEachCallback, AfterEachCallback {

   private GreenMail smtpServer;
   private String hostname;
   private int port;

   public SMTPServerExtension() {
       this(25);
   }

   public SMTPServerExtension(int port) {
       this("localhost", port);
   }

   public SMTPServerExtension(String hostname, int port) {
       this.hostname = hostname;
       this.port = port;
   }

   public List<ExpectedMail> getMessages() {
       return Lists.newArrayList(smtpServer.getReceivedMessages()).stream()
           .parallel()
           .map(mimeMessage -> ExpectedMail.transformMimeMessage(mimeMessage)).collect(Collectors.toList());
   }


   @Override
   public void afterEach(ExtensionContext extensionContext) throws Exception {
       smtpServer.stop();
   }

   @Override
   public void beforeEach(ExtensionContext extensionContext) throws Exception {
       smtpServer = new GreenMail(new ServerSetup(port, hostname, "smtp"));
       smtpServer.start();
   }

Enter fullscreen mode Exit fullscreen mode

Integration Tests

Next, I had to update Spring integration tests and it was quite easy as class SpringExtension is included in Spring 5.

@RunWith(SpringJUnit4ClassRunner.class)
Enter fullscreen mode Exit fullscreen mode

become

@ExtendWith(SpringExtension.class)
Enter fullscreen mode Exit fullscreen mode

Mockito Tests

Let's continue with tests that use Mockito. Like we have done with Spring integration tests, we have to register an extension :

@RunWith(MockitoJUnitRunner.class)
Enter fullscreen mode Exit fullscreen mode

become

@ExtendWith(MockitoExtension.class)
Enter fullscreen mode Exit fullscreen mode

In fact, class MockitoExtension is not provided by Mockito yet and it will be introduced with Mockito 3.
One solution is the same as TemporaryFolderExtension...that is to keep our tests in JUnit 4. However, it's also possible to create our own extension and so Junit team give one implementation of MockitoExtension in its samples.
I decided to import it into my project to complete my migration.

Remove JUnit 4

Then, to ensure all my tests run under JUnit 5, I checked if there is any JUnit 4 dependency by executing :

mvn dependency:tree

Enter fullscreen mode Exit fullscreen mode

And so, I had to exclude some of them :

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>junit</groupId>
                    <artifactId>junit</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.dbunit</groupId>
            <artifactId>dbunit</artifactId>
            <version>${dbunit.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>junit</groupId>
                    <artifactId>junit</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

Enter fullscreen mode Exit fullscreen mode

Maven

Last but not least, I needed to update the maven surefire plugin to make it works with JUnit 5.

<!--
        The Surefire Plugin is used during the test phase of the build lifecycle to execute the unit tests of an application.
        -->
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>${maven-surefire-plugin.version}</version>
            <dependencies>
                <dependency>
                    <groupId>org.junit.platform</groupId>
                    <artifactId>junit-platform-surefire-provider</artifactId>
                    <version>1.1.1</version>
                </dependency>
                <dependency>
                    <groupId>org.junit.jupiter</groupId>
                    <artifactId>junit-jupiter-engine</artifactId>
                    <version>${junit.version}</version>
                </dependency>
            </dependencies>
        </plugin>

Enter fullscreen mode Exit fullscreen mode

Be careful with the version of your maven surefire plugin as the 2.20 has a memory leak. JUnit documentation suggests the version 2.21.

Conclusion

This migration was really easy, but even so, JUnit 5 is totally different from JUnit 4. In the end, I was able to remove the import of junit-vintage-engine as I don't have Junit 4 test anymore. I only regret the fact that I had to create my own temporary folder extension and Mockito extension.
Finally, it's possible to get more help with your migration by consulting Junit5-samples.

A big thanks to Sonyth, Mickael and Houssem for their time and proofreading.

💖 💪 🙅 🚩
vga
Victor Gallet

Posted on April 23, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related