Vert.x And Callback Hell

One thing which some developers may not find appealing in Vert.x is the use of callbacks. Specifically when you have all sorts of nested Lambda/Closure blocks in your code, it can make your code harder to understand and much harder to test. In this post, I will demonstrate a way of writing your code so that the impact of callback hell is minimized and testing options are improved.

Let's look at an example of a simple Vert.x Web Verticle:


import io.vertx.core.json.JsonObject;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.AbstractVerticle;

public class REST extends AbstractVerticle {

    public void start() {
        Router router = Router.router(vertx);
        router.get("/v1/customer/:id").handler(ctx -> {
            vertx.eventBus().send("some.processor", ctx.params(), reply -> {
                // How do we test the logic inside of this Lambda?!?!
            });
        });
    }
}

How would you be able to perform an isolated test of the logic contained in either of the lambdas? Well, the simple answer is that you can't. You could do an integration test, but in many cases that is overkill and also won't let you manipulate the code so that you can cover all cases. The good news is that there is an answer, and it works in all of the Vert.x language implementations I have used. Let's modify the example above and see if we can make it easier to read AND easier to test:


import io.vertx.core.json.JsonObject;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.AbstractVerticle;

public class REST extends AbstractVerticle {

    public void start() {
        Router router = Router.router(vertx);
        router.get("/v1/customer/:id").handler(this::handleCustomerById);
    }

    void handleCustomerById(final RoutingContext ctx) {
        vertx.eventBus().send("some.processor", ctx.params(), reply -> { handleCustomerReply(ctx, reply); });
    }

    void handleCustomerReply(RoutingContext ctx, AsyncResult<Message<JSONObject>> reply) {
        if (reply.succeeded()) {
            ctx.response().setStatusCode(200).setStatusMessage("OK").end(reply.result().encodePrettily());
        } else {
            ctx.response().setStatusCode(404).setStatusMessage("NOT FOUND").end();
        }
    }
}

By splitting out the logic into separate methods, we can now easily inject Mock objects into the methods for testing. Additionally, we have increased readability by giving those methods meaningful names. This opens up a lot of options for unit and integration testing while improving maintainability. You'll also note that I left off the public/private modifiers on the methods so that they can be easily tested using test suites using the same package name.

Overall, this is a quick and simple way to overcome a lot of frustration experienced by developers who deal with callback hell, so I hope that you can leverage this option in your code in the near future!

Comments

Carlos Rijo said…
Thank you for this article.
It helped me in my thesis development.

Thank you sir.
Cheers

Popular Posts