What is JUnit 5?
According to its website,
JUnit 5 is the next generation of JUnit. The goal is to create an up-to-date foundation for developer-side testing on the JVM. This includes focusing on Java 8 and above, as well as enabling many different styles of testing.
This article is for everyone that did or did not hear about JUnit and wonders why they would want to use it.
I don't want to go into details on all the features or how it works internally, I just want to show some of the new features that are most valuable to me so far.
So let's get right into it...
Display Name
Previously in JUnit, tests got their names from their test methods.
E.g.:
public class AccountTest {
@Test
public void testMethod() {
// do some tests here
}
}
When you run it, it will print out testMethod
as the name for the test which is problably not very useful and most importantly not readable. Test shoud be human readable because tests are the only true code documentation that does not get deprecated. So people started trying to come up with some more descriptive test names.
public class AccountTest {
@Test
public void withdrawSomeAmountGreaterThanCurrentBalanceFailsWithExcepetion() {
// do some tests here
}
}
Better? Not so much. At least it gives you some idea on what is going to happen here. Something about withdrawing some amount from an account that is greater than the current balance. That should fail. But how to make it more readable? Some developers (including myself) started breaking with the strict Java convention of camelCase and started using underlines to separate aspects of the test, for instance method__scenario__expectedResult
.
public class AccountTest {
@Test
public void withdraw__amountGreaterThanCurrentBalance__failsWithExcepetion() {
// do some tests here
}
}
This is a bit more readable although it hurts the eyes of every Java developer that is used to camelCase. What you actually want is using any arbitrary string as the test name.
Kotlin allow you to use string literals like e.g.
internal class AccountTest {
@Test
fun `should thrown an exception when an amount is withdrawn greater that the current balance`() {
// do some tests here
}
JUnit 5 to the rescue. Although not as simple as in Kotlin, JUnit 5 comes with an @DisplayName
annotation that can be used for both test classes and methods. The downside is, you still need to make a method name:
@DisplayName("An account")
class AccountTest {
@Test
@DisplayName("should throw an exception when an amount is withdrawn greater that the current balance")
void withdrawBalanceExceeded() {
// do some tests here
}
}
So although its the typical Java annotation cluttering going on, the result (as in what the IDE shows) is much more readable.
Note that you do not need to use public
anymore with methods and classes.
Nested Tests
While pair-programming with a colleague, I was copy and pasting tests and descriptions and just change parts of it to tests different edge cases. She told me I should use nested tests. We tried it and I immediately fell in love with the idea. Instead of having test like this:
- An account
- should throw an exception when an amount is withdrawn greater that the current balance
- should reduce the balance by the amount that is withdrawn
You could do something like that
- An account
- withdrawal
- should throw an exception when the amount greater that the current balance
- should reduce the balance by the amount
- withdrawal
With JUnit 5, it looks like this:
@DisplayName("An account")
public class AccountTest {
@Nested
@DisplayName("withdrawal")
class Withdrawal {
@Test
@DisplayName("should throw an exception when the amount greater that the current balance")
public void failureBalanceExceeded() {
// do some tests here
}
@Test
@DisplayName("should reduce the balance by the amount")
public void success() {
// do some tests here
}
}
}
Again, the gross number of lines increase by using @Nested
as it is necessary to wrap tests within a nested class but it keeps the tests its self cleaner.
Extensions
JUnit 5 is built to be very extensible. Custom or third-party extensions can be added by using the @ExtendWith
annotation at class level.
Spring Boot
Spring boot integration tests for example are now run by using @ExtendWith(SpringExtension.class)
. Note that this only works from spring-boot 2.x and higher.
Mockito Extension
There will be an updated MockitoExtension
that could do something like that:
@ExtendWith(MockitoExtension.class)
class MyMockitoTest {
@BeforeEach
void init(@Mock Person person) {
when(person.getName()).thenReturn("Dilbert");
}
@Test
void simpleTestWithInjectedMock(@Mock Person person) {
assertEquals("Dilbert", person.getName());
}
}
Migration from 4 to 5
Those new features in JUnit 5 come at a price: They are not compatible with JUnit 4 tests. This is called JUnit Jupiter.
"But wait, what about my existing 500 test cases suite?" you might ask. It not as bad as you might think. The JUnit team did a smart thing, they moved all JUnit Jupiter code and annotations in a separate package so can have both JUnit 4 and 5 in the same code base. You would still need to add the new JUnit platform plus JUnit 4 test support which is called JUnit Vintage. There is a nice article about the migration you can find here. So in a nutshell, JUnit 5 = Platform + Jupiter + Vintage.
We actually have a project with hundreds of tests and we step-by-step migrate them. All new tests are written with JUnit 5 and once we need to touch existing tests, we migrate them. It basically delete all JUnit import
s, and replace @Before
by @BeforeEach
as well as @BeforeClass
by @BeforeAll
.
Conclusion
Although Java itself could be improved a lot by e.g. introducing meta programming (which has its disadvantages, too) or at least allow string literals as method names to make tests even more readable, but JUnit 5 is way better than JUnit 4 and I would assume that there will be a lot of extensions to come in the future.
Moreover, there are a lot of improvements to JUnits assertion library but since I use AssertJ, I didn't cover that part. See the JUnit 5 documentation for reference.