Luciano Fiandesio
Luciano Fiandesio

Reputation: 10215

Mocking nested function returning Deferred in Jasmine

I'm trying to write a Jasmine test for a module that uses a jQuery ajax wrapper library named jquery.rest

This module under test:

var module = function() {

  function getData(callback) {

    IP.read().done(function (data) {
        console.log("done");
        callback(data);
    });
  }

  return {
    getData: getData
  }

}();

The client and IP variables are declared in a different file, like so:

var client = new $.RestClient('/rest/api/');
var IP = client.add('ip');

I would like to mock the read() function so that it would return a Json payload that I define in my test. The read() method returns a $.Deferred object.

I have tried different approaches (using Jasmine spies) but without success.

Upvotes: 0

Views: 959

Answers (1)

try-catch-finally
try-catch-finally

Reputation: 7634

I see two ways to do this:

  1. spy $.ajax() and call fake function that returns your own deferred

    Contra: you indirectly test the library

  2. mock $.RestClients interface and return your own deferred

    Contra: more work to mock the library when not only testing the callback is required. (The more complicated your mocking the more error prone your test is.)


TL;DR Skip this if this is known.

But first let's look at how RestClient works... It has two basic objects, a Resource and a Verb. The RestClient is actually a Resource object (1). Resource objects will return another Resource object when add()ing a rest fragment (2). The pre-defined verb read will return a Verb instance's call method (3).

  1. https://github.com/jpillora/jquery.rest/blob/gh-pages/dist/jquery.rest.js#L382
  2. https://github.com/jpillora/jquery.rest/blob/gh-pages/dist/jquery.rest.js#L241
  3. https://github.com/jpillora/jquery.rest/blob/gh-pages/dist/jquery.rest.js#L245

From the bottom to the top of that chain a request method is accessible from the call() Method (4). If not explicitly overridden it defaults to $.ajax(). (5)

  1. https://github.com/jpillora/jquery.rest/blob/gh-pages/dist/jquery.rest.js#L174
  2. https://github.com/jpillora/jquery.rest/blob/gh-pages/dist/jquery.rest.js#L66

If not configured differently, a call to read() will result in a call to $.ajax(), returning a promise.

So, when doing new new $.RestClient().add("...").add("...").read() you'll get what you'd get with $.ajax().


Variant 1:

describe("getData()", function(){
        // Handle to ajax()' deferred, scoped to the
        // describe so the fake ajax() and the it()
        // have access to it
    var def,
        underTest;

    beforeEach(function(){
        // Mock $.ajax (or what a Verb calls in the end)
        // assign "def" with a deferred and return it,
        // the test can then resolve() or reject() it
        spyOn($, "ajax").and.callFake(function(opts) {
            def = $.Deferred();
            return def;
        });

        // This is under test
        underTest = new UnderTest();
    });

    afterEach(function(){
        // Ensure a missing call of ajax() will fail the test
        def = null;
    });

    it("should call callback on successful read", function() {
        var callback = jasmine.createSpy("callback");
        // Indirectly call ajax() which will create def
        underTest.getData(callback);
        // Resolve the deferred to succeed the response
        def.resolve({a: 1});
        expect(callback).toHaveBeenCalledWith({a: 1});
    });

    it("should not call callback on failed read", function(){
        var callback = jasmine.createSpy("callback");
        underTest.getData(callback);
        def.reject();
        expect(callback).not.toHaveBeenCalled();
    });
});

The fake is returning a deferred, not a promise, but in this case it's OK since it has the same interface, and nothing/no one should reject or resolve the deferred here except us.

Variant 2:

describe("getData()", function(){
        // Store original reference
    var origRestClient,
        // See first code block
        def,
        underTest;

    // Mock thr Resouce object
    function MockResource() { }

    // Simplify the behaviour of this mock,
    // return another Mock resource
    MockResource.prototype.add = function() {
        return new MockResource();
    };

    // What Verb.call() would do, but directly
    // return a deferred
    MockResource.prototype.read = function() {
        def = $.Deferred();
        return def;
    };

    beforeEach(function(){
        // Replace RestClient
        origRestClient = $.RestClient;
        $.RestClient = MockResource;

        underTest = new UnderTest();
    });

    afterEach(function(){
        // Restore RestClient
        $.RestClient = origRestClient;
        def = null;
    });

    it("should call callback on successful read", function() {
        var callback = jasmine.createSpy("callback");
        underTest.getData(callback);
        def.resolve({a: 1});
        expect(callback).toHaveBeenCalledWith({a: 1});
    });

    it("should not call callback on failed read", function(){
        var callback = jasmine.createSpy("callback");
        underTest.getData(callback);
        def.reject();
        expect(callback).not.toHaveBeenCalled();
    });
});

The mocking of Resouce needs more work than what I've done if you'd like to test the path and request data too, with the above code this is not possible.

Upvotes: 1

Related Questions