Assertions with Vert.x Futures and JUnit5
During development of Vert.x event manager library (a blog post about it is coming soon) I wanted to play with new vertx-junit5
library. I like the new async assertion APIs of vertx-junit5
, but I feel very unconfortable using VertxTestContext.succeding(Handler)
when I need to run sequentially different async tasks. With this method, your code rapidly grows in a big callback hell! Plus the interfaces I wanted to test are all in Future
s style more than callback style.
In this post I’m going to explain you two methods I’ve added with a PR that simplify tests with Future
s
assertComplete()
and assertFailure()
The PR adds methods:
Future<T> assertComplete(Future<T> fut)
Future<T> assertFailure(Future<T> fut)
These methods take a future as parameter and register to it the handler that asserts the completion/failure of it. They return a copy of the future you passed as parameter
For example this callback style assertion:
1 | methodThatReturnsAFuture().setHandler(testContext.succeding(result -> { |
Turns into:
1 | testContext.assertComplete(methodThatReturnsAFuture()).setHandler(asyncResult-> { |
Nothing revolutionary, right? To appreciate it let’s look at a more real use case
Testing a Future chain
Let’s say that we want to test an update method of a class that manage some entities in a database. A common flow for this kind of tests is:
- Use the raw db client to add some data
- Use the class instance you want to test to update data on db
- Retrieve data from db to test if update is successfull
Assuming that both raw db client and entity manager has futurized APIs, without these methods, this test translates in 3 nested callbacks. Now you can simplify it like this:
1 | testContext.assertComplete( |
With just one assertComplete()
we assert that all chain of async operations completes without errors. Then I set an handler that does the final assertions before completing the test
Now, let’s assume that you want to do the same test as before but testing a failure of your method. To do it you need to check every single step of future chain:
1 | testContext.assertComplete(rawClient.create(someData)) |
Tricks and tips
The bad thing of future chains is passing values through the chain. Let’s say that in previous example the exception throwed by update()
method doesn’t return an exception that contains a super handy method like getEntityId()
. But to get the data from db you need the id
of your data instance, so how you can solve it?
You have two ways that really depend on your code style:
-
If you are a bit more functional, use
CompositeFuture.join()
to transform a tuple of Futures (one of them already completed with the value you want to pass through the chain) to a single Future that encapsulates both the previous async operation result and the new result. This method works only when you are in a chain of completed handlers because when a future insideCompositeFuture.join()
fails, the “join future” is not an instance ofCompositeFuture
and doesn’t return any information about other joined futures. I prefer to avoid this method, but keep it in mind because you can find it useful sometimes. -
If you don’t care about functional stuff, just use old but gold
AtomicReference
s:
1 | AtomicReference<String> entityId = new AtomicReference<>(); |
If you have any good tips don’t hesitate to contact me! Happy testing!