How to test the done and fail Deferred Object by using jasmine

19,546

Solution 1

We actually ran into the same problem, trying to test Deferred objects that represent AJAXed template scripts for on-the-fly templating. Our testing solution involves using the Jasmine-Ajax library in conjunction with Jasmine itself.

So probably it will be something like this:

describe('When Submit button handler fired', function () {
  jasmine.Ajax.useMock();

  describe('if the message is empty', function () {

    beforeEach(function() {
      spyOn(backendController, 'submitForm').andCallThrough();
      // replace with wherever your callbacks are defined
      spyOn(this, 'onSuccess');
      spyOn(this, 'onFailure');
      this.view.$el.find('#message').text('');
      this.view.$el.find('form').submit();
    });

    it('backendController.submitForm and fail Deferred Object should be called', function () {
      expect(backendController.submitForm).toHaveBeenCalledWith('');
      mostRecentAjaxRequest().response({
        status: 500, // or whatever response code you want
        responseText: ''
      });

      expect( this.onSuccess ).not.toHaveBeenCalled();
      expect( this.onFailure ).toHaveBeenCalled();
    });
});

Another thing, if you can, try to break up the functionality so you're not testing the entire DOM-to-response-callback path in one test. If you're granular enough, you can actually test asynchronous Deferred resolutions by using Deferred objects themselves inside your tests!

The key is to actually use Deferred objects within your tests themselves, so that the scope of the expect call is still within your it function block.

describe('loadTemplate', function() {
  it('passes back the response text', function() {
    jasmine.Ajax.mock();
    loadTemplate('template-request').done(function(response) {
      expect(response).toBe('foobar');
    });
    mostRecentAjaxRequest().response({ status:200, responseText:'foobar' });
  });
});

Solution 2

Here is how I managed to do it.

Essentially, the $.ajax object returns a Deferred object, so you can spy on $.ajax and return a Deferred and then trigger it manually to run the .done() code in your JavaScript

Code

Index.prototype.sendPositions = function() {
  var me = this;
  $.ajax({
    ...
  }).done(function(data, textStatus, jqXHR) {
    me.reload();
  }).fail(function(jqXHR, textStatus, errorThrown) {
    console.log(errorThrown);
  });
};

Test

it("should reload the page after a successful ajax call", function(){
  var deferred = new jQuery.Deferred();
  spyOn($, 'ajax').andReturn(deferred);
  spyOn(indexPage, 'reload');
  indexPage.sendPositions();
  deferred.resolve('test');
  expect(indexPage.reload).toHaveBeenCalled();
});

Solution 3

It would be much easier to test if you had a var with the ajax request promise object. In that case you could do:

 it('should do an async thing', function() {     
   var mutex = 1;
   var promF = jasmine.createSpy('prF');

   runs( function() {
     var promise1 = $.ajax();
     promise1.always(function(){
       mutex--;
     });
     promise1.fail(function(){
       promF();
     });
   });

   waitsFor(function(){
     return !mutex;
   }, 'Fetch should end', 10000);

   runs( function() {
      expect(promF).toHaveBeenCalled();
   });
 });

Below I post untested code that might suit you. I suppose that the ajax call is initialized from the .submit() class? Maybe you should initialize the ajax request from a runs() block and not from beforeEach(), but you should try which one works.

describe('When Submit button handler fired and city is defined', function () {
    var ajaxRequestSpy,
        failSpy, successSpy, alwaysSpy,
        mutex;
    beforeEach(function () {
        ajaxRequestSpy = spyOn(backendController, 'ajaxRequest').andCallThrough();
        failSpy = spyOn(ajaxRequestSpy(), 'fail').andCallThrough()
        successSpy = spyOn(ajaxRequestSpy(), 'success').andCallThrough();
        mutex = 1; // num of expected ajax queries
        alwaysSpy =  spyOn(ajaxRequestSpy(), 'always').andCallFake(function() {
             mutex--;
        });
        this.view = new MyView({
            el: $('<div><form>' +
                '<input type="submit" value="Submit" />' +
                '<input type="text" name="city">' +
                '</form></div>')
        });
        this.view.$el.find('form').submit();
    });
    it('backendController.ajaxRequest should be called', function () {
        runs( function() {
            // maybe init ajax here ?   
        });

        waitsFor( function() {
            return !mutex;
        }, 'ajax request should happen', 5000);

        runs( function() {
            expect(ajaxRequestSpy).toHaveBeenCalled(); // true
            expect(failSpy).toHaveBeenCalled(); // Error: Expected spy fail 
                                            // to have been called.
        });

    });
});

But, I am not sure that the line

failSpy = spyOn(ajaxRequestSpy(), 'fail').andCallThrough();

does what you want. Is it possible to spy on another spy? And if yes why are you calling the spy ? Maybe you should try

failSpy = spyOn(ajaxRequestSpy, 'fail').andCallThrough();
Share:
19,546
Lorraine Bernard
Author by

Lorraine Bernard

Updated on June 12, 2022

Comments

  • Lorraine Bernard
    Lorraine Bernard almost 2 years

    Here is the code about the javascript submit request (1).
    Here is the test about mocking the ajax request by using jasmine (2).

    I would like to mock the server behaviour. Any ideas?
    See the comment in (1) and (2) for more details.

    P.S.:
    Actually in both case the done and the fail Deferred Object of fakeFunction are called.


    (1)

    submitForm: function () {
         // the server execute fail only if message.val() is empty
         // and I would like to mock this behaviour in (2)
         backendController.submitForm(message.val()).done(this.onSuccess).fail(this.onError);
    },
    
    backendController.submitForm = function (message) {
        return $.ajax({
            url: 'some url',
            type: 'POST',
            dataType: 'json',
            data: {
                message: message
            }
        }).done(function () {
            //some code;
        });
    };
    

    (2)

    describe('When Submit button handler fired', function () {
        var submitFormSpy,
            fakeFunction = function () {
                this.done = function () {
                    return this;
                };
                this.fail = function () {
                    return this;
                };
                return this;
            };
    
        beforeEach(function () {
            submitFormSpy = spyOn(backendController, 'submitForm').andCallFake(fakeFunction);
        });
    
        describe('if the message is empty', function () {
            beforeEach(function () {
                this.view.$el.find('#message').text('');
                this.view.$el.find('form').submit();
            });
            it('backendController.submitForm and fail Deferred Object should be called', function () {
                expect(submitFormSpy).toHaveBeenCalled();
                // how should I test that fail Deferred Object is called?
            });
        });
    
        describe('if the message is not empty', function () {
            beforeEach(function () {
                this.view.$el.find('#message').text('some text');
                this.view.$el.find('form').submit();
            });
            it('backendController.submitForm should be called and the fail Deferred Object should be not called', function () {
                expect(submitFormSpy).toHaveBeenCalled();
                // how should I test that fail Deferred Object is not called?
            });
        });
    
    });