JUnit 5 brings a lot of new, interesting features into Java developer toolbox. From my perspective, one of the most important is new Extensions API, which allows a user to add custom behavior into the test case, similar to @Rules and @Runners in JUnit 4, but in more concise and elegant way.
In order to create own extension only two steps are required:
Create class that implements Extension interface, like callback interfaces
Register new extension in your test
All of this is very easy to implement, yet powerful in possibilities it offers.
NOTE: In this article I am going to mostly focus on practical usage of Extensions API. To read more about theory and how it works underneath follow this link.
One of the practical usage of Extensions API is to move common code, like configuration, to a single place. For example, in one of the projects I participated, there was a need for integration with Rest API that had only a contract defined – no implementation existed before we started writing a code. Because experience shows, that integration with third-party code is usually problematic, we wanted to be as prepared as possible, so we decided to create a separate project with integration tests that will check if provided API fulfils contract we agreed to. We used Rest Assured for writing these tests, which is a great tool for testing REST endpoints.
With Rest Assured we faced a challenge – it doesn’t provide easy mechanism to configure its settings globally, so configuration needs to be duplicated in every test:
Notice port, baseUri and basePath settings. This part had to be duplicated in every test case.
It was a bit problematic because, we didn’t know then, where the external service will be located, so every configuration update required changes in every test case that was written. But this work could be avoided thanks to Extensions API:
Setting common configuration properties has been moved into single place – Test Lifecycle Callback
Extensions API provides an easy way to register callback methods, and all is done by implementing appropriate interfaces. In the above example BeforeAllCallback interface has been implemented, which ensures that beforeAll method will be called before any test method runs in a test class that uses this extension – similar behavior to @BeforeAll annotation. Thanks to this approach updating default settings or adding new ones (like appending a custom header to every request) can be done by changing a single file, instead of changing every test case like in the previous example.
In previous releases of JUnit moving common code to a single place, was also possible. It was very easy to create base abstract class, and extend it in every test class. But when application grows a lot of common code (configurations of different services, creating and mocking complex objects) could go there, so the base class grew and contained more and more elements unrelated with each other. All of this could result with access to configurations and functions not needed in particular test case. With Extensions API managing common functionalities is much simpler, as programmers declare which Extension they want to register for particular test class, so there is no risk some tests will have access to things they don’t need to.
Extensions can be registered with @ExtendWith annotation on a class level. The test case has been significantly simplified:
Remember that you don’t need to use only one ExtendWith annotation, you can combine as many extensions in a single test class as you want.
Of course, BeforeAllCallback isn’t the only interface Extensions API provides. We can register a callback for every test lifecycle hook. For example, if we want to be sure that our config won’t pollute future test cases, we can reset all settings in AfterAllCallback:
It is now possible to provide port, baseUri and rootPath properties as standard Java System Properties. If a property is not provided then the default one is used. Thanks to this approach we can dynamically configure the location of REST API without any changes in a code itself.
In this article, a simple extension has been created, which separates common test configuration and allows to dynamically set all needed properties. Thanks to this approach, the configuration is managed in one place only and test cases aren’t polluted with information unrelated to test itself – this results in a cleaner and easier to understand tests.
For code samples feel free to explore this repository.