Testing ExpressJS Web Services

Let’s say you have a web application written in NodeJS and you want to test it. What’s the best way to go about that? Fortunately, this is a common enough problem that there are modules and recipes to go along with it.

Separating Express from HTTP

ExpressJS contains syntactic sugar to implement a complete web service. You will commonly see code like this:

var express = require('express');

var app = express();
// Do some other stuff here
app.listen(3000);

Unfortunately, this means that you have to be doing HTTP calls to test the API. That’s a problem because it doesn’t lend itself to easily being tested. Fortunately, there is an easier way. It involves separating the express application from the HTTP logic. First of all, let’s create a web-application.js file. Here is mine:

import bodyParser from 'body-parser';
import compression from 'compression';
import express from 'express';
import logCollector from 'express-winston';
import staticFiles from 'serve-static';

import logger from './lib/logger';
import apiRoute from './routes/api';

/**
 * Create a new web application
 * @param {boolean} [logging=true] - if true, then enable transaction logging
 * @returns {express.Application} an Express Application
 */
export default function webApplication(logging = true) {
    // Create a new web application
    let webApp = express();

    // Add in logging
    if (logging) {
        webApp.use(logCollector.logger({
            winstonInstance: logger,
            colorStatus: true,
            statusLevels: true
        }));
    }

    // Add in request/response middleware
    webApp.use(compression());
    webApp.use(bodyParser.urlencoded({ extended: true }));
    webApp.use(bodyParser.json());

    // Routers - Static Files
    webApp.use(staticFiles('wwwroot', {
        dotfiles: 'ignore',
        etag: true,
        index: 'index.html',
        lastModified: true
    }));

    // Routers - the /api route
    webApp.use('/api', apiRoute);

    // Default Error Logger - should be added after routers and before other error handlers
    webApp.use(logCollector.errorLogger({
        winstonInstance: logger
    }));

    return webApp;
}

Yes, it’s written in ES2015 – I do all my work in ES2015 right now. The export is a function that creates my web application. I’ve got a couple of extra modules – an api route (which is an expressjs router object) and a logging module.

Note that I’ve provided a logging parameter to this function. Setting logging=false turns off the transaction logging. I want transaction logging when I am running this application in production. That same logging gets in the way of the test results display when I am running tests though. As a result, I want a method of turning it off when I am testing.

I also have a http-server.js file that does the HTTP logic in it:

import http from 'http';

import logger from './lib/logger';
import webApp from './web-application';

webApp.set('port', process.env.PORT || 3000);

logger.info('Booting Web Application');
let server = http.createServer(webApp());
server.on('error', (error) => {
    if (error.syscall !== 'listen') {
        throw error;
    }
    if (error.code) {
        logger.error(`Cannot listen for connections (${error.code}): ${error.message}`);
        throw error;
    }
    throw error;
});
server.on('listening', () => {
    let addr = server.address();
    logger.info(`Listening on port ${addr.family}/(${addr.address}):${addr.port}`);
});
server.listen(webApp.get('port'));

This uses the Node.JS HTTP module to create a web server and start listening on a TCP port. This is pretty much the same code that is used by ExpressJS when you call webApp.listen(). Finally, I have a server.js file that registers BabelJS as my ES2015 transpiler and runs the application:

require('babel-register');
require('./src/http-server');

The Web Application Tests

I’ve placed all my source code in the src directory (except for the server.js file, which is in the project root). I’ve got another directory for testing called test. It has a mocha.opts file with the following contents:

--compilers js:babel-register

This automatically compiles all my tests from ES2015 using BabelJS prior to executing the tests. Now, for the web application tests:

/// <reference path="../../typings/mocha/mocha.d.ts"/>
/// <reference path="../../typings/chai/chai.d.ts"/>
import { expect } from 'chai';
import request from 'supertest';

import webApplication from '../src/web-application';

describe('src/web-application.js', () => {
    let webApp = webApplication(false);

    it('should export a get function', () => {
        expect(webApp.get).to.be.a('function');
    });

    it('should export a set function', () => {
        expect(webApp.set).to.be.a('function');
    });

    it('should provide a /api/settings route', (done) => {
        request(webApp)
            .get('/api/settings')
            .expect('Content-Type', /application\/json/)
            .expect(200)
            .end((err) => {
                if (err) {
                    return done(err);
                }
                done();
            });
    });
});

First note that I’m creating the web application by passing the logging parameter of false. This turns off the transaction logging. Set it to true to see what happens when you leave it on. You will be able to see quite quickly that the test results get drowned out by the transaction logging.

My http-server.js file relies on a webApp having a get/set function to store the port setting. As a result, the first thing I do is check to see whether those exist. If I update express and they decide to change the API on me, these tests will point that out.

The real meat is in the third (highlighted) test. This uses supertest – a WebAPI testing facility that pretends to be the HTTP module from Node, listening on a port. You send requests into the webApp using supertest instead of the HTTP module. ExpressJS handles the request and sends the response back to supertest and that allows you to check the response.

There are two parts to the test. The first is the construction of an actual request:

    request(webApp)
        .get('/api/settings')

Supertest uses superagent underneath to actually do the requests. Once you have linked in the ExpressJS application, you can send a GET, POST, DELETE or any other verb. DELETE is a special case because it is a reserved word – use del() instead:

    request(webApp)
        .del('/tables/myTable/1')

You can add custom headers. For example, I do a bunch of work with azure-mobile-apps – I can test that with:

    request(webApp)
        .set('ZUMO-API-VERSION', '2.0.0')
        .get('/tables/myTable/1')

Check out superagent for more examples of the API here.

The second part of the request is the assertions. You can assert on anything – a specific header, status code or body content. For example, you might want to assert on a non-200 response:

   request(webApp).get('/api/settings')
       .expect(200)

You can also expect a body. For example:

    request(webApp).get('/index.html')
        .expect(/<html>/)

Note the use of the regular expression here. That pattern is really common. You can also check for a specific header:

    request(webApp).get('/index.html')
        .expect('X-My-Header', /value/);

Once you have your sequence of tests, you can close out the connection. Since superagent and supertest are asynchronous, you need to handle the test asynchronously. That involves passing in a parameter of ‘done’ and then calling it after the test is over. You pass a callback into the .end() method:

    request(webApp).get('/index.html')
        .expect('X-My-Header', /value/)
        .end((error) => {
            done(error);
        });

Wrapping up

The supertest module, when combined with mocha, allows you to run test suites without spinning up a server and that enables you to increase your test coverage of a web service to almost 100%. With this, I’ll now be able to test my entire API surface automatically.