JUnit 5 offers a foundation for testing frameworks on the JVM as well as a new programming model.
The JUnit testing framework for Java has just moved to version 5. Unlike previous releases, JUnit 5 features modules from several subprojects, including:
Platform, for launching testing frameworks on the JVM and defining the TestEngine API via a command line.
Jupiter, for programming and extension models for writing tests and extensions and then (via plugins) building them within JUnit, Gradle, or Maven.
Vintage, for running JUnit 3 and 4 tests on the JUnit 5 platform.
In Jupiter, a developer can use annotations as meta-annotations, in which you define an annotation that automatically inherits the semantics of meta-annotations—a new programming model in JUnit. Also, Jupiter lets test constructors and methods to have parameters, allowing for more flexibility and enabling dependency injection for constructors and methods.
JUnit 5 requires Java 8 or higher version at runtime. But developers still can test code previous versions of the Java Development Kit. JUnit 5 artifacts do not ship with compiled module descriptors for JDK 9, but there are accommodations for JDK 9. Tests can be run on the java class path; in this regard, there are no changes between Java 8 and 9, according to documentation. Also, running JUnit Jupiter tests on the module path is implemented by pro, a Java 9-compatible build tool.
The top five reasons you should be using JUnit 5 right now.
Sure, you like JUnit. (I mean, who doesn’t?) And you’ve probably heard that JUnit 5 is coming out. But… you’re just not convinced that it’s worth the time to look at it. I mean, not just yet. You’re waiting for it to be released, right?
Well, I say wait no more, and in this post, I’ll give you five reasons you should be using JUnit 5. Like, right now.
Reason #1 – Awesome New Features in JUnit 5
@Nested – this annotation allows you to create nested groups of related test classes within a single test class. This (1) allows your unit tests a more natural style of expression (e.g., Behavior-Driven Testing), (2) nesting enables additional structure that makes testing multiple overloaded methods much easier to write (so long testMyMethod_StringStringInteger_1()), and (3) lets all of the nested tests share expensive overhead at the class level, while still allowing the Before/After (think setUp()/tearDown()) within the nested test.
@Tag – this annotation allows you give tag names to your unit test classes and methods that allow the filtering mechanism to skip a class or test(s) within a class based on these tags (but don’t confuse this with block-level conditional execution provided by Assumptions, which we’ll see next).
Select Assertions and Assumptions work with JDK 8 Lambda support to let you write more compact tests. The new assertAll() method, for example, lets you group assertions, and execute them all of the assertions in a group, and report failures together. Conditional execution of unit tests using Assumptions is nothing new to JUnit, but the Assumptions API has been revamped to make it more expressive.
The new assumingThat() method (part of the Assumptions class) combines the new, more expressive API with lambda expressions to create unit tests that are easier to read, and test reports that are easier to decipher.
Designed to replace the existing extension poins in JUnit 4 (like Runner and @Rule), the JUnit 5 extension model defines a number of points in the unit test lifecycle (ten at the time of this writing!), and a corresponding interface for each one. You use the JUnit 5 extension model to plug in code (called, you guessed it, extensions) that implements one or more of these interfaces, and when that point in the lifecycle is reached your code is executed. This lets you do things like execute code before and after tests, and even control whether or not tests are run at all.
Test method parameter injection
Unless you were using a third party library with a prior version of JUnit, the standard Runner implementations did not allow test methods to have parameters. That changes with JUnit 5. Through the ParameterResolver interface (corresponding to one of the extension points in the test lifecycle), you can write code to resolve parameters when your tests run.
Through the supports() method, your ParameterResolver decides whether or not the specified parameter is supported by your Extension. If not, the framework marches on, but if so, your resolve() method handles creating/accessing/coughing up the object instance that is passed in for the parameter. Very cool indeed.
Reason #2 – New Architecture in JUnit 5
Architecture? Zzzzzzz. Who cares? Well, the architecture of JUnit 5 has a clear separation of concerns between how to discover and run tests, and how to write tests. Gone are the days when tool vendors use JUnit internal APIs to function (which, ironincally, limits JUnit’s ability to move forward).
The JUnit 5 architecture is split up into three projects:
The idea is that you as an application developer don’t really need to worry about how JUnit does its thing. But if you do, the same APIs are exposed (no more “internal” APIs used only by tools developers), providing a consistent approach to extending JUnit.
This means better tool support, and a cleaner API, which enables support for multiple versions of JUnit tests.
Reason #3 – Backward Compatibility for JUnit 4
Upgrading to a new major release of software usually means rewriting EVERYTHING, right? Not with JUnit 5; it is totally backward compatible with JUnit 4. In fact, you can run JUnit 4 (and even JUnit 3) unit tests in the same project as JUnit 5 tests. It takes a little work (hey, nothing is free, right?) but your investment in JUnit 3 and 4 is still good.
How does JUnit 5 achieve this? Through the JUnit Vintage test engine, which allows JUnit 4 and JUnit 3 tests to be run. Not only that, but until JUnit 5 native tool support catches up, you should still be able to use your favorite IDE to run JUnit4 tests with the JUnitPlatform class.
I won’t lie to you, there are some features of JUnit 4 that don’t play so well with JUnit 5 (e.g., Rules), but is migration help available in the JUnit 5 user docs.
Reason #4 – Open Test Alliance
Open… what now? To facilitate an open approach to JUnit tool support, the JUnit Lambda team (the code name of JUnit 5) formed a new open source project called the Open Test Alliance for the JVM.
So far, testing frameworks have one button to push if a test fails: java.lang.AssertionError. Sure, they can all extend that (and do), but each has its own take on it, leading to divergence, and difficulty in integrating those frameworks in with our favorite IDEs. The Open Test Alliance’s framework provides common building blocks for testing frameworks.
Why should you care? Because consistent implementations lead to better tool support. And that’s a cool feature all of us developers can enjoy.
Reason #5 – Choices for your testing framework
As Nicolai Parlog explains it in this post, the entire goal of the JUnit 5 revamp was to allow the success of JUnit to enable the use of other testing frameworks. Because other testing frameworks can build on the JUnit 5 architecture (by providing a test engine), we have a choice of frameworks.