Jasmine test for Angular service does not resolve deferred call
Asked Answered
B

1

6

I'm pretty new to Angular and I am working to test an Angular service that runs off an API level service which wraps a bunch of calls to a REST service. Because it is working with HTTP requests, both parts of the service are working with promises and it seems to work fine, but I am having trouble getting any tests of promised behaviour working.

The relevant part of my service code ( grossly simplified ) looks like this:

angular.module('my.info')
 .service('myInfoService', function (infoApi, $q) {
    infoLoaded: false,
    allInfo: [],
    getInfo: function () {
        var defer = $q.defer();
        if (infoLoaded) {
            defer.resolve(allInfo);
        } else {
            infoApi.getAllInfo().then(function (newInfo) {
                allInfo = newInfo;
                infoLoaded = true;
                defer.resolve(allInfo);
            });
        }
        return defer.promise;
    }
});

When I am setting up my mock I have something like this:

   describe("Info Service ",
     function() {
        var infoService, infoRequestApi, $q;
        beforeEach(module("my.info"));
        beforeEach(function() {
            module(function($provide) {
                infoRequestApi = {
                   requestCount: 0,
                   getAllInfo: function() {
                       var defer = $q.defer(); 
                       this.requestCount++;
                       defer.resolve( [ "info 1", "info 2" ] );
                       return defer.promise;
                   }
                };
                $provide.value("infoApi", infoRequestApi);
            });
            inject(function( _myInfoService_, _$q_ ) {
                 infoService = _myInfoService_,
                 $q = _$q_;
            });
        });

        it("should not fail in the middle of a test", function(done) {
            infoService.getInfo().then( function(infoResult) {
                  // expectation checks.
                  expect( true ).toBeTrue();
              }).finally(done);
        });
   });

Any synchronous tests pass fine, but when I try to run any tests like this I get a message saying: Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.

It seems as though something about the way that Angular.Mocks handles the deferred result is causing it to fail. When I step through the test, the mock object's defer variable is being set correctly, but the then statement in the service is never called. Where am I going wrong?

Benilda answered 31/8, 2016 at 17:9 Comment(0)
E
4

Short Answer

You have to kick off a digest cycle. You can do that with $rootScope.$apply().

it("should not fail in the middle of a test", inject(function($rootScope) {
  var actualResult = null;
  infoService.getInfo()
    .then(function(infoResult) { actualResult = infoResult; });
  $rootScope.$apply();
  expect(actualResult).toEqual(["info 1", "info 2"]);
}));

Note

I wouldn't try to create a fake infoRequestApi service the way you are doing it. You should be injecting that service as well, and spying on its functions. For instance, something like this:

it("should not fail in the middle of a test", inject(function($rootScope, infoApi, $q) {
  var deferred = $q.defer();
  spyOn(infoApi, 'getAllInfo').and.returnValue(deferred.promise);
  var actualResult = null;
  var expectedResult = ["info 1", "info 2"];

  infoService.getInfo()
    .then(function(infoResult) { actualResult = infoResult; });

  deferred.resolve(expectedResult);
  $rootScope.$apply();
  expect(actualResult).toBe(expectedResult);
}));

Refactored

Also, your code can be simplified a bit (untested, but close to what I would expect to see).

angular.module('my.info')
  .service('myInfoService', function (infoApi, $q) {
     return {
       infoLoaded: false,
       allInfo: [],
       getInfo: function () {
         var self = this;
         return this.infoLoaded ? $q.resolve(this.allInfo) :
           infoApi.getAllInfo().then(function (newInfo) {
             self.allInfo = newInfo;
             self.infoLoaded = true;
           });
     }
  };
});

describe("Info Service ", function() {
  var infoService, infoApi, $q, $rootScope;

  beforeEach(module("my.info"));
  beforeEach(inject(function( _myInfoService_, _$q_ _$rootScope_, _infoApi_) {
    infoService = _myInfoService_,
    $q = _$q_;
    $rootScope = _$rootScope_;
    infoApi = _infoApi_;
  });

  it("should do something", function() { // update message
    var deferred = $q.defer();
    spyOn(infoApi, 'getAllInfo').and.returnValue(deferred.promise);
    var actualResult = null;
    var expectedResult = ["info 1", "info 2"];

    infoService.getInfo()
      .then(function(infoResult) { actualResult = infoResult; });

    deferred.resolve(expectedResult);
    $rootScope.$apply();
    expect(actualResult).toBe(expectedResult);
  });
});
Eschalot answered 31/8, 2016 at 17:39 Comment(3)
Thank you double, both for the good answer and the refactoring suggestions. I suspected that mocking approach was a little long-winded! The only problem I have now is that it seems like if I have a couple of service layers it looks as though I need to inject a mock for every constructor right the way down, but this is much tidier and more idiomatic.Benilda
@Benilda No problem. You only need to mock the dependencies that are used in the service under test. You don't need to mock their dependencies, so the question of layers is moot.Eschalot
You are correct, I was actually running into a problem with the Chutzpah test runner for Visual Studio, which was causing spurious failures. The same tests work fine under other test runners.Benilda

© 2022 - 2024 — McMap. All rights reserved.