68

I'm having hard times trying to test promise-based code in Angularjs.

I have the following code in my controller:

    $scope.markAsDone = function(taskId) {
        tasksService.removeAndGetNext(taskId).then(function(nextTask) {
            goTo(nextTask);
        })
    };

    function goTo(nextTask) {
        $location.path(...);
    }

I'd like to unit-test the following cases:

  • when markAsDone is called it should call tasksService.removeAndGetNext
  • when tasksService.removeAndGetNext is done it should change location (invoke goTo)

It seems to me that there is no easy way to test those two cases separately.

What I did to test the first one was:

var noopPromise= {then: function() {}}
spyOn(tasksService, 'removeAndGetNext').andReturn(noopPromise);

Now to test the second case I need to create another fake promise that would be always resolved. It's all quite tedious and it's a lot of boilerplate code.

Is there any other way to test such things? Or does my design smell?

nephiw
  • 1,964
  • 18
  • 38
Michal Ostruszka
  • 2,089
  • 2
  • 20
  • 23
  • The accepted solution did not work for me. This did: http://stackoverflow.com/questions/21895684/how-do-i-unit-test-an-angularjs-controller-that-relies-on-a-promise?rq=1 – sibidiba Apr 17 '14 at 04:42

3 Answers3

109

You will still need to mock the services and return a promise, but you should use real promises instead, so you don't need to implement its functionality. Use beforeEach to create the already fulfilled promise and mock the service if you need it to ALWAYS be resolved.

var $rootScope;

beforeEach(inject(function(_$rootScope_, $q) {
  $rootScope = _$rootScope_;

  var deferred = $q.defer();
  deferred.resolve('somevalue'); //  always resolved, you can do it from your spec

  // jasmine 2.0
  spyOn(tasksService, 'removeAndGetNext').and.returnValue(deferred.promise); 

  // jasmine 1.3
  //spyOn(tasksService, 'removeAndGetNext').andReturn(deferred.promise); 

}));

If you'd rather prefer to resolve it in each it block with a different value, then you just expose the deferred to a local variable and resolve it in the spec.

Of course, you would keep your tests as they are, but here is some really simple spec to show you how it would work.

it ('should test receive the fulfilled promise', function() {
  var result;

  tasksService.removeAndGetNext().then(function(returnFromPromise) {
    result = returnFromPromise;
  });

  $rootScope.$apply(); // promises are resolved/dispatched only on next $digest cycle
  expect(result).toBe('somevalue');
});
Chui Tey
  • 5,436
  • 2
  • 35
  • 44
Caio Cunha
  • 23,326
  • 6
  • 78
  • 74
  • can you explain the purpose of $rootScope = _$rootScope_; in your first example? – aamiri Sep 20 '13 at 13:23
  • 2
    What I'm doing is injecting the `$rootScope` (if you insert a _ before and after, it will be ignored) and exposing it through a local variable. This way you don't need to inject it into each separately spec, as all of them are probably going to use it. – Caio Cunha Sep 20 '13 at 20:31
  • 13
    +1 I was missing this $rootScope.$apply() - very helpful thanks – Mike Jan 23 '14 at 11:07
  • @CaioToOn Is there some way to dispatch a promise without applying the scope? – Federico Nafria Feb 05 '14 at 00:46
  • 3
    For the record, the whole `var deferred = $q.defer(); deferred.resolve('something'); returnValue(deferred.promise)` can be reduced to `returnValue($q.when('something'))` – charlespwd Dec 21 '15 at 18:41
4

Another approach would be the following, taken straight out of a controller I was testing:

var create_mock_promise_resolves = function (data) {
    return { then: function (resolve) { return resolve(data); };
};

var create_mock_promise_rejects = function (data) {
    return { then: function (resolve, reject) { if (typeof reject === 'function') { return resolve(data); } };
};

var create_noop_promise = function (data) {
    return { then: function () { return this; } };
};
yangmillstheory
  • 1,055
  • 13
  • 31
  • 2
    Your promise mock is going to run the resolve() or reject() functions synchronously, but in AngularJs, it's always done asynchronously. So I wouldn't recommend this solution because it doesn't accurately test the code. – Kayhadrin Jun 13 '14 at 03:17
  • 1
    In unit tests, you want the ability to run asynchronous code synchronously. This is why services like `$httpBackend` are automatically mocked out in the Angular testing environment. – yangmillstheory Jun 16 '14 at 13:14
  • Actually, I totally see your point. These promises will automatically flush themselves. You want the ability to flush them on command. – yangmillstheory Jun 16 '14 at 13:22
0

And as yet another option you can avoid having to call $digestby using the Q library (https://github.com/kriskowal/q) as a drop-in replacement for $q e.g:

beforeEach(function () {
    module('Module', function ($provide) {
        $provide.value('$q', Q); 
    });
});

This way promises can be resolved/rejected outside of the $digest cycle.

Michael
  • 2,258
  • 1
  • 23
  • 31