How do I verify jQuery AJAX events with Jasmine?

60,892

Solution 1

I guess there are two types of tests you can do:

  1. Unit tests that fake the AJAX request (using Jasmine's spies), enabling you to test all of your code that runs just before the AJAX request, and just afterwards. You can even use Jasmine to fake a response from the server. These tests would be faster - and they would not need to handle asynchronous behaviour - since there isn't any real AJAX going on.
  2. Integration tests that perform real AJAX requests. These would need to be asynchronous.

Jasmine can help you do both kinds of tests.

Here is a sample of how you can fake the AJAX request, and then write a unit test to verify that the faked AJAX request was going to the correct URL:

it("should make an AJAX request to the correct URL", function() {
    spyOn($, "ajax");
    getProduct(123);
    expect($.ajax.mostRecentCall.args[0]["url"]).toEqual("/products/123");
});

function getProduct(id) {
    $.ajax({
        type: "GET",
        url: "/products/" + id,
        contentType: "application/json; charset=utf-8",
        dataType: "json"
    });
}

For Jasmine 2.0 use instead:

expect($.ajax.calls.mostRecent().args[0]["url"]).toEqual("/products/123");

as noted in this answer

Here is a similar unit test that verifies your callback was executed, upon an AJAX request completing successfully:

it("should execute the callback function on success", function () {
    spyOn($, "ajax").andCallFake(function(options) {
        options.success();
    });
    var callback = jasmine.createSpy();
    getProduct(123, callback);
    expect(callback).toHaveBeenCalled();
});

function getProduct(id, callback) {
    $.ajax({
        type: "GET",
        url: "/products/" + id,
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        success: callback
    });
}

For Jasmine 2.0 use instead:

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

as noted in this answer

Finally, you have hinted elsewhere that you might want to write integration tests that make real AJAX requests - for integration purposes. This can be done using Jasmine's asyncronous features: waits(), waitsFor() and runs():

it("should make a real AJAX request", function () {
    var callback = jasmine.createSpy();
    getProduct(123, callback);
    waitsFor(function() {
        return callback.callCount > 0;
    });
    runs(function() {
        expect(callback).toHaveBeenCalled();
    });
});

function getProduct(id, callback) {
    $.ajax({
        type: "GET",
        url: "data.json",
        contentType: "application/json; charset=utf-8"
        dataType: "json",
        success: callback
    });
}

Solution 2

Look at the jasmine-ajax project: http://github.com/pivotal/jasmine-ajax.

It's a drop-in helper that (for either jQuery or Prototype.js) stubs at the XHR layer so that requests never go out. You can then expect all you want about the request.

Then it lets you provide fixture responses for all your cases and then write tests for each response that you want: success, failure, unauthorized, etc.

It takes Ajax calls out of the realm of asynchronous tests and provides you a lot of flexibility for testing how your actual response handlers should work.

Solution 3

here is a simple example test suite for an app js like this

var app = {
               fire: function(url, sfn, efn) {
                   $.ajax({
                       url:url,
                       success:sfn,
                       error:efn
                   });
                }
         };

a sample test suite, which will call callback based on url regexp

describe("ajax calls returns", function() {
 var successFn, errorFn;
 beforeEach(function () {
    successFn = jasmine.createSpy("successFn");
    errorFn = jasmine.createSpy("errorFn");
    jQuery.ajax = spyOn(jQuery, "ajax").andCallFake(
      function (options) {
          if(/.*success.*/.test(options.url)) {
              options.success();
          } else {
              options.error();
          }
      }
    );
 });

 it("success", function () {
     app.fire("success/url", successFn, errorFn);
     expect(successFn).toHaveBeenCalled();
 });

 it("error response", function () {
     app.fire("error/url", successFn, errorFn);
     expect(errorFn).toHaveBeenCalled();
 });
});

Solution 4

When I specify ajax code with Jasmine, I solve the problem by spying on whatever depended-on function initiates the remote call (like, say, $.get or $ajax). Then I retrieve the callbacks set on it and test them discretely.

Here's an example I gisted recently:

https://gist.github.com/946704

Share:
60,892

Related videos on Youtube

mnacos
Author by

mnacos

Updated on July 08, 2022

Comments

  • mnacos
    mnacos almost 2 years

    I am trying to use Jasmine to write some BDD specs for basic jQuery AJAX requests. I am currently using Jasmine in standalone mode (i.e. through SpecRunner.html). I have configured SpecRunner to load jquery and other .js files. Any ideas why the following doesn't work? has_returned does not become true, even thought the "yuppi!" alert shows up fine.

    describe("A jQuery ajax request should be able to fetch...", function() {
    
      it("an XML file from the filesystem", function() {
        $.ajax_get_xml_request = { has_returned : false };  
        // initiating the AJAX request
        $.ajax({ type: "GET", url: "addressbook_files/addressbookxml.xml", dataType: "xml",
                 success: function(xml) { alert("yuppi!"); $.ajax_get_xml_request.has_returned = true; } }); 
        // waiting for has_returned to become true (timeout: 3s)
        waitsFor(function() { $.ajax_get_xml_request.has_returned; }, "the JQuery AJAX GET to return", 3000);
        // TODO: other tests might check size of XML file, whether it is valid XML
        expect($.ajax_get_xml_request.has_returned).toEqual(true);
      }); 
    
    });
    

    How do I test that the callback has been called? Any pointers to blogs/material related to testing async jQuery with Jasmine will be greatly appreciated.

  • mnacos
    mnacos over 13 years
    Thanks @jasminebdd, the jasmine-ajax project looks like the way to go for testing my js code. But what if I wanted to test with actual requests to the server, e.g. for connectivity/integration tests?
  • Sebastien Martin
    Sebastien Martin about 12 years
    @mnacos jasmine-ajax is mostly useful for unit testing in which case you specifically want to avoid the call to the server. If you're doing integration tests, this is probably not what you want and should be designed as a separate test strategy.
  • Lorraine Bernard
    Lorraine Bernard over 11 years
    +1 Alex great answer. Actually I faced a similar issue for which I have opened a question Test Ajax request using the Deferred object. Could you take a look? thanks.
  • Darren Newton
    Darren Newton over 11 years
    I really wish your answer was part of the official documentation for Jasmine. Very clear answer to a problem that has been killing me for a few days.
  • Steve Hansell
    Steve Hansell over 11 years
    Excellent answer and example. Thank you.
  • Thomas Amar
    Thomas Amar almost 11 years
    This answer should really be marked as the official answer to this question.
  • João Paulo Motta
    João Paulo Motta about 9 years
    Thanks dude. This example helped me a lot! Just note that if you are using Jasmine > 2.0 the syntax for andCallFake is spyOn(jQuery, "ajax").and.callFake( /* your function */ );
  • CSS
    CSS almost 9 years
    It's a great answer, but it doesn't quite work for testing functions that make calls to functions that fire AJAX requests. Is there a good suggestion for pushing it out a level of nested functions without firing the AJAX request? I also have a question out about this at stackoverflow.com/questions/31975928/…. Thanks, C§
  • camiblanch
    camiblanch almost 9 years
    The current way to get the most recent call information is $.ajax.calls.mostRecent()
  • Watchmaker
    Watchmaker over 7 years
    How would you implement that for plain JS ajax?
  • O.O
    O.O over 5 years
    Not sure if this is a version issue but I got an error on .andCallFake, used .and.callFake instead. Thanks.