Thursday, March 28, 2024

Test Asynchronous Methods Using the Jasmine runs() and waitFor() Methods

In the Testing Ajax Event Handlers using Jasmine Spies article, we saw how the use of Jasmine spies is an excellent way to test asynchronous Ajax calls where we don’t care about the practical implementation of the HttpXmlRequest or the network connection. However, there are times when that is precisely what you want to verify! In those instances, a better approach is to use the Jasmine runs() and waitFor() methods, which were both created especially for that purpose. In today’s article, we’ll take a look at some practical examples.

The runs() and waitFor() Methods Described

Both the runs() and waitFor() methods work in tandem in the testing of asynchronous processes.

Calls to run() methods act as individual testing blocks, the last of which would make the asynchronous call. In the simplest case, a setTimeout() can be used to simulate asynchronous behavior. In a more complex usage, an actual call would be made, such as to an Ajax-enabled object.

The waitsFor() method accepts three arguments:

  • a “latch” function
  • a failure message
  • a timeout (in milliseconds)

A latch function is not the same as a callback; whereas a callback executes once the asynchronous call returns, the latch function executes in 10ms intervals until it returns true or the timeout expires. In the event of a timeout, the spec fails with the provided error message. Hence, if the runs() function returns anything falsy, that does not terminate the test; it continues until the set timeout, which would be five seconds for the following example:

it("should simulate an asynchronous call", function () {
  runs(function() {
    flag = false;
    value = 0;

    setTimeout(function() {
      value++;
      //keep returning false…
      flag = false;
    }, 500);
  }); 

  waitsFor(function() {
    return flag;
  }, "The Value should be incremented", 5000); 
});
Ajax Tests Using run() and waitsFor() should simulate an asynchronous call.

timeout: timed out after 5000 msec waiting for The Value should be incremented

Once the asynchronous conditions have been met and waitsFor() returns true, another runs() block defines the final test behavior. This is usually expectations based on state after the asynchronous call returns. In the next example, setInterval() is utilized to mimic several call attempts, whereby the third one succeeds. Since the waitsFor() timeout is sufficiently generous, the test will pass. However, the final call to runs() will fail because value will equal 3 and not 4 as tested:

it("should simulate an asynchronous call", function () {
  runs(function() {
    flag = false;
    value = 0;
    intId = setInterval(function() {
      console.log(value);
      if (++value == 3) { clearInterval(intId); flag = true; }
    }, 500);
  }); 

  waitsFor(function() {
    return flag;
  }, "The Value should be incremented", 5000); 
  
  runs(function() {
    expect(value).toEqual(4); //this will fail
  });
});
Expected 3 to equal 4.

A shortfall of the above approach is its reliance on global variables. The absence of the var declaration keyword means that the flag, value, and intId variables are all created at the window level. A better solution is to declare the variables within the describe() method and then use the beforeEach() and afterEach() setup and teardown methods to initialize the variables and cancel setInterval(). In that case, the run() is not even required:

describe("Ajax Tests Using run() and waitsFor()", function() {
  var flag, value, intId;

  beforeEach(function() {
    flag = false,
    value = 0,
    intId = setInterval(function() {
      console.log(value);
      if (++value == 3) { flag = true; }
    }, 500);
  });

  afterEach(function() {
    clearInterval(intId); 
  });

  it("should simulate an asynchronous call", function () {
    waitsFor(function() {
      return flag;
    }, "The Value should be incremented", 5000); 
  });
});

Jasmine will call the runs() and waitFor() methods in the order you passed them. As soon as the JS parser gets to a waitFor() method it will poll it until it returns true and only then will it continue onto the next runs() method:

it("should simulate an asynchronous call", function () {
    waitsFor(function() {
      return value == 3;
    }, "The Value should be incremented", 5000); 
    
    runs(function() {
      expect(value).toEqual(3);
    });
    
    waitsFor(function() {
      return flag;
    }, "The Value should be incremented", 5000); 
    
    runs(function() {
      expect(flag).toEqual(true);
    });
  });

Making Real Ajax Requests

When it comes time to perform integration testing, you’ll probably want to execute real Ajax calls rather than just mimic them. No problem. Just create a spy, make the Ajax call, followed by waitsFor(), where you can check the callback.callCount property or what-have-you:

it("should make a real AJAX request", function () {
    var callback = jasmine.createSpy();
    makeAjaxCall(callback);
    waitsFor(function() {
        return callback.callCount > 0;
    }, "The Ajax call timed out.", 5000);
    
    runs(function() {
        expect(callback).toHaveBeenCalled();
    });
});

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

Conclusion

Believe it or not, the jasmine testing framework is not straight-forward enough for some people, one of which is Derick Bailey. In an effort to make asynchronous processes even easier to test with jasmine he developed the Jasmine.Async library. Has he succeeded? Tune in later to find out…

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