5 reasons why you should use JUnit5 — with examples

Daniel Beleza
4 min readApr 13, 2023
Photo by Tony Hand on Unsplash

Introduction

When it comes to testing Java applications, JUnit has been the de facto standard for many years. However, with the release of JUnit 5, there are several compelling reasons to upgrade your testing framework. In this article, we’ll explore five advantages of JUnit 5 and how they can help you write better tests and catch errors earlier in the development process.

1. Better support for parameterized tests

Imagine you have a toy car that can go fast or slow depending on how much you push it. With JUnit 5, you can test the car with different amounts of pushing, without having to do the same test over and over again.

Here’s an example:

import org.junit.jupiter.api.Assertions.assertEquals 
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource

class ToyCarTest {

@ParameterizedTest
@ValueSource(ints = [1, 2, 3, 4])
fun testToyCarSpeed(pushAmount: Int) {
val car = ToyCar()
car.push(pushAmount)
assertEquals(pushAmount, car.speed)
}
}

In this code, we’re testing a ToyCar object with different amounts of pushing. With the @ParameterizedTest annotation and the @ValueSource parameter, we're able to test the car with different amounts of pushing without having to write the same test over and over again.

2. Improved test execution

When you play with your toys, sometimes they don’t work the way they’re supposed to. JUnit 5 helps us make sure our toys work the way they’re supposed to by checking them with special tools that make sure they’re doing what they’re supposed to do.

Here’s an example:

import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
import java.lang.Thread.sleep

class ToyTest {

@Test
@Timeout(1000)
fun testToy() {
val toy = Toy()
toy.play()
sleep(500)
assertTrue(toy.isWorking)
}
}

In this code, we’re testing a Toy object to make sure it's working properly. We're using the @Timeout annotation to make sure the test doesn't run for too long, and we're using the Assertions class to check that the toy is working properly.

3. Simplified test writing

Writing tests is like telling your parents how to play with your toys. With JUnit 5, it’s easier to tell your parents how to play with your toys by using special words that make it clear what you want them to do.

Here’s an example:

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test

@DisplayName("Toy Car Tests")
class ToyCarTest {

private lateinit var car: ToyCar

@BeforeEach
fun setUp() {
car = ToyCar()
}

@Test
@DisplayName("Test Toy Car Speed")
fun testToyCarSpeed() {
car.push(3)
assertEquals(3, car.speed)
}
}

In this code, we’re testing a ToyCar object to make sure it's working properly. We're using the @DisplayName annotation to give our test a more descriptive name, and we're using the @BeforeEach annotation to set up our ToyCar object before each test. This makes our test code easier to read and understand.

4. Better integration with build tools

JUnit 5 has improved integration with build tools, making it easier to run and manage tests as part of your build process.

This means that you can catch errors earlier and more efficiently, without having to manually run tests every time you make a change to your code.

By adding JUnit 5 dependencies to your Gradle build file, you can easily incorporate JUnit 5 tests into your build process and take advantage of its benefits.

dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.7.2")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.2")
}

In this code, we’re adding JUnit 5 dependencies to our Gradle build file. This allows us to run our JUnit 5 tests as part of our build process, making it easier to catch errors early on.

5. Better support for dynamic tests

Dynamic tests are a powerful feature of JUnit 5 that allow you to generate tests at runtime, based on specific conditions or criteria.

This means that you can write a single test method that can generate multiple tests based on the input data or conditions.

This feature is particularly useful when you’re dealing with a large number of similar objects or conditions, as it allows you to generate tests dynamically and efficiently.

JUnit 5 provides built-in support for dynamic tests through the @TestFactory annotation and the dynamicTest function, making it easy to generate dynamic tests in your test classes.

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DynamicTest.dynamicTest
import org.junit.jupiter.api.TestFactory

class ToyTest {

@TestFactory
fun testToys(): List<DynamicTest> {
val factory = ToyFactory()
return factory.toys.map { toy ->
dynamicTest("Test Toy ${toy.name}") {
assertEquals(toy.name, toy.name)
}
}
}
}

In this code, we’re testing a number of Toy objects by generating dynamic tests at runtime. We're using the @TestFactory annotation to generate a list of dynamic tests, and the dynamicTest function to create each test dynamically. This allows us to test multiple Toy objects with a single test class, without having to write a separate test for each object.

Conclusion

Overall, updating to JUnit 5 provides significant benefits in terms of writing and executing tests, and makes the testing process much more efficient and effective. By taking advantage of these benefits, developers can ensure that their code is reliable and well-tested, leading to a better overall user experience.

If you still using JUnit3 and don’t know for sure how to migrate to JUnit5, I share my knowledge in my previous article.

Happy coding!

--

--

Daniel Beleza

I’m an Android developer geek, with a great passion of looking for better ways to write good code and design great apps for everyone!