TLDR; Rather than migration your assertions line by line, create an abstraction class to represent the new implementation and then perform inline refactoring.
I’m experimenting with migrating my projects to JUnit 5.
Many of the “how to migrate to JUnit 5” blog posts show differences, but not a lot of strategies. I used a Branch By Abstraction strategy to migrate JUnit 4 Assertions. This allowed me to experiment with using JUnit5 assertions or AssertJ assertions.
Differences between JUnit 5 and JUnit 4
The main differences between JUnit4 and JUnit5 seem to be: annotations, rules, and assertions.
- If I find and replace, then I can change the annotations.
- Assertions, if they are statically imported, I could find and replace the static import statements.
But assertions have the issue that in JUnit5 the messages have to go at the end of the parameter call list.
e.g.
Junit 4 would be:
Assert.assertNotNull("name should not be null", name);
A direct conversion to Junit5 would be. But that would be incorrect because this means assert that the String “name should not be null” is not null, and if it is, show the error message in the variable name. Which is the opposite from the current Junit4 test.
Assertions.assertNotNull("name should not be null", name);
Assertions are the main issue I have to solve in my migration.
Useful Articles, but Not Viable Strategies
The following articles are all useful, but when they do present strategy for migrating, the strategy is a lot of manual work to check the assertions.
- https://junit.org/junit5/docs/current/user-guide/#migrating-from-junit4 JUnit 5 docs have migration tips
- https://www.baeldung.com/junit-5-migration explains differences but not “how to migrate”
- https://vocabhunter.github.io/2017/10/17/migrating-to-junit-5.html explains some IDE short cuts for switching assertion arguments, but this has to be done on a line by line basis
- annotations can be find and replace
- https://www.petrikainulainen.net/junit-5-the-ultimate-resource/
- https://medium.com/@ramtop/porting-your-existing-tests-to-junit5-1adfd7f0ea51
None of these seem to present a strategy I like for migrating to JUnit 5.
Going line, by line, while your code is ‘broken’ doesn’t seem like an effective approach.
Basic Steps
The first steps I took were:
- Commenting out JUnit 4 in the pom.xml
<!--
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
-->
- Adding JUnit 5 with the backwards compatibility functionality in the vintage engine
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.3.1</version>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.3.1</version>
</dependency>
This was a quick view of “What doesn’t work”.
For more complicated projects we might also need to add the migration support package.
You can find all the Migration tips on the JUnit 5 site
Assertion Abstractions
Since Abstractions are the main issue I face, I thought I might investigate Assertion libraries, after all if I’m going to have to change all the assertions, perhaps I should change them all to an Assertion library.
Perhaps migrate to AssertJ or Hamcrest first?
AssertJ
AssertJ uses an Assertions package, and has a fluent interface so might even be easier for beginners because of the code completion.
AssertJ has ‘soft’ assertions to assert on all assertions and report on all even when one fails. Which is functionality in JUnit 5 as well, but I don’t need to use JUnit 5 Assertions to get it.
AssertJ has migration scripts for helping migrate
I would have to read these to evaluate them first.
Hamcrest
I have used Hamcrest before. But stuck to JUnit 4 because it was simpler and had more code completion.
https://github.com/hamcrest/JavaHamcrest/wiki/The-Hamcrest-Tutorial
Branch By Abstraction
Since I want to evaluate the different libraries.
I could use a Branch by Abstraction approach.
Currently my code has
Assert.assertNotNull("name should not be null", name);
I only use a subset of “Assert” so I could easily create an “Assert” class which allows me to migrate to JUnit 5 without changing my code e.g.
package uk.co.compendiumdev.junitmigration.tojunit5;
import org.junit.jupiter.api.Assertions;
public class Assert {
public static void assertNotNull(final String message, final String actual) {
Assertions.assertNotNull(actual, message);
}
}
If I had the above then I could simply import that into my Test Class, no need to change the code in my Test Class and then I have used Junit 5 Assertions.
If I used this throughout my code base, then instead of working on Assertion migration at a line by line level, I could use IntelliJ to perform an inline refactor for each method and it would change my Junit4 code from
Assert.assertNotNull("name should not be null", name);
and change it to JUnit 5
Assertions.assertNotNull(name, "name should not be null");
I haven’t seen this strategy mentioned in any of the migration blogs that I read.
Branch By Abstraction to AssertJ
Because this is a Branch by Abstraction, I could experiment with different implementations. I could create a AssertJ implementation of my Assert abstraction:
package uk.co.compendiumdev.junitmigration.toassertj;
import static org.assertj.core.api.Assertions.assertThat;
public class Assert {
public static void assertNotNull(final String message, final String actual) {
assertThat(actual).isNotNull().overridingErrorMessage(message);
}
}
By changing the imports I could switch between AssertJ or JUnit5 without changing my test code until I am happy to settle on one of the approaches.
I could clearly make this even more flexible by coding to an interface and having the Assert wrapper be a configurable factory which switches between the two, but I don’t need that degree of complexity.
Branch By Abstraction Strategy for Assertion Migration
This seems to be a simpler approach to assertion migration than I’ve seen mentioned.
This approach is open to me even if I statically import the methods, because I can statically import the methods from my abstraction class instead of the main library.
I’m not sure why this isn’t the most communicated migration approach.
I created a video showing this approach in action below, and you can see all the code on github:
And the Video