Reaping the Benefits of Junit 5
A version of this article was first published on the Oracle blog.
It’s been a few years since the release of JUnit 5. If you haven’t started using it for your development testing yet, you should. JUnit 5 comes with a multitude of new features and improvements that can save you time and headaches. Let’s take a look at how to get started with JUnit 5 to reap the benefits of the latest technology.
Reasons to Migrate From JUnit 4 to JUnit 5
If you’ve been using JUnit 4 for a while, migrating tests may seem like a daunting task. The good news is that you probably don’t need to convert any tests—JUnit 5 can run JUnit 4 tests using the Vintage library, so you can just start writing new tests with JUnit 5.
Here are four solid reasons to start using JUnit 5:
- JUnit 5 leverages features from Java 8 or later, like lambda functions, making tests more powerful and easier to maintain.
- JUnit 5 has added some very useful new features for describing, organizing, and executing tests. For instance, tests get better display names and can be organized hierarchically.
- JUnit 5 is organized into multiple libraries, so only the features you need are imported into your project. With build systems like Maven and Gradle, including the right libraries is easy.
- JUnit 5 can use more than one extension at a time, which JUnit 4 could not (only one runner could be used at a time). This means you can easily combine the Spring extension with other extensions (like your own custom extension).
How to Migrate to JUnit 5
Switching from JUnit 4 to JUnit 5 is quite simple, even if you have existing JUnit 4 tests. Most organizations don’t need to convert old JUnits to JUnit 5 unless new features are needed.
- Update your libraries and build systems from JUnit 4 to JUnit 5. Be sure to include the JUnit-vintage-engine artifact in your test runtime path to allow your existing tests to execute.
- Start building new tests using the new JUnit 5 constructs.
- (Optional) Convert JUnit 3 and JUnit 4 tests to JUnit 5.
Important Differences Between JUnit 5 and JUnit 4
JUnit 5 tests look mostly the same as JUnit 4, but there’s a few differences you should be aware of.
JUnit 5 uses the new org.JUnit.jupiter package for its annotations and classes. For example, org.JUnit.Test becomes org.JUnit.jupiter.api.Test.
The @Test annotation no longer has parameters; each of these has been moved to a function. For example, to indicate that a test is expected to throw an exception in JUnit 4:
In JUnit 5, this has changed to:
Similarly, timeouts have changed. In JUnit 4, they looked like this:
In JUnit 5, timeouts looks like this:
Here are other annotations that have changed:
- @Before has become @BeforeEach
- @After has become @AfterEach
- @BeforeClass has become @BeforeAll
- @AfterClass has become @AfterAll
- @Ignore has become @Disabled
- @Category has become @Tag
- @Rule and @ClassRule are gone—use @ExtendWith and @RegisterExtension instead
JUnit 5 assertions are now in org.JUnit.jupiter.api.Assertions. Most of the common assertions, like assertEquals() and assertNotNull() look the same as before, but there are a few key differences:
- The error message is now the last argument, for example: assertEquals(“my message”, 1, 2) would be assertEquals(1, 2, “my message”)
- Most assertions now accept a lambda which constructs the error message, which is only called when the assertion fails.
- assertTimeout() and assertTimeoutPreemptively() have replaced the @Timeout annotation (note that there is a @Timeout annotation in JUnit 5, but it works differently than JUnit 4).
- There are several new assertions, described below.
Note that you can continue to use assertions from JUnit 4 in a JUnit 5 test if you prefer.
Assumptions have been moved to org.JUnit.jupiter.api.Assumptions.
The same assumptions exist, but now support BooleanSupplier as well as Hamcrest matchers to match conditions. Lambdas can be used (of type Executable) for code to execute when the condition is met.
Here’s an example in JUnit 4:
In JUnit 5, it becomes this:
In JUnit 4, customizing the framework generally meant using a @RunWith annotation to specify a custom runner. Using multiple runners was problematic, and usually required chaining or using a @Rule. This has been simplified and improved in JUnit 5 using extensions.
For example, building tests with the Spring framework looked like this in JUnit 4:
With JUnit 5, you include the Spring Extension instead:
The @ExtendWith annotation is repeatable, meaning that multiple extensions can be combined easily.
You can also define our own custom extensions easily by creating a class which implements one or more interface from org.JUnit.jupiter.api.extension, and then adding it to our test with @ExtendWith.
Converting Tests to JUnit 5
To convert an existing JUnit 3 or JUnit 4 test to JUnit 5, the following steps should work for most tests:
- Update imports to remove JUnit 4 and add JUnit 5. For instance, update the package name for the @Test annotation, and both the package and class name for assertions (from Asserts to Assertions). Don’t worry yet if there are compilation errors, completing the following steps should resolve them.
- Globally replace old annotations and class names with new ones. For example, replace all @Before with @BeforeEach, and all Asserts with Assertions.
- Update assertions. Any assertions that provide a message need to have the message argument moved to the end. Pay special attention when all three arguments are strings! Also, update timeouts and expected exceptions (see above for examples).
- Update assumptions if you’re using them.
- Replace any instances of @RunWith, @Rule, or @ClassRule with the appropriate @ExtendWith annotations. You may need to find updated documentation online for the extensions you’re using for examples.
Note that migrating parameterized tests will require a little more refactoring, especially if you have been using JUnit 4 Parameterized (the format of JUnit 5 parameterized tests is much closer to JUnitParams).
So far, I’ve discussed only existing functionality and how it has changed. But JUnit 5 offers plenty of new features to make our tests more descriptive and maintainable.
With JUnit 5, you can add the @DisplayName annotation to classes and methods. The name is used when generating reports, which makes it easier to describe the purpose of tests as well as tracking down failures, for example:
You can also use a display name generator to process your test class and/or method to generate test names in any format you like. See the JUnit documentation for specifics and examples.
JUnit 5 introduced some new assertions, such as:
- assertIterableEquals() performs a deep verification of two iterables, using equals()
- assertLinesMatch() verifies that two lists of strings match; it accepts regular expressions in the “expected” argument.
- assertAll() groups multiple assertions together. The added benefit is that all assertions are performed, even if individual assertions fail.
- assertThrows() and assertDoesNotThrow() have replaced the expected property in the @Test annotation
Test suites in JUnit 4 were useful, but Nested tests in JUnit 5 are easier to set up and maintain, and they better describe the relationships between test groups, for example:
In the example above, you can see that I use a single class for all tests related to MyClass. I can verify that the class is instantiable in the outer test class, and I use a nested inner class for all tests where MyClass is instantiated and initialized. The @BeforeEach method only applies to tests in the nested class.
The @DisplayNames annotations for the tests and classes indicate both the purpose and organization of tests. This helps to understand the test report because you can see the conditions under which the test is performed (Verify MyClass with initialization) and what the test is verifying (myMethod returns true). This is a good test design pattern for JUnit 5.
Test parameterization existed in JUnit 4 with built-in libraries like JUnit4Parameterized or third-party libraries like JUnitParams. In JUnit 5, parameterized tests are completely built in and adopt some of the best features from JUnit4Parameterized and JUnitParams, for example:
The format looks like JUnitParams, where parameters are passed to the test method directly. Note that the values to test with can come from several different sources. Here, I just have a single parameter so it’s easy to use a @ValueSource. @EmptySource and @NullSource to indicate that I want to add an empty string and a null to the list of values to run with, respectively (and you can combine them, as above, if using both). There are multiple other value sources, such as @EnumSource and @ArgumentsSource (a custom value provider). If you need more than one parameter, you can also use @MethodSource or @CsvSource. See the JUnit 5 documentation for more details and examples.
Another test type added in JUnit 5 is @RepeatedTest, where a single test is repeated a specified number of times.
Conditional Test Execution
JUnit 5 provides the ExecutionCondition extension API to enable or disable a test or container (test class) conditionally. This is like using @Disabled on a test but it can define custom conditions. There are multiple built-in conditions, such as:
- @EnabledOnOs and @DisabledOnOs: Enables a test only on specified operating systems.
- @EnabledOnJre and @DisabledOnJre: Specifies the test should be enabled or disabled for specific versions of Java.
- @EnabledIfSystemProperty: Enables a test based on the value of a JVM system property.
- @EnabledIf: Uses scripted logic to enable a test if scripted conditions are met.
Test templates are not regular tests. They define a set of steps to perform, which can then be executed elsewhere using a specific invocation context. This means that you can define a test template once, then build a list of invocation contexts at runtime to run that test with. Find more details and examples in the Junit 5 documentation.
Dynamic tests are like test templates—the tests to run are generated at runtime. However, while test templates are defined with a specific set of steps and run multiple times, dynamic tests use the same invocation context but can execute different logic. One use for dynamic tests would be to stream a list of abstract objects and perform a separate set of assertions for each based on their concrete types. For good examples, see the Junit 5 documentation.
JUnit 5 is a powerful and flexible update to the JUnit framework. It provides a variety of improvements and new features to organize and describe test cases, as well as help in understanding test results. Updating to JUnit 5 is quick and easy—just update your project dependencies and start using the new features.
Brian McGlauflin is a software engineer at Parasoft with experience in full stack development using Spring and Android, API testing, and service virtualization. He is currently focused on automated software testing for Java applications with Parasoft Jtest.