Eclipse Vertx is perfectly asynchronous framework, so testing its components require a bit more work, than with “old school” synchronous applications. The main actor here is verticle, that is defined as independent unit of computation. In other words, verticles encapsulate some functionality and provides an access to it using async API. For instance, to communicate with a verticle we can utilize various messaging patterns. That means, that to validate verticle’s work, we need to check how it can consume incoming messages and provide a valid output via the eventbus component.

But, this is not a rocket science. To enable Vertx-specific testing features in “normal” JUnit 5 unit tests, developers can use vertx-junit5 extension. In order to use it, you need to add the library to your dependency management system.

In case of Maven, add following entries to pom.xml:

<dependencies>
    <!-- ...other dependencies -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.6.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.6.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-junit5</artifactId>
        <version>3.8.5</version>
        <scope>test</scope>
    </dependency>
</dependencies>

For Gradle users, add to build.gradle:

dependencies {
    // ...other dependencies
    implementation 'io.vertx:vertx-junit5:3.8.5'
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.6.0'
}

Both code snippets register JUnit-Jupiter library and vertx-junit5 extension. If you want to use custom assertions library, like AssertJ, you can also combine it with Vertx-junit5.

The complete source code for this post is available in this Github repository.

Test structure

Vertx unit tests generally are not different from “ordinary” JUnit5 tests, although to enable these async testing features you need:

  1. Extend test with @ExtendWith(VertxExtension.class) annotation
  2. Inject Vertx and VertxTestContext instances to test cases.

The code snippet below presents a basic test structure for Vertx unit test:

//extension
@ExtendWith(VertxExtension.class)
class SampleTest{

    //execute before each test
    @BeforeEach
    void before(Vertx vertx, VertxTestContext context){
        System.out.println("Before...");
        vertx.deployVerticle(new SampleVerticle(), result -> {
            if (result.succeeded()) {
                context.completeNow();
            } else {
                context.failNow();
            }
        });
    }

    @Test
    void test(Vertx vertx, VertxTestContext context){
        System.out.println("Test...");
        //assertions...
    }

    //execute after each test
    @AfterEach
    void after(Vertx vertx, VertxTestContext context){
        System.out.println("After...");
    }
}

Note, that here we use lifecycle methods to deploy a verticle and to clean up context after test will fire. To get more about actual scope of these Vertx components, check the next section.

Instances scopes

This section explores lifecycle scopes of Vertx and VertxTestContext instances, that Vertx-junit5 extension injects to unit tests.

Vertx

When you use setup methods (@BeforeAll/@BeforeEach) to create a Vertx instance (for example, to deploy verticles), you need to remember these rules:

  • Vertx object created in @BeforeAll-annotated method is used in all test methods
  • Vertx object created in @BeforeEach-annotated method is used for single test case, until @AfterEach is fired

Also, when Vertx object is created in a test method (@Test), it is used only in that test case.

VertxTestContext

Vertx-junit5 creates a new instance of VertxTestContext object for each method, no matter @Before..., @Test or After....

Test context methods

This section presents several methods offered by VertxTestContext class.

Assertion methods

This group contains methods, that validate result of async computations. VertxTestContext provides following methods:

  • verify = this method allows verifications and assertions of supplied code block
  • assertComplete = this method validates that async result is completed
  • assertFailure = this method checks that async result is failed

Take a look on the code snippet below, which demonstates use cases of mentioned methods:

@Test
void findOnePersonSuccessTest(Vertx vertx, VertxTestContext context){
    EventBus eventBus = vertx.eventBus();
    Person mockPerson = new Person("John", "Doe", 28);
    String id = "id";
    JsonObject payload = new JsonObject().put("id", id);
    eventBus.request(Routes.DATA_FIND_ONE, payload, result ->{
        if (result.succeeded()){
            JsonObject reply = JsonObject.mapFrom(result.result().body());
            context.verify(() -> {
                assertAll(
                    () -> assertEquals("John", reply.getString("firstName")),
                    () -> assertEquals("Doe", reply.getString("lastName")),
                    () -> assertEquals(28, reply.getInteger("age"))
                );
            });
            context.completeNow();
        } else {
            context.failNow(result.cause());
        }
    });
}

Complete methods

An another group of context methods that provides functionality to finish test execution. The result can be successful or failed:

  • completeNow = competes test context and makes test pass successfuly
  • failNow = completes test context and makes test fail

Following examples demonstrates how to utilize these methods:

@BeforeEach
void setup (Vertx vertx, VertxTestContext context){
    vertx.deployVerticle(verticle, result -> {
        if (result.succeeded()){
            context.completeNow();
        } else {
            context.failNow(result.cause());
        }
    });
}

Methods for async result handling

The third group of methods is used to create async result handlers, which expect a certain result of execution (success or failure), and optionally allow to pass the result to an another callback.

  • completing creates an async handler which expects a success result and after it completes the test context
  • succeeding also creates an async handler which expects a success result, but requires to call completeNow to finish test context successfuly
  • failing creates an async handler which expects a failure result

Take a look on the code snippet below:

@Test
void succeedingTest(Vertx vertx, VertxTestContext context){
    context.succeeding(res->context.verify(()->{
        Assertions.assertTrue(true);
        context.completeNow();
    }));
}

@Test
void failingTest(Vertx vertx, VertxTestContext context){
    context.failing(res->context.verify(()->{
        Assertions.assertEquals(5,4);
        context.completeNow();
    }));
}

Add checkpoints to your tests

Vertx framework brings the concept of checkpoints. Checkpoint represents a test completion stage, and flagging it advances towards the test context completion. When all checkpoints have been flagged, then VertxTestContext makes test pass. For instance, you can flag several steps: creation of server, request sending, response handling etc.

That means, if you want to verify that your code passes a series of required steps, you can add checkpoints for each of the step in your test. This code snippet demonstates how to use checkpoints:

@Test
void doTest(Vertx vertx, VertxTestContext context) throws Exception{
    Checkpoint serverCheckpoint = context.checkpoint();

    /* pass 5 as argument to flag checkpoints 5 times */
    Checkpoint requestCheckpoint = context.checkpoint(5);
    Checkpoint responseCheckpoint = context.checkpoint(5);

    //create server
    vertx.createHttpServer().requestHandler(req->{
        req.response().end("Hello server");
        //send response
        responseCheckpoint.flag();
    }).listen(4567, res->{
        if (res.succeeded()){
            //server created
            serverCheckpoint.flag();
        } else {
            //something wrong
            context.failNow(res.cause());
        }
    });

    //send request

    //send 5 times
    HttpClient client = HttpClient.newHttpClient();
    HttpRequest request = HttpRequest.newBuilder(URI.create("http://localhost:4567/")).build();
    for (int i=0; i<5; i++){
        var res = client.send(request, HttpResponse.BodyHandlers.ofString());
        String body = res.body();
        if (body.equalsIgnoreCase("Hello server")){
            requestCheckpoint.flag();
        } else {
            context.failNow(new Exception());
        }
    }
}

Hope that this post helped you to jump into writing unit tests for reactive Vertx components. The example code for this article can be accessed in this github repository. If you have questions regarding Vertx testing – don’t hesitate to ask them in comments below or contact me.