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
sbrannen posted on
Jan 18, 2018
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.
暂无评论内容