Unit testing AJAX requests with QUnit

13,051

Solution 1

With jQuery, you can use the xhr object that .ajax() returns as a promise, so you can add more handlers (see below) than just the single success, complete and error ones you define in the options. So if your async function can return the xhr object, you can add test-specific handlers.

As for the URL, that's a little trickier. I've sometimes set up a very simple Node server on localhost, which just serves canned responses that were copied from the real server. If you run your test suite off that same server, your URLs just need to be absolute paths to hit the test server instead of the production server. And you also get a record of the requests themselves, as a server sees them. Or you can have the test server send back errors or bad responses on purpose, if you want to see how the code handles it.

But that's of course a pretty complex solution. The easier one would be to define your URLs in a place where you can redefine them from the test suite. For instance:

/* in your code */
var X = function () {
    this.fire = function () {
        return $.ajax({ url: this.constructor.url, ... });
    };
};
X.url = "someURL.php"; // the production url

/* in your tests */
X.url = "stub.php"; // redefine to the test url

Also, QUnit has an asyncTest function, which calls stop() for you. Add a tiny helper to keep track of when to start again, and you've got a pretty good solution.

Here's what I've done before

// create a function that counts down to `start()`
function createAsyncCounter(count) {
    count = count || 1; // count defaults to 1
    return function () { --count || start(); };
}

// ....

// an async test that expects 2 assertions
asyncTest("testing something asynchronous", 2, function() {
    var countDown = createAsyncCounter(1), // the number of async calls in this test
        x = new X;

    // A `done` callback is the same as adding a `success` handler
    // in the ajax options. It's called after the "real" success handler.
    // I'm assuming here, that `fire()` returns the xhr object
    x.fire().done(function(data, status, jqXHR) {
        ok(data.ok);
        equal(data.value, "foobar");
    }).always(countDown); // call `countDown` regardless of success/error
});

Basically countDown is a function that counts down to zero from whatever you specify, and then calls start(). In this case, there's 1 async call, so countDown will count down from that. And it'll do so when the ajax call finishes, regardless of how it went, since it's set up as an always callback.
And because the asyncTest is told to expect 2 assertions, it'll report an error if the .done() callback is never called, since no assertions will be run. So if the call completely fails, you'll know that too. If you want to log something on error, you can add a .fail() callback to the promise chain.

Solution 2

If it's a unit test that can (and should) be run in isolation from the server side, you can simply "replace" $.ajax to simulate whatever behavior. One easy example:

test("Test AJAX function", function() {
  // keep the real $.ajax
  var _real_ajax = $.ajax;

  // Simulate a successful response
  $.ajax = function(url, opts) {
    opts.success({expected: 'response'});
  }

  var myX = new X();
  // Call your ajax function
  myX.fire();
  // ... and perform your tests

  // Don't forgot to restore $.ajax!
  $.ajax = _real_ajax;
});

Obviously you can also perform a real ajax call with stubbed url/data:

// Simulate a successfully response
$.ajax = function(url, opts) {
  opts.success = function(data) { 
    console.log(data);
    start();
  }
  _real_ajax('stub.php', opts)
}

If you haven't a complex response, I prefer the first approach, because it is faster and easy to comprehend.
However, you can also take another way and put the Ajax logic in it's own method, so you can easily stub it during tests.

Share:
13,051
James Allardice
Author by

James Allardice

I'm a senior engineer and co-founder of orangejellyfish, with a passion for all things web development. I am a particularly big fan of ES2015 (ES6), React, Node, CouchDB and (in the past) jQuery (38th to earn the gold jQuery badge, 57th to earn the gold JavaScript badge). You can follow me on Twitter as @james_allardice. Some things I've written: linterrors.com - Get detailed explanations of JSLint, JSHint and ESLint error messages. Placeholders.js - An HTML5 placeholder attribute polyfill. No dependencies on jQuery. Patterns.js - An HTML5 pattern attribute polyfill. No dependencies, and works in IE6! Progressive.js - Interact with the DOM before it's ready! gzipper - Find the gzipped size of any file or text Notepad++ jQuery code hints - A Notepad++ plugin to provide jQuery autocompletion.

Updated on July 05, 2022

Comments

  • James Allardice
    James Allardice almost 2 years

    We are trying to implement QUnit JavaScript tests for a JS-heavy web app. We are struggling to find a way to successfully test methods that involve jQuery AJAX requests. For example, we have the following constructor function (obviously this is a very simplistic example):

    var X = function() {
        this.fire = function() {
            $.ajax("someURL.php", {
                data: {
                    userId: "james"
                },
                dataType: "json",
                success: function(data) {
                    //Do stuff
                }
            });
        };
    };
    var myX = new X();
    myX.fire();
    

    We are trying to find a way to test the fire method, preferably with a stubbed URL instead of the real someURL.php.

    The only obvious solution to me at the moment is add the URL, and the success callback, as arguments to the constructor function. That way, in the test, we can create a new instance of X and pass in the stub URL, and a callback to run when the stub returns a response. For example:

    test("Test AJAX function", function() {
        stop();
        var myX = new X();
        //Call the AJAX function, passing in the stub URL and success callback
        myX.fire("stub.php", function(data) {
            console.log(data);
            start();
        });
    });
    

    However, this doesn't seem like a very nice solution. Is there a better way?

  • James Allardice
    James Allardice over 12 years
    This is great. Thank you very much :)
  • Adam Waselnuk
    Adam Waselnuk over 9 years
    For isolating my tests from the server side, I had a lot of success with this library github.com/jakerella/jquery-mockjax