This is my test:
it('add.user() should POST to /users/, failure', function() {
mockBackend.expectPOST("/users/", {username:'u', password: 'p', email: 'e', location: 'loc'}).respond(400, {msg: "bad request"});
BaseService.add.user({username:'u', password: 'p', email: 'e', location: 'loc'});
mockBackend.flush();
});
afterEach(function() {
mockBackend.verifyNoOutstandingExpectation();
mockBackend.verifyNoOutstandingRequest();
});
And when I run this test, I get this error:
Chromium 53.0.2785 (Ubuntu 0.0.0) Factory: BaseService add.user() should POST to /users/, failure FAILED
[object Object] thrown
Error: [$rootScope:inprog] $digest already in progress
http://errors.angularjs.org/1.3.15/$rootScope/inprog?p0=%24digest
at /home/user/Documents/ebdjango/ebdjangoapp/static/js/angular.js:63:12
at beginPhase (/home/user/Documents/ebdjango/ebdjangoapp/static/js/angular.js:14820:15)
at Scope.$digest (/home/user/Documents/ebdjango/ebdjangoapp/static/js/angular.js:14262:9)
at Function.$httpBackend.verifyNoOutstandingExpectation (node_modules/angular-mocks/angular-mocks.js:1557:38)
at Object.<anonymous> (tests/test_base.js:61:21)
This is BaseService.add.user()
:
self.add = {
user: function(user) {
return $http.post("/users/", user)
.then(function successHandler(response) {
return $http.post("/custom-api-auth/login", user)
}).then(function successHandler(response) {
$window.location.href = "/";
// if there are errors, rewrite the error messages
}).catch(function rejectHandler(errorResponse) {
for (prop in errorResponse.data) {
if (prop == "email") {
errorResponse.data[prop] = "Please enter a valid email address.";
} else if (prop == "username") {
errorResponse.data[prop] = "Username can only contain alphanumeric characters and '.'";
} else if (prop == "password") {
errorResponse.data[prop] = "Please enter a valid password";
}
}
throw errorResponse;
};
How do I prevent a $digest already in progress
error from occurring?
Edit: If I remove throw errorResponse;
, the test works But I need throw errorResponse;
there because I need to display the errors on the front end (which another controller takes care of.. BaseService.add.user().catch()
basically rewrites the errors which should be displayed on the front end).
Edit 2: When the error message says at Object.<anonymous> (tests/test_base.js:61:21)
it points to the line: mockBackend.verifyNoOutstandingExpectation();
The problem with the code above is that it throws inside catch
block. As opposed to other promise implementations, throwing and rejecting in $q is not the same thing.
Considering that $q promise chains are executed on digest ($httpBackend.flush()
here) synchronously, throwing inside catch
block will result in uncaught error, not in rejected promise. It likely prevents a digest from being completed and results in $digest already in progress error on next digest.
So generally $q.reject
should be used for expected errors in promises, while throw
should be used only for critical errors. It should be
return $q.reject(errorResponse);
It is recommended to use Jasmine promise matchers to test it:
expect(BaseService.add.user({ ... }).toBeRejectedWith({ ... });
Otherwise it has to be tested by more complicated promise chain:
var noError = new Error;
BaseService.add.user({ ... })
.then(() => $q.reject(noError))
.catch((err) => {
expect(err).not.toBe(noError);
expect(err).toEqual(...);
});
$rootScope.$digest();
Location changes should be stubbed in tests, because changing real location in tests is the last thing the one needs:
module({ $window: {
location: jasmine.spyObj(['href'])
} })
And it is preferable to use $location
service in Angular applications instead of accessing location
directly, unless proven otherwise, $location.path('/...')
instead of location.href = '/...'
.
It is already safe for using in tests, although $location.path
can be additionally spied for testing.