3

I would like to test this function:

    function initializeView() {
        var deferred = $q.defer();
        if(this.momentArray) {
            core.listMoments(constants.BEST_MOMENT_PREFIX, '').then(function(moments) {
                //Ommitted
                deferred.resolve(moments);      
            }, function(error) {
                console.log("ERROR");
                deferred.reject(error);
            });
        }
        else {
            deferred.resolve();
        }
        return deferred.promise;    
    };

The function calls core.listMoments:

    function listMoments(prefix, startAfter) {
        // var deferred = $q.defer();
        var promises = [];
        return awsServices.getMoments(prefix, startAfter).then(function(moments) { //Mocked
            console.log("getMoments Returned"); //Does not print
            for(var i = 0; i < moments.length; i++) {
                // moments[i].Key = constants.IMAGE_URL + moments[i].Key;
                promises.push(getMomentMetaData(moments[i]));
            }
        return $q.all(promises);
        });
    };

Here is my test function:

it('Should correctly initialize the view', function(done) {
    spyOn(awsServices, 'getMoments').and.callFake(function() {
        console.log("getMoments Has been mocked"); //This prints
        return $q.resolve(mock_moment);
    });
    service.initializeView().then(function() {
        done();
    })
});

The problem is with the awsServices 'getMoments' mock. The call to awsServices.getMoments is in the listMoments function. I would like to mock out this function but when I do it does not execute the "then" part of the promise.

So based on my console logs it would print the 'getMoments Has been mocked' log but it would not print 'getMoments Returned' log. So the function is mocked but for some reason it is not moving into the then statement and my test just times out.

MatTaNg
  • 923
  • 8
  • 23
  • 41
  • `getMoments` will return `mock_moments`, but inside `.then` of `getMoments` in `listMoments` function it will pass each moment data to `getMomentMetaData`(I suspect you also need to mock this method, if it is doing actual ajax). – Pankaj Parkar Sep 04 '17 at 03:27
  • Your right I do need to mock that but it's not even getting to it right now. – MatTaNg Sep 04 '17 at 04:49
  • You could directly mock the `core.listMoments` to validate the behavior of `initializeView` function only. And test `listMoments` separately. – dpellier Sep 04 '17 at 09:07
  • I could, I would like to make this an integration test though. – MatTaNg Sep 04 '17 at 16:26

2 Answers2

6

In order to get the .then() part of a promise to work in such a test, you need to use a $rootScope.$apply(). This is needed whether the promise is in your test code or in a referenced library that is being tested. Think of it like the flush() function for $http or $timeout calls.

The Testing example from the Angular documentation's $q page shows how to use it:

it('should simulate promise', inject(function($q, $rootScope) {
  var deferred = $q.defer();
  var promise = deferred.promise;
  var resolvedValue;

  promise.then(function(value) { resolvedValue = value; });
  expect(resolvedValue).toBeUndefined();

  // Simulate resolving of promise
  deferred.resolve(123);
  // Note that the 'then' function does not get called synchronously.
  // This is because we want the promise API to always be async, whether or not
  // it got called synchronously or asynchronously.
  expect(resolvedValue).toBeUndefined();

  // Propagate promise resolution to 'then' functions using $apply().
  $rootScope.$apply();
  expect(resolvedValue).toEqual(123);
}));

Note that they inject $rootScope.

C.C.
  • 565
  • 2
  • 7
  • Your situation is similar to in this other question as well: https://stackoverflow.com/questions/23705051/how-do-i-mock-a-service-that-returns-promise-in-angularjs-jasmine-unit-test?rq=1 – C.C. Sep 11 '17 at 21:49
1

$q promises can be synchronous (when they are resolved synchronously) and depend on digest cycles.

There should generally be no asynchronous done callback in Angular tests.

Angular tests are supposed to be synchronous, so are $q promises. In order to achieve that a digest should be triggered manually when an existing promise (the ones that is returned from getMoments and initializeView) is chained with then. If done callback is placed inside then and a digest is not triggered, this will result in spec timeout.

spyOn(awsServices, 'getMoments').and.callFake(function() {
    console.log("getMoments Has been mocked"); //This prints
    return $q.resolve(mock_moment);
});
service.initializeView();
$rootScope.$digest();

The thing that can be improved here is isolation. There are several units (methods) involved in a single test. This will affect troubleshooting when one of them fails.

Usually unit testing implies that only one unit is tested at time, while the rest are mocked or stubbed. In this case in one test service.listMoments is called and awsServices.getMoments is mocked, and in another test service.initializeView is called and service.listMoments is mocked.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565