the importance of scope.apply() when testing promises
the importance of scope.apply() when testing promises

the importance of scope.apply() when testing promises

2015, Nov 10    

“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>
</p>
</p>

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

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!