0

I am using Jasmine 2.5.2 to write unit tests for code that performs Ajax requests using jQuery 3.1.1 . I would like to mock out the Ajax call, providing my own response status and text.

I am using the Jasmine ajax plug-in (https://github.com/pivotal/jasmine-ajax).

Following the example on https://jasmine.github.io/2.0/ajax.html, which uses the XMLHttpRequest object, works fine.

describe("mocking ajax", function() {
    describe("suite wide usage", function() {
        beforeEach(function() {
            jasmine.Ajax.install();
        });

        afterEach(function() {
            jasmine.Ajax.uninstall();
        });

        it("specifying response when you need it", function() {
            var doneFn = jasmine.createSpy("success");

            var xhr = new XMLHttpRequest();
            xhr.onreadystatechange = function(args) {
                if (this.readyState == this.DONE) {
                    doneFn(this.responseText);
                }
            };

            xhr.open("GET", "/some/cool/url");
            xhr.send();
            expect(jasmine.Ajax.requests.mostRecent().url).toBe('/some/cool/url');
            expect(doneFn).not.toHaveBeenCalled();

            jasmine.Ajax.requests.mostRecent().respondWith({
                "status": 200,
                "contentType": 'text/plain',
                "responseText": 'awesome response'
            });
            expect(doneFn).toHaveBeenCalledWith('awesome response');                      
        });
    });
});

NB: this differs slightly from the documented example, had to change jasmine.Ajax.requests.mostRecent().response() to jasmine.Ajax.requests.mostRecent().respondWith().

When I use jQuery Ajax, the doneFn is never called.

describe("mocking ajax", function() {
    describe("suite wide usage", function() {
        beforeEach(function() {
            jasmine.Ajax.install();
        });

        afterEach(function() {
            jasmine.Ajax.uninstall();
        });

        it("specifying response when you need it", function() {
            var doneFn = jasmine.createSpy("success");

            $.ajax({
                method: "GET",            
                url: "/some/cool/url"})
            .done(function(result) {
                doneFn(result);
            });
            expect(doneFn).toHaveBeenCalledWith('awesome response');                      
        });
    });
});

Jasmine states that

Jasmine-Ajax mocks out your request at the XMLHttpRequest object, so should be compatible with other libraries that do ajax requests.

$.ajax returns a jqXHR from 1.4.x and not XMLHttpRequest - does this break support with Jasmine Ajax?

Lozzer
  • 155
  • 2
  • 12

3 Answers3

8

Here are a couple of ways you could mock ajax in your jasmine tests

Approach A:

  • Using mock ajax.js, you can mock the global xhr object as described in the doc
  • However you'll need to specify a spy on the success function and let it callThrough (as seen in the code below)
  • See it in action here

    describe('ajax test suite', function() {
    
      beforeEach(function() {
        jasmine.Ajax.install();
      });
    
      afterEach(function() {
        jasmine.Ajax.uninstall();
      });
    
      it('sample test', function() {
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function(args) {
          if (this.readyState == this.DONE) {
            testObj.successFunction(this.responseText);
          }
        };
        spyOn(testObj, 'successFunction').and.callThrough();
        xhr.open("GET", "https://jsonplaceholder.typicode.com/posts/1");
        xhr.send();
        expect(jasmine.Ajax.requests.mostRecent().url).toBe('https://jsonplaceholder.typicode.com/posts/1');
        expect(testObj.successFunction).not.toHaveBeenCalled();
        jasmine.Ajax.requests.mostRecent().respondWith({
          "status": 200,
          "contentType": 'text/plain',
          "responseText": 'awesome response'
        });
        expect(testObj.successFunction).toHaveBeenCalledWith('awesome response');
      });
    });
    

Approach B:

  • Directly mocking the $.ajax object. Be it get/post/load or any ajax flavor from jquery, you can simply mock the $.ajax as shown below.

    var testObj = {
      ajaxFunction : function(url){     
       $.ajax({url : url}).done(this.successFunction.bind(this));
      },
      successFunction : function(data){
       console.log(data);
     }
    }
    
    describe('ajax test suite', function(){
        it('sample test', function(){
         testObj.ajaxFunction('https://jsonplaceholder.typicode.com/posts/1');
         spyOn($, 'ajax').and.callFake(function(e) {
              return $.Deferred().resolve({'text':'this a a fake response'}).promise();
         });
         spyOn(testObj, 'successFunction').and.callThrough();
         testObj.ajaxFunction('https://jsonplaceholder.typicode.com/posts/1');
         expect(testObj.successFunction).toHaveBeenCalledWith({'text':'this a a fake response'});
      });
    });
    
danronmoon
  • 3,814
  • 5
  • 34
  • 56
Winter Soldier
  • 2,607
  • 3
  • 14
  • 18
  • Your second approach worked for me here. The fake can also return `$.Deferred().reject(obj)` to test an Ajax error, where obj has a responseText property with the error text. – Lozzer Mar 08 '17 at 11:02
  • @Lozzer that is correct. With deferred you can either resolve/reject based on your requirement. – Winter Soldier Mar 08 '17 at 16:38
  • What if we have other jQuery AJAX calls that we want, or need, to call through in our test? I'm starting to feel like this is the only solution that will work, and I'm not at all happy about it. If I'm just going to mock the entire $.ajax function then what's the point of using jasmine.Ajax in the first place? – endemic Mar 23 '18 at 20:11
1

When mocking the $.ajax object, the parameter passed in can be used directly to trigger a success or failure, without going through the promise interface.

spyOn($, 'ajax').and.callFake(function (options) {
    var result = undefined, status, xhr;
    options.success(result, status, xhr);
});

spyOn($, 'ajax').and.callFake(function (options) {
    var xhr = undefined, status, error;
    options.error(xhr, status, error);
});
Lozzer
  • 155
  • 2
  • 12
0

In addition to danronmoon answer(Approach B):

I added options.success(testData) inside spyOn($, 'ajax') to trigger this.processResponseData

Fn to test:

 var objUnderTest = {};
 objUnderTest.makeRequest(requestUrl) {
    $.ajax(requestUrl)
      .then(response => {
        this.processResponseData(response.data[0]) // get data from first array item
      })
  }

Test:

  describe('objUnderTest.makeRequest', () => {
    const testData = {data: [{'text': 'this a a fake response'}]};

    let processResponseData;
    beforeAll(() => {
      spyOn($, 'ajax').and.callFake(function (options) {
        options.success(testData)
        return $.Deferred().resolve(testData).promise();
      });
      processResponseData = spyOn(objUnderTest, 'processResponseData')
    })

    it('should make ajax call', () => {
      objUnderTest.makeRequest({success: stockTicker.processResponseData});

      expect(processResponseData).toHaveBeenCalled();
    });
  });
Pavlo Oliinyk
  • 380
  • 4
  • 10