This blog originally appeared on strongloop.com. StrongLoop provides tools and services for companies developing APIs in Node. Learn more...

When people build REST APIs with Express, testing and documentation are often an afterthought. The situation with testing your documentation is often even worse. When's the last time you built an automated testing system for your documentation's examples? When I sat down to write acquit, I wanted a simple tool to generate documentation from mocha tests for mongoose. But, the paradigm of generating documentation from tests is even more useful for API-level integration tests. In this article, I'll explain

  • What API-level integration tests are
  • How to use acquit to parse integration tests into documentation

API-Level Integration Tests

You may have seen the testing pyramid before:

The components of the testing pyramid are as described below.

  • Unit tests test individual units of code in isolation (for instance, individual functions). They are fast and you usually have a lot of them.
  • Integration tests test the integrations between different units.
  • E2E tests test the system as a whole.

The concept "integration test" is flexible depending on your application and choice of testing paradigm. In this article, you will be primarily concerned with integration tests that test your REST API as a whole. If your entire product is a REST API then you may consider this an E2E test. But, for the purposes of this article, these tests will be called "API-level integration tests."

How are you going to write API-level integration tests for Express? NodeJS' concurrency model makes writing API-level integration tests simple. The key idea is that your mocha tests can start and stop an Express server. Once your mocha tests start a server, the same process can use an HTTP client (for example, request or superagent) to send requests to your Express server. No need for any messy multithreading.

For this article, you'll be using the acquit-example repo as an example. This repo defines a trivial REST API in the server.js file. There's one route: GET /user/:user returns a user object if the username is in the list, and an HTTP 404 otherwise.

var express = require('express');
var status = require('http-status');
var users = require('./users');

var createServer = function(port) {
  var app = express();

  app.get('/user/:user', function(req, res) {
    if (users.list.indexOf(req.params.user) === -1) {
      return res.status(status.NOT_FOUND).
        json({ error: 'Not Found' });
    }
    res.json({ user: req.params.user });
  });

  return app.listen(port);
};

module.exports = createServer;

http-status defines a map from readable HTTP status names to numeric codes. For instance, NOT_FOUND === 404. This module, while simple, is a module that I'd never write a web server without.

The users.js file included in the above code example is a stub for a real database. In a real application you'd probably want to install MongoDB and Mongoose, but the extra setup would just add extra overhead to this example. The users.js file is shown below.

exports.list = [
  'user1',
  'user2'
];

Nothing too fancy, but, as you'll see, enough to enable tests to manipulate the list of users. Another key advantage of API-level integration tests in NodeJS is that you can re-use the same database interface you already use in your code. In other words, you can use Mongoose or whatever your database interface of choice is to insert data for each test within mocha's elegant flow control. In this case, tests can require() the same users.js file and manipulate the underlying list of users as shown in the test/api-integration.test.js file below.

var assert = require('assert');
var superagent = require('superagent');
var server = require('../server');
var users = require('../users');
var status = require('http-status');

describe('/user', function() {
  var app;

  before(function() {
    app = server(3000);
  });

  after(function() {
    app.close();
  });

  it('returns username if name param is a valid user', function(done) {
    users.list = ['test'];
    superagent.get('http://localhost:3000/user/test').end(function(err, res) {
      assert.ifError(err);
      assert.equal(res.status, status.OK);
      var result = JSON.parse(res.text);
      assert.deepEqual({ user: 'test' }, result);
      done();
    });
  });

  it('returns 404 if user named `params.name` not found', function(done) {
    users.list = ['test'];
    superagent.get('http://localhost:3000/user/notfound').end(function(err, res) {
      assert.ifError(err);
      assert.equal(res.status, status.NOT_FOUND);
      var result = JSON.parse(res.text);
      assert.deepEqual({ error: 'Not Found' }, result);
      done();
    });
  });
});

The above code demonstrates the two key ideas of API-level integration tests. First, since you're using NodeJS, you can create an Express server and then use superagent to send HTTP requests to it. Second, since the server and tests share the same process, you can manipulate the server in whatever way your tests require. You can manipulate the underlying data, shut down the server mid-request, even simulate malicious requests.

Hopefully this example shows you why I think NodeJS is so easy to test. API-level integration tests aren't a replacement for proper unit testing. Unit tests are like broccoli - you may not like writing them, but you should write them anyway if you want your codebase to grow up big and strong. However, API-level integration tests are useful for catching bugs in integrations between modules, great for REST API TDD, and useful for generating documentation. As a matter of fact, documentation is the subject of the next section.

Generating Documentation from Tests

API-level integration tests are a cool concept that might not be obvious to people from Java or C++ backgrounds (myself included), but there's nothing new there. The application of API-level integration tests to documentation, though, is much more novel.

In this section, you'll learn about the acquit module, a lightweight tool I wrote to generate documentation from mocha tests. Acquit started out as a tool to generate docs for Mongoose's browser component: I wanted documentation with examples that I knew would work as advertised in IE9. Dusting off my Windows laptop and hitting F12 repeatedly was not a viable approach. Since then, I've used to generate documentation for numerous npm modules, including mongoose-autopopulate, kareem, and even acquit itself. When connected to API-level integration tests, you have a mechanism to generate well-tested documentation for your REST API.

All the work necessary to integrate acquit for docs generation is in this GitHub commit. The key work is in the docs.js file:

var acquit = require('acquit');

var content = require('fs').
  readFileSync('test/api-integration.test.js').toString();
// Parse the contents of the test file into acquit 'blocks'
var blocks = acquit.parse(content);
var header = require('fs').readFileSync('./header.md').toString();

var mdOutput = header + '\n\n';

// For each describe() block
for (var i = 0; i < blocks.length; ++i) {
  var describe = blocks[i];
  mdOutput += '## ' + describe.contents + '\n\n';
  mdOutput += describe.comments[0] ?
    acquit.trimEachLine(describe.comments[0]) + '\n\n' :
    '';

  // This test file only has it() blocks underneath a
  // describe() block, so just loop through all the
  // it() calls.
  for (var j = 0; j < describe.blocks.length; ++j) {
    var it = describe.blocks[j];
    mdOutput += '#### It ' + it.contents + '\n\n';
    mdOutput += it.comments[0] ?
      acquit.trimEachLine(it.comments[0]) + '\n\n' :
      '';
    mdOutput += '```javascript\n';
    mdOutput += '    ' + it.code + '\n';
    mdOutput += '```\n\n';
  }
}

require('fs').writeFileSync('README.md', mdOutput);

Similar to dox, acquit doesn't limit you to a particular output format. All acquit provides you is the parse() function, which parses your tests into a handy format for documentation generation, and the trimEachLine() helper. The trimEachLine() helper is useful for trimming asterisks from multiline comments.

The parse() function uses the esprima JS parser to parse the mocha tests you saw in the last section into "blocks" that look like what's shown below.

[
  {
    "type": "describe",
    "contents": "/user",
    "blocks": [
      {
        "type": "it",
        "contents": "returns username if name param is a valid user",
        "comments": [
          "*\n   *  In addition to parsing the test contents and code..."
        ],
        "code": "\n    users.list = ['test'];\n..."
      },
      {
        "type": "it",
        "contents": "returns 404 if user named `params.name` not found",
        "comments": [
          "*\n   *  Acquit also has a handy `acquit.trimEachLine()` function..."
        ],
        "code": "\n    users.list = ['test'];\n..."
      }
    ],
    "comments": []
  }
]

You can then use this output to generate markdown (as shown in this example), jade, plain HTML, or whatever your preferred output format is. Running docs.js file with node docs.js will generate a README.md file from your tests that provides some high-level documentation for the example REST API.

Conclusion

Hopefully this article got you excited about generating documentation. With acquit, generating documentation is as easy as writing good tests. With API-level integration tests and acquit, you have no more excuses to avoid documenting your REST API.

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus