简体   繁体   中英

AngularJS : testing a factory that returns a promise, while mocking a service that uses $http

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?

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();
  });

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM