Alexander Beletsky's development blog

My profession is engineering

Testing REST services with javascript

In previous article we’ve reviewed general concept of REST. Now we will implement some basic REST service. And our approach will be - test methods before, implement them later. I’m talking about kind integration tests, the tests that would act exactly as your client, making real calls to storage and return real results. I will use jQuery and qUnit as my weapon of choice. Like in case of FuncUnit it is easy and fun to create those tests.

Why should I start from tests? Pretty simple, by implementing tests before you are looking on your service as client<, not as a developer. When I was working to version 1 of my REST API I didn’t do any tests, basically because I didn’t know how to do them. When I was ready and started implementation of the client code and documentation, I found out major API issues that I had no time to solve. Those issues were related to: design, security, formats and convenience of usage. TDD principles works the same here: clear and simple design through series of tests.

Simple framework

I rely of jQuery and qUnit. jQuery $.ajax method is used to send and receive data. All tests are done in qUnit fashion. What is good to have more: small wrapper function for doing API calls, that would do initial verification of results and work synchronously. Why is it synchronous? Because tests are not application and you do not need all benefits of async calls. Asynchronous behavior requires additional effort for synchronization of results. Even if qUnit supports asynchronous testing, it should be avoided as possible since it makes test code harder to write and read. So, I came up with such implementation:

function api_test(url, type, data, callback) {
  $.ajax(
    {
      url: url,
      type: type,
      processData: false,
      contentType: 'application/json; charset=utf-8',
      data: JSON.stringify(data),
      dataType: 'json',
      async: false,
      complete: function (result) {
        if (result.status == 0) {
          ok(false, '0 status - browser could be on offline mode');
        } else if (result.status == 404) {
          ok(false, '404 error');
        } else {
          callback($.parseJSON(result.responseText));
        }
      }
    });
}


* This source code was highlighted with Source Code Highlighter.

Internal If/else statement could be extended with some specific result codes you might expect. If API call has been finished successfully, result JSON object will be parsed and submitted to a test callback.

Also, I found useful to create a small helper function that would construct API call signature, based on URL, method and parameters:

  // helper
  function createCallUrl(url, apiToken, method, params) {
    var callUrl = url + apiToken + "/" + method;
    for (var p in params) {
      callUrl += "/" + params[p];
    }

    return callUrl;
  }


* This source code was highlighted with Source Code Highlighter.

Setup the environment

All tests requires a setup. In integration testing we basically rely on existing environment (the same that will be used by real application).

Due to security reasons all API calls receive an api token as first argument for any call. Api token is received after successful authentication, so a StartUp for each test we need to login, receive api token and only then proceed with method tests. For qUnit is is natural to place this code to module setup.

  module("v11 api tests", {
    // setup method will authenticate to v.1.1. API by calling 'authenticate'
    // it will store apiToken, so rest of tests could reuse that

    setup: function () {
      var me = this;

      this.url = 'api/v1.1/';
      this.apiToken = null;

      // authenticate
      var method = 'authenticate';
      var data = { email: 'tracky@tracky.net', password: '111111' };
      var type = 'POST';

      api_test(this.url + method, type, data, function (result) {
        ok(result.success, method + " method call failed");

        me.apiToken = result.data.apiToken;
        ok(me.apiToken.length == 32, "invalid api token");
      });
    }
  }
  );


* This source code was highlighted with Source Code Highlighter.

Module holds API URL and token, so they are reusable through the rest of tests. If setup failed to authenticate, all tests would be failed because they could not use any call without token.

Testing methods

I have a number of REST style methods in my API:

http://trackyt.net/api/v1.1/token/tasks/all
http://trackyt.net/api/v1.1/token/tasks/add
http://trackyt.net/api/v1.1/token/tasks/delete/112
http://trackyt.net/api/v1.1/token/tasks/start/112
http://trackyt.net/api/v1.1/token/tasks/stop/112

and so on..

I’ll give some examples of tests, so you will be able to follow main idea:

Get all task call test:

  test("get all tasks method", function () {
    var method = 'tasks/all';
    var data = null;
    var type = 'GET';
    var params = [];

    var call = createCallUrl(this.url, this.apiToken, method, params);

    api_test(call, type, data, function (result) {
      ok(result.success, method + " method call failed");

      var tasks = result.data.tasks;
      ok(tasks.length >= 1, "tasks has not been returned");
    });
  });


* This source code was highlighted with Source Code Highlighter.

Get all task call receives deterministic response test:

  test("get all tasks returns all required fields", function () {
    var method = 'tasks/all';
    var data = null;
    var type = 'GET';
    var params = [];

    var call = createCallUrl(this.url, this.apiToken, method, params);

    api_test(call, type, data, function (result) {
      ok(result.success, method + " method call failed");

      var tasks = result.data.tasks;
      ok(tasks.length >= 1, "tasks has not been returned");

      var task = result.data.tasks[0];
      ok(task.id !== undefined, "Id field is absent");
      ok(task.description !== undefined, "Description field is absent");
      ok(task.status !== undefined, "Status field is absent");
      ok(task.createdDate !== undefined, "CreatedDate field is absent");
      ok(task.startedDate !== undefined, "StartedDate field is absent");
      ok(task.stoppedDate !== undefined, "StoppedDate field is absent");
    });
  });


* This source code was highlighted with Source Code Highlighter.

Add new task method test:

  test("task add method", function () {
    var method = 'tasks/add';
    var data = { description: 'new task 1' };
    var type = 'POST';
    var params = [];

    var call = createCallUrl(this.url, this.apiToken, method, params);

    api_test(call, type, data, function (result) {
      ok(result.success, method + " method call failed");
      ok(result.data != null, "data is null");
      ok(result.data.task.id > 0, "id for first item is wrong");
    });
  });


* This source code was highlighted with Source Code Highlighter.

Delete task method test:

  test("delete task method", function () {
    var me = this;

    var method = 'tasks/all';
    var data = null;
    var type = 'GET';
    var params = [];

    var call = createCallUrl(this.url, this.apiToken, method, params);

    api_test(call, type, data, function (result) {
      ok(result.success, method + " method call failed");

      var taskId = result.data.tasks[0].id;
      ok(taskId >= 1, "could not get task for deletion");

      var method = 'tasks/delete/' + taskId;
      var data = null;
      var type = 'DELETE';
      var params = [];

      var call = createCallUrl(me.url, me.apiToken, method, params);

      api_test(call, type, data, function (result) {
        ok(result.success, method + " method call failed");
        ok(result.data.id != null, "data is null");
      });
    });
  });


* This source code was highlighted with Source Code Highlighter.

Rest of tests are available on github, check it out to get additional ideas.

Running tests

As any kind of qUnit tests they could be easily run in browser.

For continues integration system, they have to be run from command-line. It is easily possible using FuncUnit + Selemium Server and described here.

Debugging the tests

Sure, you need to be able to run tests under debugger to see what might went wrong. For debuging test code, there is nothing better than FireBug. Just place the breakpoint on a line you need, press F5 to restart tests.

If you need to debug actual API implementation code (which in my case is C#/ASP.net MVC application), I start the web site under debugger (F5 in VS2010), place breakpoint in corresponding method and press F5 in browser to to restart tests.

Conclusions

I liked the idea of those integration tests by means of javascript. I was happy to get final results: the interface is more strict and more corresponds to REST principles. It is much more faster to write tests with javascript instead of C# or Java. Just compare this and this and feel the difference. Write less, get more.

As javascript could be treated as “pseudo language”, since it is easy to read it - API test suite could be used as a developers documentation. If you need to do a call, check the corresponding test, everything there.