How to fail a test on uncaptured AJAX requests

Recently, the tests for a frontend web project I’m working on started to become more and more troublesome. Full hangs in the middle of a test run became a frequent issue, destroying my precious coding flow.

I’m using Buster.JS and its server mode, where one or more browsers are captured as slaves to a server which orchestrates test runs. This is nice, because it lets me run JavaScript tests in a browser just by running buster test on the command line. For anyone following Buster.JS’s development towards the upcoming 1.0 release, it’s well known that the current implementation of the server part have stability issues, and it is soon to be replaced. Thus, as my test suite grew, I wrote off the increasing amount of test run hangs as the fault of the Buster.JS server.

Of course, this wasn’t the full picture, and as this problem became a lot worse in just a few days, I started investigating other causes. Looking at the console in the browser running the tests, it became evident that some of my tests where doing a lot of failing AJAX requests. Requests that should not have been fired off in the first place, or that should have been caught by a Sinon.JS fake server.

Using Chrome’s console, which gives you the full stack trace leading up to any failed AJAX request, it was easy to identify the code paths that led to the AJAX requests, and subsequently what tests has caused it. In a matter of minutes, I was able to avoid the the AJAX requests either by fixing the implementation or by adding a fake server to the test case, depending on what was appropriate for each case. The effect was instant, and again my test runs became as reliable as they’ve ever been.

So, how can I avoid this from happening again? I don’t want to babysit the browser console for AJAX requests accidentally caused by test runs.

Since none of my tests have a legitimate reason for using real AJAX requests, I want to fail any test that does an AJAX request that is not caught by a Sinon.JS fake server. Sinon.JS does its magic by temporarily replacing window.XMLHttpRequest with a FakeXMLHttpRequest. Thus, if I got the real XMLHttpRequest object to fail the test, I would have a solution.

One of my first attempts threw an exception when XMLHttpRequest.open() was called, but this didn’t cause the test to fail, since jQuery caught and ignored the exception. After some fiddling back and forth, I ended up with the following implementation, which simply asks Buster.JS to fail the current test. This could easily be modified to work with any other test library.

window.XMLHttpRequest.prototype.open = function() {
    buster.assertions.fail(
        'Used AJAX without capturing it with sinon.fakeServer');
};

The code needs to run in the browser before the tests are executed. In the Buster.JS life cycle, it fits perfectly as a “test helper”. Test helpers are executed after your libraries and source code, but before your test code.

Just put the above code in a file named e.g. testHelpers/fail-on-uncaptured-ajax-use.js, and then add it to the testHelpers section of your buster.js config file:

var config = module.exports;

config['Browser tests'] = {
    environment: 'browser',
    rootPath: '.',
    libs: [
        'lib/**/*.js'
    ],
    sources: [
        'src/**/*.js'
    ],
    testHelpers: [
        'testHelpers/fail-on-uncaptured-ajax-use.js',
        'testdata/**/*.js'
    ],
    tests: [
        'test/**/*-test.js'
    ]
};

Then, when you run a test which creates uncaptured AJAX requests, your test will fail hard:

$ buster test
Firefox 13.0, Linux: F
Failure: Firefox 13.0, Linux My app is talking to the server
    Used AJAX without capturing it with sinon.fakeServer
    ("GET","/api",true)@./testHelpers/fail-on-uncaptured-ajax-use.js:2
    ([object Object],w)@./lib/jquery-1.7.2.min.js:4
    ((void 0))@./lib/jquery-1.7.2.min.js:4
    ("/api")@./lib/jquery-1.7.2.min.js:4
    talkToTheServer()@./src/foo.js:2
    ()@./test/foo-test.js:3

1 test case, 1 test, 1 assertion, 1 failure, 0 errors, 0 timeouts