One of the challenges of using a remote API is not having a local instance to write tests against. However, when you're testing you do not want to test the database or the APIs that your code relies on. The goal should be to test your code, because...

  • The network is unreliable; poor connections will cause tests to fail
  • Sending data over the net is slower than sending data locally
  • Deterministic testing is awesome

In March, we showed you how to test HTTP APIs using Python and VCR, but this time I want to show off using Mocha and Nock to automate API testing in Node.js projects.

Mocha

Mocha is a testing framework for Node.js and JavaScript projects. Using it often looks like this:

describe('your project', function () {
  before(function (done) {
    doSomethingAsynchronous(done);
  });

  beforeEach(function () {
    // all your tests can now use this value
    this.name = aRandomName();
  });

  it('should do great things in life and love', function (done) {
    // the `done` callback's first argument is considered an error
    // so the test will fail if `done` receives a truthy first argument
    testSomethingAsynchronous()
    .then(function (data) {
      done();
    })
    .fail(function (err) {
      done(err);
    });
  });
});

What does testing that look like? Say you ran mocha -R nyan; this is what you'd see:

 1   -_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-__,------,
 0   -_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-__|  /\_/\
 0   -_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_~|_( ^ .^)
     -_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_ ""  ""

  1 passing (141ms)


Yep. That's a nyan cat, passing your tests. Mocha comes with a bunch of other reporters, including several for generating code coverage reports. Check out Mocha's docs for more.

Nock

Nock is an HTTP mocking library that can record HTTP calls and play them back, so that you can either mock an API by hand, or just record actual HTTP calls and use their responses in future test runs. I'll show you how to do the latter.

Nock works by intercepting any HTTP calls your application makes and recording their parameters. You can then record those parameters so future HTTP calls get the recorded responses, rather than making an HTTP request to an external service. This makes your tests fast and deterministic.

To start recording, just put this in your code:

var nock = require('nock');

nock.recorder.rec({
  dont_print: true
});

Then, to work with the recorded responses, do this:

var fixtures = nock.recorder.play();

By default, Nock records JavaScript code snippets, like this:

nock('http://api.geonames.org:80')
  .get('/postalCodeLookupJSON?postalcode=97202&username=demo&country=US')
  .reply(200, {"postalcodes":[{"adminCode2":"051","adminCode1":"OR","adminName2":"Multnomah County","lng":-122.636534,"countryCode":"US","postalcode":"97202","adminName1":"Oregon","placeName":"Portland","lat":45.484007}]}, { date: 'Wed, 07 Dec 2016 22:30:53 GMT',
  server: 'Apache',
  'cache-control': 'no-cache',
  'access-control-allow-origin': '*',
  connection: 'close',
  'transfer-encoding': 'chunked',
  'content-type': 'application/json;charset=UTF-8' });

Let's see this in action.

Geolocation with GeoNames

GeoNames offers a free API for various geolocation operations, such as looking up latitude and longitude for a zip code or finding local weather stations. Sign up for a free account at their home page and we will use their public RESTful API to run tests using Mocha and Nock.

Consider an application where you wanted to provide location-based services for a user. You might want to look up the user's geolocation based on a zip code. However, for testing purposes, remote API calls can be slow.

We're going to look up a sequence of zip codes to determine their latitude and longitude. Then, we'll record those HTTP calls, save them, and see how much faster our tests run. I'll be using this code. To follow along, you'll need Node.js installed. Then, just replace "your.geonames.username" with the username for your GeoNames account in the following list of commands:

git clone https://github.com/pymander/mocha_nock_demo.git
cd mocha_nock_demo
npm install
export GEONAMES_USER=your.geonames.username
npm test
# tests take a long time
npm test
# tests complete almost instantly

How long did it take? Here's my console output:

  lookupLocation
    ✓ look up location for zip codes from 97202 (9360ms)


  1 passing (9s)

It took about 9 seconds. How about with recorded responses?

  lookupLocation
    ✓ look up location for zip codes from 97202


  1 passing (49ms)

49 milliseconds. Dang!

So, under the hood, what's happening? All the magic is in test/record.js:

var nock = require('nock');
var path = require('path');
var fs = require('fs');

module.exports = function (name, options) {
  // options tell us where to store our fixtures
  options = options || {};
  var test_folder = options.test_folder || 'test';
  var fixtures_folder = options.fixtures_folder || 'fixtures';
  var fp = path.join(test_folder, fixtures_folder, name + '.js');
  // `has_fixtures` indicates whether the test has fixtures we should read,
  // or doesn't, so we should record and save them.
  // the environment variable `NOCK_RECORD` can be used to force a new recording.
  var has_fixtures = !!process.env.NOCK_RECORD;

  return {
    // starts recording, or ensure the fixtures exist
    before: function () {
      if (!has_fixtures) try {
        require('../' + fp);
        has_fixtures = true;
      } catch (e) {
        nock.recorder.rec({
          dont_print: true
        });
      } else {
        has_fixtures = false;
        nock.recorder.rec({
          dont_print: true
        });
      }
    },
    // saves our recording if fixtures didn't already exist
    after: function (done) {
      if (!has_fixtures) {
        var fixtures = nock.recorder.play();
        var text = "var nock = require('nock');\n" + has_fixtures.join('\n');
        fs.writeFile(fp, text, done);
      } else {
        done();
      }
    }
  }
};

record.before checks whether you have fixtures already that should be read, or whether to record and save any HTTP calls made during tests.

record.after writes any recorded HTTP calls to disk if you don't already have fixtures.

record.js gives us a recorder object that we use to record HTTP calls, and then saves them for later use. We'll then use it in our mocha tests like this:

var record = require('./record');

describe('your project', function () {
  var recorder = record('your_project');
  before(recorder.before);

  // ... all your tests

  after(recorder.after);
});

Voila! Our HTTP calls are saved to disk. Now the next time we run npm test, our tests will use the recordings. If you want to force a re-recording, say if your tests change, just do NOCK_RECORD=1 npm test.

You can use record.js in your own projects to your heart's content.