“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.
var myApp = angular.module('myApp',[]); | |
myApp.controller('FooController', ['$scope', 'fooService', function($scope, fooService) { | |
var instance = this; | |
$scope.loadingStatus = 'none'; | |
instance.onBarCompleted = function(){ | |
$scope.loadingStatus = 'completed'; | |
}; | |
instance.onBarError = function(){ | |
$scope.loadingStatus = 'error'; | |
}; | |
$scope.callBar = function() { | |
$scope.loadingStatus = 'loading…'; | |
fooService.bar() | |
.then(instance.onBarCompleted) | |
.catch(instance.onBarError); | |
} | |
}]); |
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!