@jackyalciné

Got an app idea or need some coding done? I can help! Learn more about how you & I can work together.

Testing Routes with Hapi

Hapi? :fire:. Testing routes? Not so much.

:book: tutorials :bookmark: hapi, chai, testing :clock7: :clock10: updated :clock3: about 4 minutes

I’m working on a personal project that makes uses of Hapi for its API layer. I’ve managed to go about in a mostly test driven fashion and worked from the bottom up (from data modeling up to the route definition). Whilst working on the ability to test the response states of routes, I noticed a few problems when using the Chai plugin for HTTP testing, chai-http. For completeness, here is what I’m working with1:

// @file app/routes/animal.js
const Hapi = require('hapi')
let myServer = new Hapi.Server()

myServer.route({
  method: 'GET',
  path: '/magic',
  handler: (request, reply) => {
    reply('pokemon')
  }
})

module.exports = myServer
// @file test/routes/animal_test.js
const Server = require('../../app/routes/animal')
const Chai = require('chai')

Chai.use(require('chai-http'))

// TODO: HOW?

That TODO part tripped me up. Checking Hapi’s documentation, I noticed that Hapi exposed a HTTP server object via server.listener. This gave me the idea of doing this:

// @file test/routes/animal_test.js
const Server = require('../../app/routes/animal')
const Chai = require('chai')
const assert = Chai.assert

Chai.use(require('chai-http'))

const mockServer = Chai.request(Server.listener)

mockServer.get('/magic').end(function(err, res) {
  assert.isNull(err)
  assert.statusCode(res, 200)
  assert.equal(res.result, 'pokemon')
})

This should work. But it didn’t :cry:2

⌂ hapi-chai-testing  λ  node --harmony test/routes/animal_test.js

~jacky/.nvm/versions/io.js/v3.0.0/lib/node_modules/chai/lib/chai/assertion.js:107
      throw new AssertionError(msg, {
      ^
AssertionError: expected [Error: socket hang up] to equal null
    at Function.assert.isNull (~jacky/.nvm/versions/io.js/v3.0.0/lib/node_modules/chai/lib/chai/interface/assert.js:314:32)
    at ~jacky/Development/Projects/hapi-chai-testing/test/routes/animal_test.js:12:10
    at Test.Request.callback (~jacky/.nvm/versions/io.js/v3.0.0/lib/node_modules/chai-http/node_modules/superagent/lib/node/index.js:746:30)
    at ClientRequest.<anonymous> (~jacky/.nvm/versions/io.js/v3.0.0/lib/node_modules/chai-http/node_modules/superagent/lib/node/index.js:711:10)
    at emitOne (events.js:77:13)
    at ClientRequest.emit (events.js:169:7)
    at Socket.socketOnEnd (_http_client.js:288:9)
    at emitNone (events.js:72:20)
    at Socket.emit (events.js:166:7)
    at endReadableNT (_stream_readable.js:889:12)

Definitely not what I was looking for. And to be honest, I feel like this requires me to get closer into how this works and I plan to make a follow-up post to do this. In the interim, I’ve resorted to using server.inject to simulate requests:

'use strict'

const Server = require('../../app/routes/animal')
const Chai = require('chai')
const assert = Chai.assert

const reqOpts = {
  method: 'GET',
  url: '/magic'
}

describe('Server', function () {
  it('gives me the work', function () {
    Server.inject(reqOpts, function (res) {
      assert.equal(res.statusCode, 200)
      assert.equal(res.result, 'pokemon')
    })
  })
})

Running this with Mocha gave me this:

⌂ hapi-chai-testing  λ  mocha test/routes/animal_test.js


  Server
    ✓ gives me the work


  1 passing (33ms)

Boom. This kind of sucks. I really like the tools from Hapi and friends but this little barrier of interoperability between libraries and larger projects sucks. Again, since I found a working solution, there’s no heavy need for me to get this working with chai-http at the moment. But if I or anyone else figure this out, I’ll be sure to provide a new blog post.


For compatibility information, this is me testing this on Hapi 9.3.0, but this’ll apply for older versions of Hapi (tested on 8.8.1, the version I’m using in my project).

  1. I make use of ES6 keywords a lot and I don’t use semicolons, we don’t need them. 

  2. For reference, this failed with node 0.12.0 and io.js 3.0.0 as well.