Skip to content

cjohansen.no

Sinon.JS 0.6.0 - Fake XMLHttpRequest and improved test framework integration

Sinon.JS 0.6.0 is out and it features a new FakeXMLHttpRequest object and a high-level interface for testing xhr-dependent code along with improvements to sinon.sandbox and sinon.test.

Warning: Slightly long, but thorough post. Short version is in the currently basic API docs.

Testing "Ajax" Code

Sinon.JS 0.6.0 sets out to provide an easy way to test code that makes server requests using the XMLHttpRequest object (or the XMLHTTP ActiveXObject). The problem with testing such code is that you need a server replying to the requests, and you need tests to execute asynchronously in order to succeed. This means complicated test setup and possibly long-running tests, perhaps even timeouts if tests are run too fast.

If you're using some kind library - and chances are you are - you can get around all this by using sinon.stub or sinon.mock to stub/mock e.g. jQuery.ajax. This will work, but sometimes it's nice to be able to write mini-integration tests that runs both the request and the success callback in one go. Enter sinon.useFakeXMLHttpRequest.

Faking XHR with Sinon.JS

Sinon.JS 0.6.0 introduces sinon.FakeXMLHttpRequest, a constructor that creates, well, fake XMLHttpRequest objects. They behave pretty much like native browser implementations, but they do not make actual requests. To use it, you call sinon.useFakeXMLHttpRequest();, which returns an object with a restore() method, much like other Sinon.JS fakes. When calling this function, Sinon.JS will replace the native XMLHttpRequest constructor (only if it exists) and/or the ActiveXObject constructor (only if it exists) with the fake implementation. This means that you don't have to change your production code to test it using Sinon.JS. Note that only existing constructors are replaced, so for instance IE6 won't suddenly gain a "native" XMLHttpRequest. Additionally, the ActiveXObject constructor will continue to work as expected for progIds which are not related to XMLHTTP.

Behavior verification on XHR objects

The low-level way to use the fake xhr objects is to register a listener to receive all created xhr objects. The following test does so in order to perform behavior verification on them.

// JsTestDriver test case, works with any testing framework
TestCase("MyAjaxTest", {
  setUp: function () {
    this.fakeXhr = sinon.useFakeXMLHttpRequest();
    var requests = this.requests = [];

    this.fakeXhr.onCreate = function (xhr) {
      requests.push(xhr);
    };
  },

  tearDown: function () {
    this.fakeXhr.restore();
  },

  "test something ajax-y": function () {
    jQuery.ajax({ url: "/my/page" });

    assertEquals("/my/page", this.requests[0].url);
  }
});

As I mentioned, this is the low-level way of testing the xhr objects. As you would expect, it requires a bit of setup. The test case simply collects any object created by way of XMLHttpRequest/ ActiveXObject (for XMLHTTP progIds). The fake objects have a few properties that you can use for behavior verification:

In addition to these properties, you can call some methods on it to simulate a response coming through:

Using these, you could write the above test like so:

TestCase("MyAjaxTest", {
  setUp: function () {
    this.fakeXhr = sinon.useFakeXMLHttpRequest();
    var requests = this.requests = [];

    this.fakeXhr.onCreate = function (xhr) {
      requests.push(xhr);
    };
  },

  tearDown: function () {
    this.fakeXhr.restore();
  },

  "test something ajax-y": function () {
    var spy = sinon.spy();
    jQuery.ajax({ url: "/my/page", success: spy });
    this.requests[0].respond(200, {}, "OK");

    assert(spy.called);
  }
});

In other words, using respond() will cause the xhr object to act exactly as if it had gotten a response from the server.

A higher-level XHR testing abstraction

I've tried to build Sinon.JS to be decoupled, layering increasingly higher-level concepts on top of solid low-level tools, like the one I just showed you. FakeXMLHttpRequest is no exception. You can use the sinon.fakeServer object to build a mock server for your tests that handles the requests for you. Take a look at our contrived example again:

TestCase("MyAjaxTest", {
  setUp: function () {
    this.server = sinon.fakeServer.create();
    this.server.respondWith([200, {}, "OK"]);
  },

  tearDown: function () {
    this.server.restore();
  },

  "test something ajax-y": function () {
    var spy = sinon.spy();
    jQuery.ajax({ url: "/my/page", success: spy });
    this.server.respond();

    assert(spy.called);
  }
});

The server requires less setup because it calls sinon.useFakeXMLHttpRequest and handles the requests for you. Additionally, it provides a high-level API to set the server up to respond to requests. The respondWith method adds capabilities to the server. It can be used in a number of ways:

You can call the respondWith method as many times as you want to set up the server with capabilities. This means you can add one response within a single test, or set up a comprehensive mapping of your server in a separate file to share between tests.

The upside of using the server is that it also handles synchronous requests. Whenever a synchronous request comes it, it is dealt with immediately. If an asynchronous requests comes by, it is queued, and you can tell the server to process all requests by calling server.respond();. If none of the responses matches a given request, it receives a [400, {}, ""] response.

"Faked" HTTP methods

Frameworks like Ruby on Rails allow apps to use badly supported HTTP methods DELETE and PUT by piggy-backing a POST request with a _method parameter in the body. Sinon.JS can do this too:

TestCase("MyAjaxTest", {
  setUp: function () {
    this.server = sinon.fakeServer.create();
    this.server.fakeHTTPMethods = true;
  },

  tearDown: function () {
    this.server.restore();
  },

  "test something ajax-y": function () {
    var spy = sinon.spy();
    this.server.respondWith("DELETE", [200, {}, "OK"]);

    jQuery.ajax({
      url: "/my/page",
      type: "post",
      data: { method: "_delete" },
      success: spy
    });

    this.server.respond();

    assert(spy.called);
  }
});

As you can see, setting server.fakeHTTPMethods = true tells Sinon.JS to look for _method in the POST body. If your framework of choice does something similar, but not exactly like that you can simply override the server.getHTTPMethod(request) method. The default one looks like this:

getHTTPMethod: function getHTTPMethod(request) {
  if (this.fakeHTTPMethods && /post/i.test(request.method)) {
    var match = request.requestBody.match(/_method=([^\b;]+)/);
    return !!match ? match[1] : request.method;
  }

  return request.method;
}

Override at will.

jQuery 1.3.x

While testing Sinon.JS 0.6.0 on some code using jQuery 1.3.x, I discovered (after some cursing and table-slamming) that this version of jQuery does in fact not use onreadystatechange at all. Instead, it uses setInterval to poll the xhr object for completion. Because I'm guessing there's a lot of jQuery 1.3.x in the wild, I thought it might be nice to provide a solution. The solution is to use sinon.fakeServerWithClock in place of sinon.fakeServer. This object also uses the clock object and fake setInterval and friends as discussed previously. When you call respond on this server, it also ticks the clock ahead long enough to fire all callbacks registered with setTimeout and setInterval.

The best of two worlds

To provide you with the best of two worlds, the server object also exposes a requests array which contains all the requests, in case you occasionally want to manage them manually.

Improved sandbox

The sinon.sandbox object discussed previously has been augmented to support the new XHR capabilities. Using it you can now collect all your stubs, spies and mocks along with faked timers and XMLHttpRequest in one convenient object. A single call to sandbox.restore(); will get everything back to normal. Restoring is especially important with JsTestDriver - failing to restore e.g. XHR will make it impossible for JsTestDriver to run any more tests (though it could've cached the object locally to be safe, but it doesn't).

The contrived test once again, this time using the sandbox:

TestCase("MyAjaxTest", {
  setUp: function () {
    this.sandbox = sinon.sandbox.create();
    this.sandbox.useFakeServer();
    this.sandbox.useFakeTimers();
  },

  tearDown: function () {
    this.sandbox.restore();
  },

  "test something ajax-y": function () {
    var spy = sinon.spy();

    jQuery.ajax({
      url: "/my/page",
      type: "delete"
      success: function () {
        setTimeout(spy, 16);
      }
    });

    this.sandbox.server.requests[0].respond(200, {}, "");
    this.sandbox.clock.tick(16);

    assert(spy.called);
  }
});

I realize that this example gets more contrived with each iteration, but hopefully you get the point.

Improved sinon.test (and by extension sinon.testCase)

If you've read my previous posts on Sinon.JS, or if you've used it, you may remember the sinon.test function. It can wrap a test function to provide automatic handling of the sandbox object (previously the less capable collection object). In 0.5.0 it passed a stub and a mock function to the test case which were bound to the sandbox, so using them would ensure whatever was stubbed/mocked was automatically restored and verified after the test ran.

In 0.6.0, sinon.test use a sandbox which you can configure through sinon.config. By default it no longer passes bound functions to the test. Instead, it exposes bound functions and - if configured to do so - server, requests and clock objects to the test case on which the test function is called. Let's show our contrived example again:

sinon.config = {
  useFakeTimers: true,
  useFakeServer: true
};

TestCase("MyAjaxTest", {
  "test something ajax-y": sinon.test(function () {
    var spy = this.spy();

    jQuery.ajax({
      url: "/my/page",
      type: "delete"
      success: function () {
        setTimeout(spy, 16);
      }
    });

    this.server.requests[0].respond(200, {}, "");
    this.clock.tick(16);

    assert(spy.called);
  })
});

As you can see, using the sandboxed test means no manual handling of fakes at all. I think this is pretty cool. You can also sandbox an entire test case this way:

TestCase("MyAjaxTest", sinon.testCase({
  /* ... */
}));

This is almost the same as wrapping each test in a call to sinon.test with one important exception - setUp and tearDown can use this.stub and others along with the server and clock, too.

Configuring sinon.test

While the function is currently configurable, I'm not entirely happy with the configuration option names - I'd like them to be clearer, so that may change before 1.0.0. The default configuration looks like:

sinon.config = {
  injectIntoThis: false,
  injectInto: null,
  properties: ["spy", "stub", "mock", "clock", "server", "requests"],
  useFakeTimers: false,
  useFakeServer: false
};

In other words, XHR and timers are not faked by default, but they will be exposed to the test case if you set those options to true. If you want to expose the properties on some other object than the this object of the test function, set injectIntoThis to false and use an arbitrary object for injectInto. For instance, if you're really keen on saving keystrokes, and can live with global properties, you can do this:

sinon.config = {
  injectIntoThis: false,
  injectInto: this, // global object
  useFakeTimers: true,
  useFakeServer: true
};

TestCase("MyAjaxTest", {
  "test something ajax-y": sinon.test(function () {
    var spy = spy();

    jQuery.ajax({
      url: "/my/page",
      type: "delete"
      success: function () {
        setTimeout(spy, 16);
      }
    });

    server.requests[0].respond(200, {}, "");
    clock.tick(16);

    assert(spy.called);
  })
});

Then you can use the properties as globals. I wouldn't recommend this approach though, but again - you get the point.

Ensuring compatibility with 0.5.0 tests

If you've already started using Sinon.JS 0.5.0, you may have come to depend on the stub and mock functions passed to test functions wrapped in sinon.test. In that case you can use the following configuration:

sinon.config = {
  injectIntoThis: false,
  properties: ["stub", "mock"]
};

When both injectIntoThis is false and injectInto is null, Sinon.JS passes the named properties as arguments to the test function, in the order specified. You can still pass it all the properties and fake the server, the choice is yours.

In summary

If you got all the way through, you might be anxious to try it out. Head over to cjohansen.no/sinon/ to download your copy. If you have any feedback on the APIs, suggestions, found bugs, have patches or what ever, please do get in touch!

The code is available both on Github and Gitorious, for your peeking and forking pleasure. Enjoy!

Possibly related