Nodejitsu

Save time managing and deploying your node.js app. Code faster with jitsu and npm

RESTeasy: Test any API in node.js

About the author

Name
Location
Worldwide
nodejitsu nodejitsu

It seems like every developer has their own (passionate) opinion about how software should be tested:

  • "I would kill myself without BDD"
  • ZOMG! How could you write software without TDD!?!
  • "Testing? Meh. I try to have unit tests..."

I think more developers than not fall into the mindset of the last quote; we want to test our software but we don't have the time, or resources to make 100% test coverage a reality. The underlying fact behind this dirty secret is that testing can be quite difficult and requires the utmost dedication to flesh out completely.

This excess of verbosity and difficulty was the motivation for writing APIeasy: a simple tool for writing BDD tests for any REST API. This article will explore a sample API written in node.js and how it can be tested with APIeasy.


Update: So the Internet and I were talking and it turns out there is already a library called RESTeasy. But I mean, who still uses Java anyway? In response to a request from the Internet at-large (i.e. the authors of the existing RESTeasy library) I have changed the name of this library to APIeasy.

Getting started

APIeasy will let you test any API, not just APIs written in node.js. So if you're writing in PHP, Ruby, or Python and looking for a good way to get started in node, here's your answer. For the sake of this example, however, we will be writing our API in node.

The following code sample creates a simple JSON-based web service using journey with a couple of simple routes:

  • GET /ping: Responds with { pong: true }
  • POST /ping: Responds with the POST data
  • GET /login: Responds with { token: /\d+/ }
  • GET /restricted: Responds with { authorized: true } if the correct token is sent

var http = require('http'),  
    journey = require('journey');

//
// Create a journey router. Not familiar with journey? Checkout:
// http://github.com/cloudhead/journey
//
var token, router = new journey.Router({  
  strict: false,
  strictUrls: false,
  api: 'basic'
});

//
// Create a simple (and not production ready) function
// for authorization incoming requests.
//
function isAuthorized (req, body, next) {  
  return parseInt(req.headers['x-test-authorized'], 10) !== token 
    ? next(new journey.NotAuthorized())
    : next();
}

//
// GET /ping: 
//   * Responds with 200
//   * Responds with `{ pong: true }`
//
router.get('/ping').bind(function (res) {  
  res.send(200, {}, { pong: true });
});

//
// POST /ping
//   * Responds with 200
//   * Responds with the data posted
//
router.post('/ping').bind(function (res, data) {  
  res.send(200, {}, data);
});

//
// GET /login
//   * Responds with 200
//   * Responds with { token: /\d+/ }
//
router.get('/login').bind(function (res) {  
  if (!token) {
    token = Math.floor(Math.random() * 100);
  }

  res.send(200, {}, { token: token });
});

//
// Filter requests to /restricted using isAuthorized.
//
router.filter(isAuthorized, function () {  
  //
  // GET /restricted
  //   * Responds with 200
  //   * Responds with { authorized: true }
  //
  this.get('/restricted').bind(function (res) {
    res.send(200, {}, { authorized: true });
  });
});

//
// Create a simple HTTP server to 
// consume our router.
//
http.createServer(function (request, response) {  
  var body = "";

  request.addListener('data', function (chunk) { body += chunk });
  request.addListener('end', function () {
    //
    // Dispatch the request to the router
    //
    router.handle(request, body, function (result) {
      response.writeHead(result.status, result.headers);
      response.end(result.body);
    });
  });
}).listen(8000);


Creating an APIeasy suite

So what are the concerns that one might have when testing such an API taking into consideration the benefits of descriptive tests (read: BDD)?

  1. Ensure the correct HTTP response code is returned
  2. Ensure the correct response body is returned
  3. Ensure that a custom assertion on the HTTP response is satisfied
  4. Organize tests so that each test has a meaningful description

APIeasy addresses all of these concerns in a simple, fluent (i.e. chainable) API. The core of this API are helper methods for making common types of HTTP request. Each of these methods will perform an HTTP request against the current path for the suite configured by calls to .path() or .unpath(). The optional parameters configure the subpath to the resource, URI paramaters, and optional request body.

  • .get(/* [uri, params] */):
  • .post(/* [uri, data, params] */)
  • .put(/* [uri, data, params] */)
  • .del(/* [uri, data, params] */)
  • .head(/* [uri, params] */).

After a call to one of these methods has been made the user can add tests for this particular request by using .expect() which has a couple of options:

  • .expect(200): Ensures that a 200 response code is returned
  • .expect(200, { some: 'body' }): Ensures that a 200 and that the response body is { some: 'body' }
  • .expect(function (error, response, body) { ... }): Ensures that the custom assertion on the error, response and body is met.

var APIeasy = require('api-easy');

//
// Create a APIeasy test suite for our API
//
var suite = APIeasy.describe('api');

//
// Add some discussion around the vowsjs tests.
// Not familiar with vows? Checkout:
// http://vowsjs.org 
//
suite.discuss('When using the API')  
     .discuss('the Ping resource');

In the above code sample we create a APIeasy suite, and then add some discussion around the tests we are about to create. Each method on the APIeasy suite returns the suite itself so we can continue to add additional tests and test context to the suite.


Adding tests with APIeasy

APIeasy allows you to add both tests and set default options for each outgoing HTTP request against your API. Lets add some tests for the /ping route in our sample API:

//
// Here we will configure our tests to use 
// http://localhost:8080 as the remote address
// and to always send 'Content-Type': 'application/json'
//
suite.use('localhost', 8000)  
     .setHeader('Content-Type', 'application/json');
     //
     // A GET Request to /ping
     //   should respond with 200
     //   should respond with { pong: true }
     //
     .get('/ping')
       .expect(200, { pong: true })
      //
      // A POST Request to /ping
      //   should respond with 200
      //   should respond with { dynamic_data: true }
      //
     .post('/tests', { dynamic_data: true })
       .expect(200, { dynamic_data: true })


Adding sequential tests

As you build more complex test suites, you will want to add and remove discussion text using the .discuss() and .undiscuss() methods. This design is similar to the native Javascript Array .shift() and .unshift() except that the operations are switched: .discuss() will append the string to the set of discussion used in the current suite and .undiscuss() will remove the latest discussion string.

Testing APIs also frequently requires that certain tests be run in sequence (i.e. one test must be run after another). APIeasy makes this possible through the .next() method. Tests after an invocation of .next() will be added to a separate vows batch ensuring that they run after the current tests.

//
// Add more tests
//
suite.undiscuss()  
     .discuss('when authenticating')
     .get('/login')
     .expect(200)
     .expect('should respond with the authorize token', function (err, res, body) {
       var result = JSON.parse(body);
       assert.isNotNull(result.token);

       suite.before('setAuth', function (outgoing) {
         outgoing.headers['x-test-authorized'] = result.token;
         return outgoing;
       });
     })
     //
     // Before we can test our request to /restricted 
     // the request to /login must respond. To ensure this
     // we make a call to .next() before our next call to .get()
     //
     .next()
     .get('/restricted')
       .expect(200, { authorized: true })

As you can see in the above code sample, .next() is extremely important because we can only make a successful request to /restricted after we have gotten an authentication token from our request to /login.


Running your test suite

APIeasy is built on top of vows, a test library for node.js. Running your APIeasy tests is simple: just use the vows test runner:

  vows your-test-suite.js


More documentation

There is more information available on APIeasy at the GitHub project page. Most of the documentation is written using the literate programming tool, docco. Read the source code here: http://indexzero.github.com/api-easy. APIeasy is a work in-progress, so submit your issues and feedback. Your feedback is valuable.