Oscar Funes

Published on

Using AVA with hapi.js

Recently, hapi is the framework of choice for the projects I’m working on at my job. I also prefer AVA over other test runners, so I’ve been using them together, and these are some of my notes. You can check my test repository here.

Setting up AVA

AVA has a published module that works with npm init command if you have already created a module/app and have a package.json you can do:

$ npm init ava

This command will set up a test script within the scripts property in the package.json of your app or module.

Usually running ava command is enough, because it will look for different patterns of files across your folder and try to execute the files it identifies as tests.

Writing a test

You can start writing your test by adding a test.js file at your root, something like:

my-module/
|- routes.js
|- server.js
|- test.js

We can start testing, by requiring AVA in our test file, like this:

const test = require('ava')

test('my initial test', (t) => {
  t.pass()
})

Testing a route with server.inject

For testing a route, I usually go to using server.inject method, from Hapi server, often provides a good enough way for me to check my configured routes.

If I created a route like this, in my routes.js file:

'use strict'

module.exports = [
  {
    method: 'GET',
    path: '/',
    async handler(request, h) {
      return 'ok'
    },
  },
]

In this case, we can require routes.js directly on the test file and do a server.inject to simulate a request to that endpoint.

'use strict'

const test = require('ava')
const Hapi = require('@hapi/hapi')

const routes = require('./routes')

test('path / should return ok on GET', async (t) => {
  const server = Hapi.server()

  server.route(routes)

  const { statusCode, payload } = await server.inject('/')

  t.is(payload, 'ok')
  t.is(statusCode, 200)
})

Testing an initialized but not started a server

This is a case where you want, for example, an initialized cache or onPreStart extension. But the server listener not started yet. For example, you are setting an extension in the onPreStart event.

First, let’s see what happens if we don’t initialize the server.

test('initialize server to have cache configured', async (t) => {
  const server = Hapi.server()

  server.ext({
    type: 'onPreStart',
    async method(s) {
      server.app.value = 1
    },
  })

  t.is(server.app.value, 1)
})

The above test will fail with server.app.value being undefined. To fix this, we can execute server.initialize() which will run the method we setup.

Let’s try again:

test('initialize server to have cache configured', async (t) => {
  const server = Hapi.server()

  server.ext({
    type: 'onPreStart',
    async method() {
      server.app.value = 1
    },
  })

  await server.initialize()

  t.is(server.app.value, 1)
})

Test against a running server

Let’s say that instead of using server.inject we want to run a request against a running server, we can use the initial test we setup, but instead, use server.start() and then make a request; in my example, we’ll use the Wreck module.

test('route / should return ok on GET request', async (t) => {
  const server = Hapi.server()

  server.route(routes)

  await server.start()

  const { res, payload } = await Wreck.get(`${server.info.uri}/`)

  t.is(payload.toString(), 'ok')
  t.is(res.statusCode, 200)
})

In this case, I’m using server.info.uri property because I’m not setting a port when creating the server, then Hapi will choose a random port to initialize the server listener.

This has been useful to me when making multipart requests, or integrating a server with all the plugins that I want to set up.

Testing events

Let’s say we set up a server and decide to listen to an event; we would like to test that the handler receives the message. Usually, with event emitters, I’ll use the callback method from AVA, meaning that the test would end when the listener is called.

test.cb('listen to custom event', (t) => {
  const server = Hapi.server()

  server.event('custom')

  server.events.on('custom', (message) => {
    t.is(message, 'ok')
    t.end()
  })

  server.events.emit('custom', 'ok')
})

A few things to note here, Hapi requires to register an event, through server.event before you can emit or listen to it. This has helped me prevent typos or debugging before making the mistake of trying to emit or listen to an event that doesn’t exist.

So, Hapi doesn’t use Node.js Event Emitters; it uses it’s own implementation called Podium. This is the module that helps with handling the rules, like registering an event before listening or emitting. One feature I like specially is the ability to await the emit operation.

test('listen to custom event', async (t) => {
  const server = Hapi.server()

  server.event('custom')

  server.events.on('custom', (message) => {
    t.is(message, 'ok')
  })

  await server.events.emit('custom', 'ok')
})

What the await will do, is that emit is a promise that will wait for all listeners to have received the event before resolving the promise. That way, we can guarantee the listener we setup has been executed. We could check this by adding a t.plan(1) at the beginning of the test. If you first wanted to see it fail, you could set up t.plan(0) .

test('listen to custom event await', async (t) => {
  t.plan(1) // change to 0 to see it fail first

  const server = Hapi.server()

  server.event('custom')

  server.events.on('custom', (message) => {
    t.is(message, 'ok')
  })

  await server.events.emit('custom', 'ok')
})

Conclusion

I’ve barely scratched the surface with both Hapi and AVA, but wanted to write down some of the more common use cases I’ve seen as of lately.

I’ve thought about using t.snapshot for payload validation, either in inject or against a started server.

I would recommend further reading both Hapi and AVA documentation for further interesting topics, like AVA recipes. I specially like the idea of a setup function, which I think I will cover later when I go further into why I like AVA so much!

Happy Coding!