Thursday, September 3, 2015

Testing the Koa.js Response Context through koa-controller


Today I was test driving some new endpoints for my Node.js REST API.  I decide it was time to add koa-controller so that I could manage routes better and also map them to controller actions.

koa.js

I'm using koa.js for building out our new REST API in Node.js.

koa uses generator functions.  When you create a koa route, like anything, you can grab the request context and set the response context.

koa-controller

koa-controller adds in support for matching routes with controllers and actions.

A koa-controller receives the koa request context and then in your controller action, you can process the context.  And then set the context's response data, etc which koa uses to send back the http response.

With this middleware, I had created an initial action function as so in my controller:

(I apologize for shitty blogger and the lack of formatting below.  I'm working on moving this blog to some other platform)

module.exports = {
    find: function *(){
        var response = this;
        response.statusCode = 200;

        _gateway.find(function(foundData){
            if(!foundData || Object.keys(foundData).length === 0){
                response.body = null;
                response.statusCode = 204;
            }

            response.body = foundData;
        });
    }
};

problem is, how can my test get back the koa context to test whether my controller did what it was supposed to?  Right now, koa-controller passes back the context automatically.  But I wanna catch it for testing in my unit tests.

You can't just do a return response and we want to keep our functions async anyway and use callbacks (ideally promises) along with generators not because "it's cool"...but because it's necessary and also required by koa in the first place to be using generators for this.

Technically I should mock the koa context because I'm not testing koa; I wanna be testing  the logic I wrote in my controller only.  That'll be next.  But I wanted to figure out how I'd get the context back regardless first.

Generator Functions Save the Day

Since my action function has to be a generator function for koa-controller, that means I could use yield to return the context if I'd like.

That also meant making my callback function a generator or else this wasn't gonna work.

So here it is with yield and the callback generator function

module.exports = {
    find: function *(){
        var response = this;
        response.statusCode = 200;

        _gateway.find(function*(foundData){
            if(!foundData || Object.keys(foundData).length === 0){
                response.body = null;
                response.statusCode = 204;
            }

            response.body = foundData;

            yield Object.create(response);
        });
    }
};

notice I simply added * to _gateway.find's callback function.

Now you're probably wondering why I created another object here instead of just doing a yield response; 

Well koa-controller is handling returning the koa context so when I tried that, it was conflicting with koa-controller core code.  

So what I had to do is simply clone the context and return it.  Now it's independent of whatever process was handling the real context prior to this.

My Mocha.js Test

I use mocha.js for my BDD.

So now I can can test the context's values that came back from the controller's find action function:

it('should return no country when no country exists', function(done){
    var countries = {};
    countryMockGateway.setCountries(countries);
    countryController.gateway(countryMockGateway);

    var koaResponseContext = countryController.find().next().value;

    should.not.exist(koaResponseContext.body);
    koaResponseContext.statusCode.should.equal(204);
    done();
});

ES5

With ES5, you can easily clone which was very hard to do prior with simply Object.create().

So the next time your architect tells you "We don't need to be using new stuff like ES6, that's overboard", you can tell him he's full of garbage.  I didn't have an architect who did that where I"m working but I have in the past.  So here's a good use case for testing and using generators and cloning to easily accomplish what would have probably been a tangled mess of code to do the same thing prior to ES5.