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:
- When we
POST
to/subscribe
with an empty JSON payload, we expect a400 Bad Request
response. - When we
POST
to/subscribe
with a JSON payload containing an invalid email address, we expect a400 Bad Request
response. - When we
POST
to/subscribe
with a JSON payload containing a valid email address, we expect a200 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.