6
\$\begingroup\$

I'm writing unit tests for a web application that has quite a lot of UI interactions. I would like some feedback regarding on how to handle click events with asynchronous code.

My goal here is to simluate users interacting with the page. That's why I'm triggering events rather than making function calls.

Here is my idea so far:

$('#foo').on('click', function(){

     var dfd = $.Deferred();

     simulateAsync().done(dfd.resolve).fail(dfd.reject);
     $(this).data({ promise: dfd.promise() });
});

function simulateAsync(){

    var dfd = $.Deferred();

    setTimout(function(){
        $('body').text('foo');
        dfd.resolve();
    }, 1000);

    return dfd.promise();
}

Testing the code:

test('foo text set on body after 1000ms when clicking #foo', function(assert){

    var $foo = $('#qunit-fixture').find('#foo'),
        done = assert.async();

    $foo.trigger('click');

    var completePromise = $foo.data('promise');

    completePromise.done(function(){
        assert.equal($('#bar').text(), 'bar');
        done();
    });
});

http://jsfiddle.net/0m4t4ndt/2/

\$\endgroup\$
5
  • \$\begingroup\$ I'm not sure what you're testing here. The click handler looks like test code. How does this help you test a real click handler that won't call simulateAsync? \$\endgroup\$ Commented Jan 28, 2015 at 3:35
  • \$\begingroup\$ @DavidHarkness My real code replaces simluateAsync with an actual ajax call. My goal here is just to know when all the code in my click handler has executed, including the promise(s). \$\endgroup\$
    – Johan
    Commented Jan 28, 2015 at 8:38
  • \$\begingroup\$ I assumed you are using QUnit; is that the case? \$\endgroup\$ Commented Jan 30, 2015 at 3:54
  • \$\begingroup\$ @DavidHarkness Yes. But I don't want to mock the asynchronous code like in your example - I want to run the actual API call. I'm aware that it adds a dependency to my external API, but since my tests rely on data from it, that's how I want it to work. \$\endgroup\$
    – Johan
    Commented Jan 30, 2015 at 9:46
  • \$\begingroup\$ Very well, I added a non-mocking solution. \$\endgroup\$ Commented Jan 31, 2015 at 16:42

2 Answers 2

6
+200
\$\begingroup\$

Without the real code to be tested, I can only provide an example of how I've tested something similar with QUnit and Sinon.JS, though you don't even need the latter.

You can either use Sinon.JS to mock a response to the AJAX call or substitute your own manual mock for $.getJSON or $.ajax. I find the former preferable as it documents the AJAX call and tests the full front-end code, but it's beyond the scope of this question. You could argue that you're needlessly testing jQuery itself, and you'd be right. YMMV :)

Mocking JQuery

Let's assume this click event handler that uses the /load-foo AJAX call to acquire a JSON string. I've omitted the extra deferred for simplicity as your current test of the promise attached to the #foo element is fine.

$('#foo').on('click', function(){
    $.getJSON('/load-foo', {
        foo: 'bar'
    }).done(function (foo) {
        $('body').text(foo);
    });
});

Here's how you would test this handler.

test('foo text via AJAX set on body after clicking #foo', function(assert) {
    var $foo = $('#qunit-fixture').find('#foo'),
        getJSON = $.getJSON;

    $.getJSON = function (url, data) {
        assert.equal(url, '/load-foo');
        assert.equal(data.foo, 'bar');
        return new $.Deferred().resolve('"foobar"');
    }

    $foo.trigger('click');

    assert.equal($('body').text(), 'foobar');
    $.getJSON = getJSON;
});

For safety, I'd recommend saving and restoring $.getJSON in the module's setup and teardown functions. If you don't, any exception thrown in the test will leave the mock in place.

Mocking AJAX

While this does result in a lot of boilerplate sometimes, you can use Sinon's fake server to mock the AJAX responses. The beauty here is that it will match up the request to the response to ensure your AJAX calls (URL, data, type, etc.) are all correct and then supplies a canned response.

Here's the same test above using a fake server.

module('AJAX tests', {
    setup: function () {
        this.server = sinon.fakeServer.create();
    },
    teardown: function () {
        this.server.restore();
    }
});

test('foo text via AJAX set on body after clicking #foo', function(assert) {
    var $foo = $('#qunit-fixture').find('#foo');

    // setup the expected request with the canned response
    this.server.respondWith('/foo', 
        [ 200, { 'Content-Type': 'text/json' }, '"foobar"' ]
    );

    $foo.trigger('click');

    // handle queued requests immediately
    this.server.respond();

    assert.equal($('body').text(), 'foobar');
});

If you're mocking a lot of JSON AJAX requests, you can create a common fixture to make this a little saner. You can call this.mockAjax with a number for an HTTP response code, an object to convert it to JSON, or a string as the raw response.

var ajaxFixture = {
    setup: function () {
        this.server = sinon.fakeServer.create();
    },
    teardown: function () {
        this.server.restore();
    },
    mockAjax: function (url, response) {
        var type = { 'Content-Type': 'text/json' };
        if (typeof response === 'number') {
            this.server.respondWith(url, [ response, type, '' ]);
        }
        else if (typeof response === 'object') {
            this.server.respondWith(url, [ 200, type, JSON.stringify(response) ]);
        }
        else {
            this.server.respondWith(url, [ 200, type, response ]);
        }
    }
);

Then you simply use this as the module's fixture:

module('AJAX tests', ajaxFixture);

test(...);

Non-Mocking Solution

As you say in your comment, you want to execute the full AJAX call to your backend service.

Note: This is not unit testing anymore but rather functional or integration testing. Unit testing involves testing each small piece (unit) in isolation of the others: the click handler. Adding in jQuery, XMLHTTPRequest, and your backend service will make isolating bugs more difficult. But it is useful for making sure all the pieces work together.

I still recommend doing actual unit tests in addition to integration testing for quicker development as they typically run much faster and can be executed with every change to a source/test file from your IDE.

First, modifying your click handler invalidates any test you run, so we'll need to leave that in place, i.e., no simulated call or extra deferred. Your best bet is to poll for the call to complete with an interval and fail the test if it doesn't complete within X seconds.

test('foo text via AJAX set on body after clicking #foo', function(assert) {
    var $foo = $('#qunit-fixture').find('#foo'),
        $body = $('body'),
        done = assert.async(),
        timeout = Date.now() + 5000, // timeout after 5s
        timer;

    $foo.trigger('click');

    timer = window.setInterval(function () {
        if ($body.text() === 'foobar') {
            assert.equal($body.text(), 'foobar');
            window.clearInterval(timer);
            done();
        }
        else if (Date.now() >= timeout) {
            assert.ok(false, 'AJAX timed out');
            window.clearInterval(timer);
            done();
        }
    }, 250);
});

Obviously, this won't work if you're using Sinon's fake timers, so disable them for this test if enabled globally. Also, you'll need to figure out if a slow AJAX call will interfere with later tests when it finally completes and puts "foobar" into the body. Maybe you want to put it into the fixture itself instead of polluting the global body.

\$\endgroup\$
7
  • \$\begingroup\$ Except the obvious downsides of polling, I like your suggestion about setInterval. I've been thinking a bit back and forth whether if I should mock my ajax requests in general and just use arbitrary responses based on "real" API calls. What is your personal opinion regarding this? Thanks a lot for your feedback! \$\endgroup\$
    – Johan
    Commented Jan 31, 2015 at 17:49
  • \$\begingroup\$ I find mocking the AJAX response extremely useful and use it in all our JS unit tests. It allows us to run the unit tests both quickly (few seconds for all tests) and without an external server. We also run them in Jenkins on every commit. We've been building up our acceptance/integration tests using Codeception which uses WebDriver to control a browser and perform end-to-end testing. \$\endgroup\$ Commented Jan 31, 2015 at 19:59
  • \$\begingroup\$ I've added an example to illustrate using Sinon's fake server which we use rather than mocking jQuery itself. This lets you test failures and JSON responses more easily and enables testing a function that makes multiple different AJAX requests. \$\endgroup\$ Commented Jan 31, 2015 at 20:13
  • \$\begingroup\$ The only downside that I see with mocking requests is that your "test responses" that you use may become deprecated over time, e.g. an object property changes its name, the data structure is modified etc. How do you keep it up to date, when you don't have control over the API that you're communicating with? Thanks for the Sinon sample, it seems like a great addition to QUnit when you're dealing with AJAX. \$\endgroup\$
    – Johan
    Commented Feb 1, 2015 at 18:02
  • 1
    \$\begingroup\$ This is where integration tests come into play. They perform higher-level tests of the complete end-to-end system. Because they take orders of magnitude to run, they are less useful during development and more during staging and deployment. BTW, Sinon also has fake timers for testing timeouts and intervals; spies, stubs and mocks; and more assertions (though we use JsHamcrest for those). \$\endgroup\$ Commented Feb 2, 2015 at 1:48
1
\$\begingroup\$

I think that what you wrote is a bad unit test for the following reasons:

  1. Unit tests should not interact with the DOM and/or AJAX

    For example, you have a line of code in your unit test:

    assert.equal($('#bar').text(), 'bar');
    

    You should not test that jQuery will do it's job and you should not play around with the DOM (imagine hundreds of unit tests some of which change the DOM or setting up an automated unit test runs in an environment without a DOM, just a JavaScript engine). Instead of this, you should just check if a jQuery method was called that puts the 'bar' text into the $('#bar') element. Something like this (I'll use Jasmine but the concepts can be mapped to any other framework):

    var $bar = {
        text: function(){}
    };
    
    spyOn(someObj, 'getBar').and.returnValue($bar);
    spyOn($bar, 'text');
    
    // Call the method that you're testing, for example:
    // someObj.writeText();
    
    expect($bar.text).toHaveBeenCalledWith('bar')
    
  2. Unit tests should be lightning fast

    You have code that stops your unit tests for a second:

    setTimout(function(){
        $('body').text('foo');
            dfd.resolve();
        }, 1000);
    

    Now imagine that you have hundreds of unit tests. Will you be discouraged to run them?

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.