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>
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
become
import org.junit.jupiter.api.Test;
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());
}
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 officialTemporaryFolder
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>
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");
}
}
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;
}
}
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();
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();
}
}
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();
}
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)
become
@ExtendWith(SpringExtension.class)
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)
become
@ExtendWith(MockitoExtension.class)
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
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>
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>
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.
Top comments (6)
I wrote a simple http server extension based on an existing junit4 one.
Existing one (junit4) gist.github.com/rponte/710d65dc3be...
Mine (junit5): gist.github.com/ahmed-musallam/f62...
really makes testing apache's HttpClient easy!
Hi Victor, many thanks for this nice guide. It helped me a lot.
I think you have a typo:
import org.junit.jupiter.api.Test;
Should be without the last semicolon
Thank you for your comment! Glad to hear it helps you.
Awesome post! thank you, helped me a lot!
Hi, starting with maven-surefire-plugin version 2.22.0 there is full support for JUnit 5 see the documentation maven.apache.org/surefire/maven-su....
Yes, thank you for your comment. A lot of things has changed since I wrote this post.
The extension
TemporaryExtension
is now available with the project junit-pioneer.org/.And Mockito now does fully support JUnit 5 -> twitter.com/sam_brannen/status/102...