0

I've got a service that has the following method (among others), which returns an $http promise

  function sessionService($http, serviceRoot) {
    return {
        getAvailableDates: function () {
            return $http.get(serviceRoot + '/session/available_dates');
        }
    };
  };

  angular.module('app').service('sessionService', ['$http', 'serviceRoot', sessionService]);

And then another factory that kinda wraps it and caches/adds data to localStorage. This returns a regular promise

angular.module('app')
    .factory('AvailableDates', AvailableDates);

AvailableDates.$inject = ['sessionService', '$window', '$q'];

function AvailableDates(sessionService, $window, $q) {
    var availableDates = [];

    return {
        getAvailableDates: getAvailableDates
    };

    function getAvailableDates() {
        var deferred = $q.defer();
        var fromStorage = JSON.parse($window.sessionStorage.getItem('validDates'));

        if (availableDates.length > 0) {
            deferred.resolve(availableDates);
        } else if (fromStorage !== null) {
            deferred.resolve(fromStorage);
        } else {
            sessionService.getAvailableDates()
                .success(function (result) {
                    availableDates = result;
                    $window.sessionStorage.setItem('validDates', JSON.stringify(availableDates));
                    deferred.resolve(availableDates);
                });
        }
        return deferred.promise;
    }
}

This all works fine. My problem is I can't figure out how to test this thing while mocking the sessionService. I've read all the related stackoverflow questions, and tried all kinds of different things, to no avail.

Here's what my test currently looks like:

describe('testing AvailableDates factory', function () {
    var mock, service, rootScope, spy, window, sessionStorageSpy, $q;
    var dates = [ "2014-09-27", "2014-09-20", "2014-09-13", "2014-09-06", "2014-08-30" ];
    var result;

    beforeEach(module('app'));

    beforeEach(function() {
        return angular.mock.inject(function (_sessionService_, _AvailableDates_, _$rootScope_, _$window_, _$q_) {
            mock = _sessionService_;
            service = _AvailableDates_;
            rootScope = _$rootScope_;
            window = _$window_;
            $q = _$q_;
        });
    });

    beforeEach(inject(function () {
        // my service under test calls this service method
        spy = spyOn(mock, 'getAvailableDates').and.callFake(function () {
            return {
                success: function () {
                    return [ "2014-09-27", "2014-09-20", "2014-09-13", "2014-09-06", "2014-08-30" ];
                },
                error: function() {
                    return "error";
                }
            };
        });

        spyOn(window.sessionStorage, "getItem").and.callThrough();
    }));

    beforeEach(function() {
        service.getAvailableDates().then(function(data) {
            result = data;
            // use done() here??
        });
    });

    it('first call to fetch available dates hits sessionService and returns dates from the service', function () {
        rootScope.$apply(); // ??

        console.log(result); // this is printing undefined

        expect(spy).toHaveBeenCalled();  // this passes
        expect(window.sessionStorage.getItem).toHaveBeenCalled(); // this passes
    });
});

I've tried various things but can't figure out how to test the result of the AvailableDates.getAvailableDates() call. When I use done(), I get the error: Timeout - Async callback was not invoked withing timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL (I've tried overriding this value, no luck).

If I take out the done(), and just call rootScope.$apply() after the .then is called, I get an undefined value as my result.

What am I doing wrong?

3
  • Not sure but probably store the promise in a variable too (thePromise), then call thePromise.done() instead of rootScope.$apply() in your test Commented Jul 3, 2015 at 20:40
  • done() (in my test) refers to jasmine's done() jasmine.github.io/2.2/… I'm using angular's promises library $q docs.angularjs.org/api/ng/service/$q which has a then() call instead of jquery's done() call Commented Jul 3, 2015 at 20:48
  • To further confuse things, angular's $http library returns a special kind of promise (which has .success() and .error callbacks). Commented Jul 3, 2015 at 20:51

1 Answer 1

2

I see more issues in your example.

The main problem is the success definition in the mock. Success is a function, which has a function as a parameter - callback. Callback is called when data is received - data is passed as the first argument.

return {
    success: function (callback) {
        callback(dates);
    }
};

Simplified working example is here http://plnkr.co/edit/Tj2TZDWPkzjYhsuSM0u3?p=preview

In this example, mock is passed to the provider with the module function (from ngMock) - you can pass the object with a key (service name) and value (implementation). That implementation will be used for injection.

module({
      sessionService:sessionServiceMock
});

I think test logic should be in one function (test), split it into beforeEach and test is not a good solution. Test is my example; it's more readable and has clearly separated parts - arrange, act, assert.

inject(function (AvailableDates) {
    AvailableDates.getAvailableDates().then(function(data) {
      expect(data).toEqual(dates);
      done();
    });

    rootScope.$apply(); // promises are resolved/dispatched only on next $digest cycle

    expect(sessionServiceMock.getAvailableDates).toHaveBeenCalled();
    expect(window.sessionStorage.getItem).toHaveBeenCalled();
  });
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.