Testing Hapi.js APIs

Published on

Hapi.js is one of my favorite JavaScript server frameworks. One of the main reasons why I use Hapi.js is that it’s very easy to test. Using a framework that’s easy to test is especially important because it saves us time and makes us more inclined to write tests in the first place, allowing us to spot mistakes faster and more consistently.

For JavaScript, I like to use the AVA package for testing, and I’ll be using it throughout this article. We can replace AVA with just about any other testing harness, such as tape, Mocha, or Jasmine, and get similar results.

Writing the Tests

In traditional TDD fashion, we will write our tests before writing the implementation. I am going to skip setup instructions for AVA, so I recommend reading the AVA documentation.

Our API needs an endpoint that allows users to subscribe to our mailing list. The path for the new endpoint is /subscribe, and it expects a POST request with a JSON payload containing one property, email.

With these requirements, we can write at least three test cases:

  1. When we POST to /subscribe with an empty JSON payload, we expect a 400 Bad Request response.
  2. When we POST to /subscribe with a JSON payload containing an invalid email address, we expect a 400 Bad Request response.
  3. When we POST to /subscribe with a JSON payload containing a valid email address, we expect a 200 OK response. (Assuming nothing goes wrong when persisting the email address.)

Now that we have our test cases, let’s write the code for one of them.

// test/subscribe.js

import test from 'ava';
import server from '../server';

const requestDefaults = {
  method: 'POST',
  url: '/subscribe',
  payload: {}
};

test('endpoint test | POST /subscribe | empty payload -> 400 Bad Request', t => {
  const request = Object.assign({}, requestDefaults);

  return server.inject(request)
    .then(response => {
      t.is(response.statusCode, 400, 'status code is 400');
    });
});

To test the endpoint, we need to import our server and call the route. I’ll discuss the details of exporting the server a little later, but let’s focus on server.inject() for a second.

Instances of Hapi.js servers have a server.inject() method that we can use to call a registered route on the server without requiring that the server is listening on open ports. This is great because we can now test the actual server functionality, both the routing and endpoint logic, without having to deal with opening ports which can be a problem sometimes.

AVA, for instance, allows us to run each test file in its own process and all of our tests can run concurrently, which means our tests run very quickly. Because each test file runs in a separate process, a new server instance is created for each. If our server is set up to open a specific port, and it is more often than not, we will run into problems because multiple servers can’t open the same port. server.inject() makes this a non-issue.

Let’s go ahead and write the code for the rest of the test cases.

// test/subscribe.js

// ...

test('endpoint test | POST /subscribe | invalid email address -> 400 Bad Request', t => {
  const request = Object.assign({}, requestDefaults, {
    payload: {
      email: 'a'
    }
  });

  return server.inject(request)
    .then(response => {
      t.is(response.statusCode, 400, 'status code is 400');
    });
});

test('endpoint test | POST /subscribe | valid email address -> 200 OK', t => {
  const request = Object.assign({}, requestDefaults, {
    payload: {
      email: 'test@example.com'
    }
  });

  return server.inject()
    .then(response => {
      t.is(response.statusCode, 200, 'status code is 200');
    });
});

Writing the Server and Endpoint

Now that we have a few tests ready to go, how do we write our server in a testable way? The first thing that we need to do is export our server so that it can be imported and used in our tests. Take a look at the code below.

// server.js

const Hapi = require('hapi');
const server = new Hapi.Server();

server.connection({ port: 8080 });
server.route({
  method: 'POST',
  path: '/subscribe',
  handler: (request, reply) => {
    return reply({
      result: 'success'
    });
  }
});
server.start(error => {
  process.exit(1);
});

module.exports = server;

We have our server set up and exported for our test files. There is still one issue, though. The whole point of using server.inject() is that we wouldn’t need to call server.start() to open ports for the server. In fact, calling server.start() in our code above will cause issues for us if we run multiple test files with AVA for different endpoints because server.start() will be called multiple times. A quick fix for this is not to call server.start() if the server is being required.

if (!module.parent) {
  server.start(error => {
    process.exit(1);
  });
}

Now that we have our tests written and a little bit of the implementation done, let’s run our tests and see what happens.

(Note: I have npm test set to run ava --verbose test/**/*.js.)

Okay, so we can see that our test case that expects a 200 OK response passed, but our other tests failed because right now our endpoint returns a 200 OK no matter what. Let’s make some quick updates to the route handler and run the tests again.

server.route({
  method: 'POST',
  path: '/subscribe',
  handler: (request, reply) => {
    if (!request.payload.email) {
      return reply({
        result: 'failure',
        message: 'Email address is required.'
      }).code(400);
    } else if (!/@/.test(request.payload.email)) {
      return reply({
        result: 'failure',
        message: 'Invalid email address.'
      }).code(400);
    }

    // persist the email address
    // ...

    return reply({
      result: 'success',
      message: 'Subscription successful.'
    });
  }
});

Once we change our route handler to respond appropriately to missing and invalid payloads, our tests pass!

Test Away

We’ve covered the basics of testing Hapi.js APIs and hopefully you can see how simple it can be. Hapi.js makes testing a breeze, which is good because testing is so important. So go forth, and test your APIs!

Complete files for this article can be found in this gist.


Verify the signed markdown version of this article:

curl https://keybase.io/sethlopez/key.asc | gpg --import && \
curl https://sethlopez.me/article/testing-hapi-js-apis/ | gpg