Benjamin Benjamin - 5 months ago 14
jQuery Question

Testing a function that is called from an jQuery AJAX callback with Jasmine

I have a function that makes an AJAX call to a service. I'm attempting to expect that the displayError function is called on a failure.

I have my function ajaxCall that accepts a url. Upon success I pass the result to displaySuccess and when there's an error I pass the details to displayError.

function ajaxCall(url) {
$.ajax({
method: "GET",
url: url,
data: "json",
beforeSend: function (xhr) {
//Do Stuff
},
error: function(xhr, textStatus, errorThrown) { displayError(xhr, textStatus, errorThrow, url)},
success: function (results) { displaySuccess(result) }
});
}

function displayError(xhr, textStatus, errorThrow, url) {
//Do Stuff//
}
function displaySuccess(results) {
//Do Stuff//
}


In Jasmine I have it successfully verifying the URL. My problem is in testing to insure that the displayError and displaySuccess functions are called.

I have the following for this specific issue so far.

describe('The ajaxCall component', function() {
it('should call the error function when the ajax call fails', function () {
var obj = {};
spyOn(obj, 'displayError');

spyOn($, "ajax").and.callFake(function (options) {
options.error();

});


ajaxCall('/myResource/get');
expect(obj.method).toHaveBeenCalled();
});
}


I'm a little new to unit testing and I've searched trying to find suggestions that would help but they make the unit test fail. Where am I going wrong with this?

Answer

This all boils down to how you spy on your objects and writing code that is more testable. Let's work through a few strategies.

Strategy 1

Given your current code is not within an object, you could test that these functions are called by simply testing their implementation directly.

Instead of testing that the functions were called, you would test their implementation directly.

Example

describe("strategy 1", function () {
    var ajaxSpy;

    beforeEach(function () {
        ajaxSpy = spyOn($, 'ajax');

        ajaxCall();
    });

    describe("error callback", function () {
        beforeEach(function() {
            spyOn(window, 'alert');

            var settings = ajaxSpy.calls.mostRecent().args[0];
            settings.error();
        });

        describe("when there is an error", function() {
            it("should alert an error message", function() {
                expect(window.alert).toHaveBeenCalledWith('Error');
            });
        });
    });
});

Strategy 2

While the above works, it can be cumbersome to write tests. Ideally, you want to test the invocation and implementation separately.

To do so, we can spy on these functions. Since these are in the global namespace, you can spy on them through the window object.

Example

describe("strategy 2", function () {
    var ajaxSpy;

    beforeEach(function () {
        ajaxSpy = spyOn($, 'ajax');

        ajaxCall();
    });

    describe("error callback", function () {
        beforeEach(function() {
            spyOn(window, 'displayError');

            var settings = ajaxSpy.calls.mostRecent().args[0];
            settings.error();
        });

        describe("when there is an error", function() {
            it("should alert an error message", function() {
                expect(window.displayError).toHaveBeenCalled();
            });
        });
    });
});

Strategy 3 (Recommended)

The final strategy, and what I recommend, has a similar setup to the second strategy, except we encapsulate our implementation into a custom object.

Doing so makes the code more testable by wrapping functionality in objects and avoids the global namespace (i.e. window).

Example

describe("solution 3", function() {
    var ajaxSpy;

    beforeEach(function() {
        ajaxSpy = spyOn($, 'ajax');

        ajaxService.ajaxCall();
    });

    describe("error callback", function() {
        beforeEach(function() {
            spyOn(ajaxService, 'displayError');

            var settings = ajaxSpy.calls.mostRecent().args[0];
            settings.error();
        });

        it("should alert an error message", function() {
            expect(ajaxService.displayError).toHaveBeenCalled();
        });
    });
});