How to Fearlessly Iterate Your REST APIs through HTTP Endpoint Testing

Written by Seth Davenport

One of the largest process problems in building single page applications with AngularJS, React or Backbone is safely maintaining the contract between the AngularJS application and the REST API.

Another issue is defining future integration contracts so the front-end team can build against mock servers and not be held up coordinating every new feature against back-end APIs.

Supertest provides a key weapon for solving these critical process problems.

What is it?

HTTP Endpoint tests are a type of integration test. In other words, we're testing a large chunk of the code without mocks, in order to verify the operation of a set of dependent components together.

In particular, we're using tools to drive the HTTP API exposed by the backend server and verify it's behaviour.

These tools are great for testing Node.js applications; they also work equally well for testing Rails, PHP, Java, .NET or Python applications. All that is required is a REST API.

Why do it?



There are several reasons you may want to do this.

Contract Validation

We live in a distributed world. Modern applications are loosely-coupled distributed systems where the components, from your mobile app, to your backend, to your hosted databases, all communicate over the web. And most likely, they do this over HTTP.

Each of these HTTP APIs defines a contract between systems. These contracts turn out to be excellent boundaries for automated validation.

Acceptance Testing

"I'm just a front-end guy" you may say. "I don't need to test my third-party backend. If those guys break it, it's their fault!"

Except when it becomes your problem...

Cook up a suite of endpoint tests and you can immediately and automatically validate new versions of services you depend on.

Documentation

A clean set of readable tests make excellent documentation for your API. Documentation you can run. Documentation that never gets out of date. Fluent, expressive tools like supertest, mocha, and chai will make you wonder why you ever tried to document with comment parsers.

And of Course, Verification

If you're writing your own backend, your endpoint tests will be another safety net allowing you to iterate fearlessly on your code.

Just Show me How Already!

Testing an HTTP endpoint is really easy, using Supertest and Mocha.

Because supertest calls are asynchronous, we would normally have to use mocha's done() function to make sure the test waits until the request has completed. However, I prefer to use the super-test-as-promised package to express this more cleanly.

Here's a simple example:

'use strict';  
var supertest = require('supertest-as-promised');  
var request = supertest('https://ngcourse.herokuapp.com');  
var assert = require('chai').assert;

describe('Backend API', function() {  
  it ('should return the list of users', function() {
    return request.get('/api/v1/users')
      .expect(200);
  });
});

Because we've returned a promise from the test itself, mocha knows it's an async test and we don't have to remember to call done().

Just run it with mocha:

mocha my-http-test.js  

Some More Complex Examples

If you can express it in HTTP, you can test it with supertest:

'use strict';  
var supertest = require('supertest-as-promised');  
var request = supertest('https://ngcourse.herokuapp.com');  
var assert = require('chai').assert;

describe('HTTP Endpoint Tests', function() {  
  var newTaskId;

  it('should be able to set and expect headers', function() {
    return request.get('/api/v1/users')
      .send('accept', 'application/json')
      .expect(200)
      .expect('content-type', 'application/json');
  });

  it('should be able to examine the response', function() {
    return request.get('/api/v1/users')
      .expect(200)
      .expect(function(response) {
        var body = JSON.parse(response.text);
        assert(body.length === 5, '5 users returned');
      });
  });

  it('should be able to POST a payload to an URL', function() {
    var jsonPayload = {
      owner: 'alice',
      description: 'Write some tests!'
    };

    return request.post('/api/v1/tasks')
      // Smart enough to set Content-type and JSON stringify.
      .send(jsonPayload)
      .expect(201)
      .expect(function(response) {
        var newTask = JSON.parse(response.text)[0];
        newTaskId = newTask._id;
        assert(
          jsonPayload.description === newTask.description,
          'Description is as expected');
      });
  });

  it('can test multi-request scenarios', function() {
    var newTaskId;
    var jsonPayload = {
      owner: 'alice',
      description: 'Write some tests!'
    };

    // Create a task.
    return request.post('/api/v1/tasks')
      .send(jsonPayload)
      .expect(200)
      .then(function(createResponse) {
        newTaskId = JSON.parse(createResponse.text)[0]._id;
        assert(!!newTaskId, 'got a new task ID');

        // Make sure we can get the newly created task.
        return request.get('/api/v1/tasks/' + newTaskId)
          .expect(200);
      })
      .then(function(getResponse) {
        var id = JSON.parse(getResponse.text)[0]._id;
        assert(newTaskId === id, 'got the same task ID back');

        // Delete the task when we're done.
        return request.delete('/api/v1/tasks/' + newTaskId)
          .expect(200);
      });
  });
});

You can even test authenticated endpoints by sending cookies or authentication headers.

Conclusion

Supertest and Mocha's fluent APIs make writing HTTP endpoint tests easy. Such a test suite becomes a very readable description of the contract between client and server. HTTP endpoint testing is another powerful weapon in your quality arsenal.