DEV Community

Oleg Agafonov for SIP3

Posted on • Updated on

How to write beautiful unit tests in Vert.x

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())
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Obviously this code won't be working because:

  1. There is no consumer at the moment we've sent the message;
  2. 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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Not like in our previous example, this code will be working fine, but:

  1. Assertion on the context object brings too much overhead;
  2. 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()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
                    }
                }
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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...

Top comments (0)