the importance of scope.apply() when testing promises
“remember kids, Jasmine likes to apply”
…sorry, what?
Well, basically, it’s simple: when writing unit tests on promises with Jasmine, remember to call $scope.apply(), it will save you some headaches!
take a look at this AngularJs controller. Look at it.
<span class=pl-k>var</span> <span class=pl-s1>myApp</span> <span class=pl-c1>=</span> <span class=pl-s1>angular</span><span class=pl-kos>.</span><span class=pl-en>module</span><span class=pl-kos>(</span><span class=pl-s>'myApp'</span><span class=pl-kos>,</span><span class=pl-kos>[</span><span class=pl-kos>]</span><span class=pl-kos>)</span><span class=pl-kos>;</span> | |
<span class=pl-s1>myApp</span><span class=pl-kos>.</span><span class=pl-en>controller</span><span class=pl-kos>(</span><span class=pl-s>'FooController'</span><span class=pl-kos>,</span> <span class=pl-kos>[</span><span class=pl-s>'$scope'</span><span class=pl-kos>,</span> <span class=pl-s>'fooService'</span><span class=pl-kos>,</span> <span class=pl-k>function</span><span class=pl-kos>(</span><span class=pl-s1>$scope</span><span class=pl-kos>,</span> <span class=pl-s1>fooService</span><span class=pl-kos>)</span> <span class=pl-kos>{</span> | |
<span class=pl-k>var</span> <span class=pl-s1>instance</span> <span class=pl-c1>=</span> <span class=pl-smi>this</span><span class=pl-kos>;</span> | |
<span class=pl-s1>$scope</span><span class=pl-kos>.</span><span class=pl-c1>loadingStatus</span> <span class=pl-c1>=</span> <span class=pl-s>'none'</span><span class=pl-kos>;</span> | |
<span class=pl-s1>instance</span><span class=pl-kos>.</span><span class=pl-en>onBarCompleted</span> <span class=pl-c1>=</span> <span class=pl-k>function</span><span class=pl-kos>(</span><span class=pl-kos>)</span><span class=pl-kos>{</span> | |
<span class=pl-s1>$scope</span><span class=pl-kos>.</span><span class=pl-c1>loadingStatus</span> <span class=pl-c1>=</span> <span class=pl-s>'completed'</span><span class=pl-kos>;</span> | |
<span class=pl-kos>}</span><span class=pl-kos>;</span> | |
<span class=pl-s1>instance</span><span class=pl-kos>.</span><span class=pl-en>onBarError</span> <span class=pl-c1>=</span> <span class=pl-k>function</span><span class=pl-kos>(</span><span class=pl-kos>)</span><span class=pl-kos>{</span> | |
<span class=pl-s1>$scope</span><span class=pl-kos>.</span><span class=pl-c1>loadingStatus</span> <span class=pl-c1>=</span> <span class=pl-s>'error'</span><span class=pl-kos>;</span> | |
<span class=pl-kos>}</span><span class=pl-kos>;</span> | |
<span class=pl-s1>$scope</span><span class=pl-kos>.</span><span class=pl-en>callBar</span> <span class=pl-c1>=</span> <span class=pl-k>function</span><span class=pl-kos>(</span><span class=pl-kos>)</span> <span class=pl-kos>{</span> | |
<span class=pl-s1>$scope</span><span class=pl-kos>.</span><span class=pl-c1>loadingStatus</span> <span class=pl-c1>=</span> <span class=pl-s>'loading…'</span><span class=pl-kos>;</span> | |
<span class=pl-s1>fooService</span><span class=pl-kos>.</span><span class=pl-en>bar</span><span class=pl-kos>(</span><span class=pl-kos>)</span> | |
<span class=pl-kos>.</span><span class=pl-en>then</span><span class=pl-kos>(</span><span class=pl-s1>instance</span><span class=pl-kos>.</span><span class=pl-c1>onBarCompleted</span><span class=pl-kos>)</span> | |
<span class=pl-kos>.</span><span class=pl-en>catch</span><span class=pl-kos>(</span><span class=pl-s1>instance</span><span class=pl-kos>.</span><span class=pl-c1>onBarError</span><span class=pl-kos>)</span><span class=pl-kos>;</span> | |
<span class=pl-kos>}</span> | |
<span class=pl-kos>}</span><span class=pl-kos>]</span><span class=pl-kos>)</span><span class=pl-kos>;</span> |
as you can see, on line 18 there’s a call to fooService.bar() and two callbacks are used to handle the success and error cases.
Here instead, there’s an example of how you could test the error case:
var mockFooService, | |
sut; | |
describe('FooController tests', function () { | |
beforeEach(function($q, $controller){ | |
mockFooService = { | |
bar: function() { | |
return $q.reject({ data: { message: 'Error message' } }); | |
} | |
}; | |
var $scope = {}; | |
sut = $controller('FooController', { $scope: $scope, fooService: mockFooService }); | |
}); | |
it('callBar() should call onBarError() if an error is thrown', function () { | |
spyOn(sut, 'onBarError').andCallThrough(); | |
scope.callBar(); | |
scope.$apply(); // without the promise will not be evaluated | |
expect(sut.onBarError).toHaveBeenCalled(); | |
}); | |
}); |
in the beforeEach() block a mock service is created with a rejected promise (line 9) and on line 15 the controller is instantiated with the mocked dependencies.
On line 23 there’s the core of the test: a call to $scope.apply().
Without it the promise will not be resolved and any method chain will not be executed.
The reason is simple: the promises implementation is tied to the digest cycle, which is not handled by Jasmine. Calling $scope.apply() will update the internal status and take care of digest for you.
Cheers!