Sunday, September 15, 2024

Testing Asynchronous jQuery Event Code Using the Jasmine-jQuery Library

The jasmine-jquery library, which was featured in my recent Accessing Anonymous Functions for Testing Using the Jasmine-jQuery Library article, provides two extensions for the Jasmine JavaScript Testing Framework:

  • a set of custom matchers for jQuery framework
  • an API for loading HTML, CSS, and JSON fixtures in your specs asynchronously using Ajax

There is another library called jasmine-ajax that can fake Ajax responses in your Jasmine suite. I’m looking forward to giving it a try shortly. In the meantime, I’d like to share a few ways that I use to fake jQuery Ajax calls using only Jasmine 2.0, jQuery, and even plain ole vanilla JavaScript. In the last article we exposed functions and event handlers within jQuery.ready() in order to test them. This tutorial will pick up from where we left off and present a few ways to test jQuery Ajax Success handler code triggered by an anonymous button click handler.

Using Jasmine Spies

I’ve written previously about testing jQuery Ajax events in the Testing Ajax Event Handlers Using Jasmine Spies article. Here’s an example that is based on code from that article that demonstrates the use of spies to test that a named success handler was invoked by an Ajax call:

it("should receive a successful response", function() {
    spyOn($, "ajax").andCallFake(function(req) {
        req.success('Menus added successfully.');
    });
 
    var checkForInformation = jasmine.createSpy(), 
        displayErrorMessage = jasmine.createSpy();
 
    $.ajax({
        url: 'acme.com',
        success: checkForInformation,
        error: displayErrorMessage
    });

    expect(checkForInformation).toHaveBeenCalled();      //Verifies this was called
    expect(displayErrorMessage).not.toHaveBeenCalled();  //Verifies this was NOT called
});

Having established that the success handler was indeed invoked, we would then test that the checkForInformation() function does what it should.

Invoking an Anonymous Ajax Event Handler

Another scenario includes one where the success handler is defined as an anonymous function:

$.ajax({
    url: 'acme.com',
    success: function(data) {
      //do stuff with data…
    }
});

How can we execute the success handler if we can’t reference it?

One solution is available to us courtesy of the spy’s calls property, which tracks and exposes every call to the spied function. By spying on $.ajax, Jasmine will silently track calls to it, while preventing the original $.ajax from executing. Invoking the function or event that contains the Ajax call in turn calls our $.ajax spy. We can then proceed to test the success handler:

spyOn($, 'ajax'); 
//invoke the button click handler that contains the ajax call
$('#btnSaveTop').trigger('click');
//call the most recent success method
$.ajax.calls.mostRecent().args[0].success('Menus added successfully.');
//test the results…

The Spy instance’s .calls.mostRecent() function returns the context (the this) of the last call to our spy. It in turn has an args property that contains the arguments for the most recent call. In our case that would be the Ajax request. Therefore, $.ajax.calls.mostRecent().args[0].success() invokes the anonymous success handler.

A Better Ajax Simulation

The problem with the above solutions is that they are missing the asynchronous aspect of Ajax calls. Depending on your code, that could produce tests that are not representative of real-life performance. I have seen some people use Promises, but they still require the addition of an artificial delay. In my opinion, it’s easier to simply include a setTimeout() inside your fake Ajax call. Just bear in mind that you have to test spied functions within the scope of the setTimeout() function. Otherwise, your tests won’t see the spied functions as having been executed!

it("should execute the callback function on success", function (done) {
  spyOn($, 'ajax').and.callFake(function(req) {
    var successMsg = 'Menus added successfully.';
    setTimeout(function() {
        //invoke the success() handler
        req.success( successMsg ); 
        //check in with our spy
        expect(window.displayAdminMessage).toHaveBeenCalled();
        expect(window.displayAdminMessage.calls.argsFor(0))
          .toEqual([successMsg,'green','top']);
        done();
    }, 500);
  });
  var spyEvent = spyOnEvent('button[type="button"][id^="btnSave"]', 'click');
  spyOn(window, 'displayAdminMessage').and.callThrough();
  $('#btnSaveTop').trigger('click');
  expect('click').toHaveBeenTriggeredOn('button[type="button"][id^="btnSave"]');
  expect(spyEvent).toHaveBeenTriggered();
});

Using a Custom Callback Function

If you don’t care to track Ajax calls, you can even override $.ajax yourself to omit the server call by intercepting the options object. Note that you can still spy on your wrapped $.ajax() function if you wish, and now you’ve got yourself some AOP-style before and after reporting to boot:

// creates a new callback function that also executes the original callback
var SuccessCallback = function(origCallback){
   return function(data, textStatus, jqXHR) {
    console.log("Calling original callback function…");
    if (typeof origCallback === "function") {
      origCallback('Menus successfully saved.', textStatus, jqXHR);
    }
    console.log("Finished executing original callback function.");
  };
};

beforeEach(function() {
  setFixtures( window.content );
  appendSetFixtures( window.readyScript );
  
  $.ajax = function(req){
    // override the callback function, then execute the original AJAX function
    settings.success = new SuccessCallback(settings.success);
    return req.success();
  };
  spyOn($, 'ajax').and.callThrough();
});

This solution is obviously more work, but it gives you more control over the proceedings.

Testing Ajax Request Parameters

As mentioned above, every call to a spy is tracked and exposed via the calls property. Through it we can check all of the request options passed to $.ajax, including the request method, action, and data.

Our final example demonstrates the testing of Ajax options as well as the final state of the menus form. If the $.Ajax success handler performed as it should, it should move the form fields under the “Add New Menu” button to the end of the “Euston” restaurant section.

Here’s a screenshot of the menu form before:

…and after the Save button’s click event:

The following code snippet incorporates both those tests. In the demo, they are split into two separate tests:

it("should submit all of the form fields via ajax and move new fields to the destination restaurant section.", function() {
  $('#btnSaveTop').trigger('click');
  
  var request    = $.ajax.calls.mostRecent().args[0],
      data       = request.data,
      formFields = data.menuData.deserialize();
      
  expect(request.method).toEqual('POST');
  expect(data.action).toEqual('save_menus');
  
  var fieldNames = Object.keys(formFields);
  //each menu has 2 fields, the name and URL
  expect(fieldNames.length/2).toEqual(10);
  
  //new fields are menu_name_1174100_2 and menu_url_1174100_2
  expect(fieldNames).toContain("menu_name_1174100_2");
  expect(formFields.menu_name_1174100_2).toEqual("new menu name 1");
  expect(fieldNames).toContain("menu_url_1174100_2");
  expect(formFields.menu_url_1174100_2).toEqual("http://new.url.1");
});

Conclusion

All of the code presented here today is up on Codepen so you can try it out and see all the code at work. In a future article, we’ll see how the jasmine-ajax library compares.

Rob Gravelle
Rob Gravelle
Rob Gravelle resides in Ottawa, Canada, and has been an IT guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured