Oleg Agafonov
Posted on April 24, 2020
As you already know from my previous blog post the SIP3 team is in love with the incredible Vert.x framework.
Reactive nature makes Vert.x extremely fast and scalable, but (as everything else) it all comes with a price 👿... And here I'm talking about writing unit tests (😈😈😈 of course if you do write them 😈😈😈)...
Is it really that complicated to write tests with Vert.x?
Let's assume that we want to check a simple thing: that a message sent via event bus was received by its consumer.
In a perfect world we would expect to see something like this (please pay attention to the test structure):
class MyVertxTest {
@Test
fun `Send message to the address`() {
// 1. Define our message
val message = "Hello, world!"
val vertx = Vertx.vertx()
// 2. Send it to the address
vertx.eventBus().send("address", message)
// 3. Retrieve and check the message
vertx.eventBus().consumer<String>("address") { event ->
assertEquals(message, event.body())
}
}
}
Obviously this code won't be working because:
- There is no consumer at the moment we've sent the message;
- Even if there was a consumer
assertEquals()
will be executed not in the context of our test thread.
Of course, the problem is not new and Vert.x team has already resolved it by introducing a TestContext
object (learn more about that from official documentation: here and here).
With the TestContext
and JUnit5 our code will look like this:
@ExtendWith(VertxExtension::class)
class MyVertxTest {
@Test
fun `Send message to the address`() {
// 1. Define our message
val message = "Hello, world!"
val context = VertxTestContext()
val vertx = Vertx.vertx()
// 3. Retrieve and check the message
vertx.eventBus().consumer<String>("address") { event ->
context.verify {
assertEquals(message, event.body())
}
context.completeNow()
}
// 2. Send it to the address
vertx.eventBus().send("address", message)
assertTrue(context.awaitCompletion(5, TimeUnit.SECONDS))
if (context.failed()) {
throw context.causeOfFailure();
}
}
}
Not like in our previous example, this code will be working fine, but:
- Assertion on the
context
object brings too much overhead; - We would love to keep the order - define test context, execute scenario and only after make all the assertions.
Trying to satisfy the last two requirements the SIP3 team decided to introduce its own test class wrapper:
@ExtendWith(VertxExtension::class)
open class VertxTest {
lateinit var context: VertxTestContext
lateinit var vertx: Vertx
fun runTest(deploy: (suspend () -> Unit)? = null, execute: (suspend () -> Unit)? = null,
assert: (suspend () -> Unit)? = null, cleanup: (() -> Unit)? = null, timeout: Long = 10) {
context = VertxTestContext()
vertx = Vertx.vertx()
GlobalScope.launch(vertx.dispatcher()) {
assert?.invoke()
deploy?.invoke()
execute?.invoke()
}
assertTrue(context.awaitCompletion(timeout, TimeUnit.SECONDS))
cleanup?.invoke()
if (context.failed()) {
throw context.causeOfFailure()
}
}
}
We put a context
object assertion within the runTest()
method. Also we used Kotlin named arguments(which is a really cool) to define test stages.
Let's rewrite our test class using VertxTest
:
class MyVertxTest : VertxTest() {
@Test
fun `Send message to the address`() {
// 1. Define our message
val message = "Hello, world!"
runTest(
execute = {
// 2. Send it to the address
vertx.eventBus().send("address", message)
},
assert = {
// 3. Retrieve and check the message
vertx.eventBus().consumer<String>("address") { event ->
context.verify {
assertEquals(message, event.body())
}
context.completeNow()
}
}
)
}
}
Of course, it's a matter of taste but we think that the code above is clean and simple even though there is always room for improvement 😄.
The SIP3 team uses this approach in all it projects. Feel free to check out our github and let us know if you loved it.
Cheers...
Posted on April 24, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.