JUnit 5 is a rewrite of the famous Java testing framework that brings new interesting features, including:
- nested tests,
- the ability to give a human-readable description of tests and test cases,
- a modular extension mechanism that is more powerful than the JUnit 4 runner mechanism (@RunWith annotation),
- conditional test execution,
- parameterized tests, including from sources such as CSV data,
- the support of Java 8 lambda expressions in the reworked built-in assertions API,
- support for running tests previously written for JUnit 4.
Testing asynchronous operations is not straightforward
Eclipse Vert.x is an increasingly popular toolkit for writing reactive applications on the JVM.
Testing code with asynchronous operations is more challenging than it seems at first sight. Indeed, let us consider the following (incomplete!) test snippet:
This test defines a periodic task every 100ms, and we would like to complete the test when the periodic task callback has been executed 3 times. Because setPeriodic defines an asynchronous operation that is being executed on another thread, the test method returns right after the call to setPeriodic, and the test framework considers the method has having succeeded.
The solution is to make the test framework runner wait until all asynchronous operations have completed successfully or not.
Vert.x already provided a module called vertx-unit for testing asynchronous operations. Its strengths are that it is polyglot, so it works for all the JVM languages that Vert.x supports, and it provides a JUnit 4 runner.
With the advent of JUnit 5 we decided to develop a specific integration for JUnit 5 (vertx-junit5) that works great of course with Java but also with the languages that have seamless interoperability with Java and where using JUnit is popular: Kotlin and Groovy. It is important to note that vertx-unit is not being abandoned, as it remains useful for other JVM languages and for projects using JUnit 4.
An example is better than a thousand words
To create a test with JUnit 5 and Vert.x we simply define a class:
Making test classes package-protected is a common idiom of JUnit 5.
The @ExtendWith annotation allows using the Vert.x extension (more on that in a minute!). In fact, a test can use several extensions, contrarily to the runners in JUnit 4. The @DisplayName annotation is optional, but it gives a human-readable description of what the test is doing. Last but not least, you can use emojis!
Back to our example of counting ticks, here is how we can write it as a method in SampleVerticleTest:
The VertTestContext class provides a context for running Vert.x asynchronous operations, and it is used to define when a test completes or fails. Remember that operations are being executed on other threads than the one of the JUnit runner. The call to completeNow() immediately marks the test as successful.
The Vert.x extension does 2 important things:
- it (optionally) injects instances of Vertx and VertxTestContext when a test method has arguments of these types, and
- it ensures that the JUnit test runner waits for the asynchronous operations to complete even when the test method execution exits.
It is possible to create both Vertx and VertxTestContext instances manually if need be, but they provide sensible defaults for a majority of test cases. It is also important to note that VertxTestContext always waits for the test completion with a timeout so as not to block the tests execution forever. The timeout delay can be customized through an annotation.
Not every asynchronous test execution has a single point of completion: there are many cases where you need to check that several specific lines of code have been executed.
We provide a checkpoint abstraction. When all created checkpoints have been flagged, then a test succeeds.
Back to our previous example, we can rewrite it more simply using a checkpoint that must be flagged 3 times:
An integration test (+ other goodies)
The Vert.x functional unit of event-processing code is called a verticle. In short, a verticle processes asynchronous events and it is managed by an event-loop, which is itself permanently tied to a thread.
Let us consider the following verticle. It starts a HTTP server on port 11981 and answers all requests with the "Yo!" text:
Now let us write an integration test for this verticle. More specifically, we need to ensure that:
- The verticle is successfully deployed in a Vertx context, and
- We need to issue HTTP requests and check the responses.
We will do that and also issue 10 HTTP client requests.
The test fits in the following method:
Many asynchronous operations in Vert.x require a AsyncResult<T> callback, where AsyncResult contains either a value in case of success, or an exception in case of failure. To make the test code easier and avoid a "if / else blocks dance", VertxTestContext provides succeeding and failing helper methods.
Here we use succeeding in 2 places because we expect successes, and we pass a callback that handles the AsyncResult value.
Another important point is that our JUnit 5 support is agnostic of the test assertion library. You may use the built-in JUnit assertions API, or do like in our example and use AssertJ. All you have to do is call verify with a lambda where assertions can be placed. Any exception thrown from inside the lambda makes the test fail with that exception.
A complete example
Here is a complete test example, including JUnit 5 features like life-cycle callbacks and a nested test that shows how to use custom Vertx and VertxTestContext objects:
- The Vert.x website provides a comprehensive description of the Vert.x ecosystem.
- The Vert.x JUnit 5 module provides a complete documentation.
- The Vert.x examples repository contain the full source code for the examples given in this article, and are generally a great resource to explore "all things Vert.x"!