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 `@Rule`s and `@Runner`s 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:
@Test void apiResponsesWithOkStatus() { given() .port(8080) .baseUri("http://example.com/") .basePath("/api") .get() .then() .statusCode(200); }
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:
public class RestAssuredExtension implements BeforeAllCallback { @Override public void beforeAll(ExtensionContext context) throws Exception { RestAssured.port = 8080; RestAssured.baseURI = "http://example.com"; RestAssured.rootPath = "/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:
@ExtendWith(RestAssuredExtension.class) class RestAssuredIntegrationTest { @Test void apiResponsesWithOkStatus() { get() .then() .statusCode(200); } }
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`:
public class RestAssuredExtension implements BeforeAllCallback, AfterAllCallback { @Override public void beforeAll(ExtensionContext context) throws Exception { RestAssured.port = 8080; RestAssured.baseURI = "http://example.com"; RestAssured.rootPath = "/api"; } @Override public void afterAll(ExtensionContext context) throws Exception { RestAssured.reset(); } }
To improve this extension, even more, a dynamic parameters setting can be added:
public class RestAssuredExtension implements BeforeAllCallback { private static final String PORT_PROPERTY = "port"; private static final String BASE_URI_PROPERTY = "baseUri"; private static final String ROOT_PATH_PROPERTY = "rootPath"; @Override public void beforeAll(ExtensionContext context) throws Exception { Optional<Integer> port = getSystemProperty(PORT_PROPERTY).map(Integer::valueOf); Optional<String> baseUri = getSystemProperty(BASE_URI_PROPERTY); Optional<String> rootPath = getSystemProperty(ROOT_PATH_PROPERTY); RestAssured.port = port.orElse(RestAssured.DEFAULT_PORT); RestAssured.baseURI = baseUri.orElse(RestAssured.DEFAULT_URI); RestAssured.rootPath = rootPath.orElse(RestAssured.DEFAULT_PATH); } @Override public void afterAll(ExtensionContext context) throws Exception { RestAssured.reset(); } private Optional<String> getSystemProperty(String propertyName) { return Optional.ofNullable(System.getProperty(propertyName)); } }
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.