Writing Testable HTTP APIs Using Node.js - The Basics

I remember writing my first test cases - it was everything but nice and clean.Testing done right is not easy.

It is not just about how you write your tests, but also how you design your entire codebase.This post intends to give an insight on how we develop testable HTTP APIs at RisingStack.

All the examples use Joyent's restify framework,and can be found at RisingStack's Github page.

Setting up your test environment

In order to run our test cases, we need a test runner and an assertion library.The test runner will sequentially run each test case, while the assertion librarywill check if the expected value equals the outcome.

But enough of the theory, let's setup our test runner and assertion library!For this post, we will use mocha as our test runner, and chai as our assertion library.

Adding mocha to your project

npm install mocha --save-devmkdir test

It will install mocha and put it as a developmentdependency to your package.json. Then you should put all your test casesunder the test folder.

Also, it is convenient to put it into your package.json's scripts section, so it can berun using the npm test command.

  "scripts": {    "test": "mocha test"  }

This will work without installing mocha globally, as npm will look for node_modules/.bin, andplace it on the PATH.

Adding chai to your project

npm install chai --save-dev

Then using chai it is time to write the first test case, just to demonstratehow mocha and chai plays together.

// test/string.jsvar expect = require('chai').expect;describe('Math', function () {  describe('#max', function () {    it('returns the biggest number from the arguments', function () {      var max = Math.max(1, 2, 10, 3);      expect(max).to.equal(10);    });  });});

The above test can be run with npm test.

Designing your codebase - time for unit tests

Unit tests are the basic building block of tests, where each test case is independent fromothers. Unit tests provide a living documentation of the system and areextremely valuable for design feedback: one looking at your test cases can figure out easily whatthe given unit does, how you engineered it, what interfaces does it expose.

As a side effect, unit tests can verify if your units work correctly.

The magic word here is: TDD, meaning Test-drive development.TDD is the process of writing an initially failing test case, that definesa function - this is where you design the interfaces of your unit.

After that all you have to do is make the tests pass by implementing the describedfunctionality.

When writing unit tests, we do not want to deal with the given unit's dependencies,so we want to use mocks instead of them. Mocks are special objects that simulate thebehavior of the mocked out dependencies. For this purpose we are going to useSinon.JS.

Let's take an example of mocking out MongoDB. Sure, first you will need sinon installed.

npm install sinon --save-dev

As we are doing TDD, first let's write our (initially) failing unit test for a Mongoose model.It will be a model called User with a static method findUnicorns.

How to do this? Let's take a look at the necessary steps:

  • start with the test setup
  • calling the object under test's method
  • finally asserting
var sinon = require('sinon');var expect = require('chai').expect;var mongoose = require('mongoose');var User = require('./../../lib/User');var UserModel = mongoose.model('User');describe('User', function() {  it('#findUnicorns', function(done) {    // test setup    var unicorns = [ 'unicorn1', 'unicorn2' ];    var query = { world: '1' };    // mocking MongoDB    sinon.stub(UserModel, 'findUnicorns').yields(null, unicorns);    // calling the test case    User.colorizeUnicorns(query, function(err, coloredUnicorns) {      // asserting      expect(err).to.be.null;      expect(coloredUnicorns).to.eql(['unicorn1-pink', 'unicorn2-purple']);      // as our test is asynchronous, we have to tell mocha that it is finished      done();    });  });});

Nice, huh? The "only" job left here is to do the actual implementation.(it can be found in the lib folder)

Putting the pieces together - writing integration tests

All our unit tests are passing, great! But how will the system as a whole function?This is where integration tests come in.

During integration testing unit tested parts are combined to verify functional andperformance requirements.

For integration tests we will use hippie.hippie is a thin request wrapper that enables powerful and intuitive API testing.

Add hippie to your project with:

npm install hippie --save-dev

To demonstrate what hippie is capable of, let's create an HTTP endpoint!

This endpoint will serve GET requests at /users. For building APIs using JSON,you can use json:api as a reference.

var hippie = require('hippie');var server = require('../../lib/Server');describe('Server', function () {  describe('/users endpoint', function () {    it('returns a user based on the id', function (done) {      hippie(server)        .json()        .get('/users/1')        .expectStatus(200)        .end(function(err, res, body) {          if (err) throw err;          done();        });    });  });});

The above example queries the server for the user with id=1. You can check thebasic implementation in the lib/Server.js file.

Next up

Now you have learnt all the basics - but what will come next? We will dive deeper in how wewrite APIs at RisingStack, including mocking external APIs like Facebook anddebug performances issues using DTrace.



Post written by Gergely Nemeth, RisingStack