Getting "$digest already in progress" in async test with Jasmine 2.0 - angularjs

I know that calling $digest or $apply manually during a digest cycle will cause a "$digest already in progress" error but I have no idea why I am getting it here.
This is a unit test for a service that wraps $http, the service is simple enough, it just prevents making duplicate calls to the server while ensuring that code that attempts to do the calls still gets the data it expected.
angular.module('services')
.factory('httpService', ['$http', function($http) {
var pendingCalls = {};
var createKey = function(url, data, method) {
return method + url + JSON.stringify(data);
};
var send = function(url, data, method) {
var key = createKey(url, data, method);
if (pendingCalls[key]) {
return pendingCalls[key];
}
var promise = $http({
method: method,
url: url,
data: data
});
pendingCalls[key] = promise;
promise.then(function() {
delete pendingCalls[key];
});
return promise;
};
return {
post: function(url, data) {
return send(url, data, 'POST');
},
get: function(url, data) {
return send(url, data, 'GET');
},
_delete: function(url, data) {
return send(url, data, 'DELETE');
}
};
}]);
The unit-test is also pretty straight forward, it uses $httpBackend to expect the request.
it('does GET requests', function(done) {
$httpBackend.expectGET('/some/random/url').respond('The response');
service.get('/some/random/url').then(function(result) {
expect(result.data).toEqual('The response');
done();
});
$httpBackend.flush();
});
This blows up as sone as done() gets called with a "$digest already in progress" error. I've no idea why. I can solve this by wrapping done() in a timeout like this
setTimeout(function() { done() }, 1);
That means done() will get queued up and run after the $digest is done but while that solves my problem I want to know
Why is Angular in a digest-cycle in the first place?
Why does calling done() trigger this error?
I had the exact same test running green with Jasmine 1.3, this only happened after I upgraded to Jasmine 2.0 and rewrote the test to use the new async-syntax.

$httpBacked.flush() actually starts and completes a $digest() cycle. I spent all day yesterday digging into the source of ngResource and angular-mocks to get to the bottom of this, and still don't fully understand it.
As far as I can tell, the purpose of $httpBackend.flush() is to avoid the async structure above entirely. In other words, the syntax of it('should do something',function(done){}); and $httpBackend.flush() do not play nicely together. The very purpose of .flush() is to push through the pending async callbacks and then return. It is like one big done wrapper around all of your async callbacks.
So if I understood correctly (and it works for me now) the correct method would be to remove the done() processor when using $httpBackend.flush():
it('does GET requests', function() {
$httpBackend.expectGET('/some/random/url').respond('The response');
service.get('/some/random/url').then(function(result) {
expect(result.data).toEqual('The response');
});
$httpBackend.flush();
});
If you add console.log statements, you will find that all of the callbacks consistently happen during the flush() cycle:
it('does GET requests', function() {
$httpBackend.expectGET('/some/random/url').respond('The response');
console.log("pre-get");
service.get('/some/random/url').then(function(result) {
console.log("async callback begin");
expect(result.data).toEqual('The response');
console.log("async callback end");
});
console.log("pre-flush");
$httpBackend.flush();
console.log("post-flush");
});
Then the output will be:
pre-get
pre-flush
async callback begin
async callback end
post-flush
Every time. If you really want to see it, grab the scope and look at scope.$$phase
var scope;
beforeEach(function(){
inject(function($rootScope){
scope = $rootScope;
});
});
it('does GET requests', function() {
$httpBackend.expectGET('/some/random/url').respond('The response');
console.log("pre-get "+scope.$$phase);
service.get('/some/random/url').then(function(result) {
console.log("async callback begin "+scope.$$phase);
expect(result.data).toEqual('The response');
console.log("async callback end "+scope.$$phase);
});
console.log("pre-flush "+scope.$$phase);
$httpBackend.flush();
console.log("post-flush "+scope.$$phase);
});
And you will see the output:
pre-get undefined
pre-flush undefined
async callback begin $digest
async callback end $digest
post-flush undefined

#deitch is right, that $httpBacked.flush() triggers a digest. The problem is that when $httpBackend.verifyNoOutstandingExpectation(); is run after each it is completed it also has a digest. So here's the sequence of events:
you call flush() which triggers a digest
the then() is executed
the done() is executed
verifyNoOutstandingExpectation() is run which triggers a digest, but you are already in one so you get an error.
done() is still important since we need to know that the 'expects' within the then() are even executed. If the then doesn't run then you might now know there were failures. The key is to make sure the digest is complete before firing the done().
it('does GET requests', function(done) {
$httpBackend.expectGET('/some/random/url').respond('The response');
service.get('/some/random/url').then(function(result) {
expect(result.data).toEqual('The response');
setTimeout(done, 0); // run the done() after the current $digest is complete.
});
$httpBackend.flush();
});
Putting done() in a timeout will make it executes immediately after the current digest is complete(). This will ensure that all of the expects that you wanted to run will actually run.

Adding to #deitch's answer. To make the tests more robust you can add a spy before your callback. This should guarantee that your callback actually gets called.
it('does GET requests', function() {
var callback = jasmine.createSpy().and.callFake(function(result) {
expect(result.data).toEqual('The response');
});
$httpBackend.expectGET('/some/random/url').respond('The response');
service.get('/some/random/url').then(callback);
$httpBackend.flush();
expect(callback).toHaveBeenCalled();
});

Related

Jasmine unit test for angular service In controller

I'm new to jasmine framework. I've gone through some tutorials and learned and started writing unit tests. 'facing one issue here is the description.
I have a controller where i can invoke a service call to get the data. See the code below.
$scope.getEmpInfo = function() {
EmpService.getInfo($scope.empid)
.then(function(data) {
$scope.empData = data;
$scope.populateEmpData($scope.empData);
}, function(reason) {
//do nothing
}
}
Now, i want to write a unit test for the above method. Im able to make a spy on serice using promise but i wasnt able to spy $scope.populateEmpData(). here is my test case.
describe('Emp data', function() {
var d, scope;
beforeEach(function() {
module("emp");
module("emo.info");
});
describe('empcontroller', function() {
beforeEach(inject(function($q,_EmpService_, $controller,$rootScope){
d = $q.defer();
empService = _EmpService_;
spyOn(empService,"getInfo").and.returnValue(d.promise);
scope = $rootScope.$new();
empCtrl = $controller("empController", {
$scope: scope,
});
}));
it('should get the Employee information ', function() {
scope.getEmpInfo();
spyOn(scope,'populateEmpData');
expect(EmpService.getInfo).toHaveBeenCalled();
//Here im getting the error.
expect(scope.populateEmpData).toHaveBeenCalled();
});
});
});
Please help resolve this issue. Thanks in advance.
It's because you are not resolving promise. You will have to make change in spyOn.
- spyOn(empService,"getInfo").and.callFake(function() {
return {
then : function(success, error) {
success();
}
} }
Now, it will go into the success callback and will try to call $scope.populateEmpData();
You're never resolving your promise. And you need to call $scope.$apply().
Why is this necessary? Because any promises made with the $q service
to be resolved/rejected are processed upon each run of angular’s
digest cycle. Conceptually, the call to .resolve changes the state of
the promise and adds it to a queue. Each time angular’s digest cycle
runs, any outstanding promises will be processed and removed from the
queue.
Unit Testing with $q Promises in AngularJS
Check it out above link it will help you.

Angularjs Mocha test $q promises without $rootScope.$apply

I have this service:
angular.module('domeeApp')
.factory('streamWidget', streamWidgetFactory);
function streamWidgetFactory($q) {
return {
loadContent: function() {
return $q(function(resolve, reject) {
resolve('test');
})
}
}
}
I'm testing it with karma/mocha/chai:
describe('streamWidget', function() {
beforeEach(module('domeeApp'));
var streamWidget;
var $timeout;
beforeEach(inject(function(_$timeout_, _streamWidget_) {
streamWidget = _streamWidget_;
$timeout = _$timeout_;
}));
it('should load new content', function(done) {
streamWidget.loadContent()
.then(function(res) {
expect(res).to.equal('test');
done();
})
.catch(function(){})
$timeout.flush();
});
});
Since $q promises doesn't work well with mocha i'm following this answer, which says to add $timeout.flush() to force the .then method of the promise to be executed.
The problem is, after calling .flush(), all my app wakes up and i start to get this errors from angular-mocks:
Error: Unexpected request: GET /partials/page/view/index.
I know about $httpBackend, but it would be insane to mock ALL the requests my app is making on startup.
Is there a way to make $q promises work with mocha without calling $timeout.flush() or $rootScope.$apply()?
As shown here, chai-as-promised can be used to assert $q promises.
With this setup
chaiAsPromised.transferPromiseness = function (assertion, promise) {
assertion.then = promise.then.bind(promise);
if (!('$$state' in promise))
return;
inject(function ($rootScope) {
if (!$rootScope.$$phase)
$rootScope.$digest();
});
};
digest cycles will be triggered automatically on promise assertions, executing the whole promise chain.
In this case the spec
it('...', () => {
...
expect(...).to.eventually...;
expect(...).to.eventually...;
$rootScope.$digest();
});
can omit $digest() call and become
it('...', () => {
...
expect(...).to.eventually...;
expect(...).to.eventually...;
});
Notice that $q promises are synchronous, they shouldn't be returned from Mocha spec or call done callback.
Here's an alternative strategy that we use because we never actually need $httpBackend, but it sometimes (randomly) fails making requests for templates used by directives (even though those templates are available in $templateCache):
beforeEach(function() {
module('app', function($provide) {
// This is using jasmine, but the idea is the same with mocha.
// Basically just replace $httpBackend with a function that does nothing.
$provide.constant('$httpBackend', jasmine.createSpy('$httpBackend'));
});
});
Of course, if you actually use $httpBackend in other cases, then this won't work, as you'll need it to mock response objects.

chai-as-promised tests don't work with $q promises

I'm trying to get chai-as-promised to work with $q promises with karma unit tests.
svc.test = function(foo){
if (!foo){
// return Promise.reject(new Error('foo is required'));
return $q.reject(new Error('foo is required'));
} else {
// get data via ajax here
return $q.resolve({});
}
};
it.only('should error on no foo', function(){
var resolvedValue = MyServices.test();
$rootScope.$apply();
return resolvedValue.should.eventually.be.rejectedWith(TypeError, 'foo is required');
});
The unit test just times out. I am not sure what I'm doing wrong here to get the promise to resolve properly. It seems to be an issue with using $q -- when I use native Promise.reject() it works fine.
I filed a ticket here, but nobody seems to be responding:
https://github.com/domenic/chai-as-promised/issues/150
The way chai-as-promised expects to modify promise assertions is transferPromiseness method.
By default, the promises returned by Chai as Promised's assertions are
regular Chai assertion objects, extended with a single then method
derived from the input promise. To change this behavior, for instance
to output a promise with more useful sugar methods such as are found
in most promise libraries, you can override
chaiAsPromised.transferPromiseness.
For Angular 1.3+ support, $q promises can be duck-typed by $$state property, so native promises won't be affected:
chaiAsPromised.transferPromiseness = function (assertion, promise) {
assertion.then = promise.then.bind(promise);
if (!('$$state' in promise))
return;
inject(function ($rootScope) {
if (!$rootScope.$$phase)
$rootScope.$digest();
});
};
chaiAsPromised chains each asserted promise with then. Even if the promise is settled, the rest of the chain still requires the digest to be triggered manually with $rootScope.$digest().
As long as the spec contains no asynchronous code, it becomes synchronous, no promise is required to be returned:
it('...', () => {
...
expect(...).to.eventually...;
expect(...).to.eventually...;
});
And is equal to mandatory $rootScope.$digest() after each set of eventually assertions/expectation when transferPromiseness wasn't set:
it('...', () => {
...
expect(...).to.eventually...;
expect(...).to.eventually...;
$rootScope.$digest();
});
You need to change the order of execution in your tests. Asynchronous tasks with chai-as-promised need to happen before the expectation.
it('does not work', () => {
$timeout.flush();
expect(myAsyncTask()).to.eventually.become('foo');
})
it('does work', () => {
expect(myAsyncTask()).to.eventually.become('foo');
$timeout.flush();
})
You need to initiate the call to the asynchronous task before flushing the queue of asynchronous tasks.
Also, don't use $rootScope.$digest. That may have other side effects that are not desirable within your test(s).
$timeout.flush is what you're looking for.
https://docs.angularjs.org/api/ngMock/service/$timeout
To get your specific test(s) working:
it('should error on no foo', function(){
MyServices.test().should.eventually.be.rejectedWith(TypeError, 'foo is required')
$rootScope.$apply();
});
it('should pass on foo', function(){
MyServices.test('foo').should.eventually.become({});
$rootScope.$apply();
}
tl;dr
it('async test', () => {
setup();
expect();
execute();
})
it('sync test', () => {
setup();
execute();
expect();
})
Given the comments posted:
Should it be mentioned that it is unethical to downvote 'rival' answers on the question you're answering?
Fair enough. I think the answer is misleading, given that there is no extra setup necessary to get chai-as-promised working with Angular without having to deal with the done callback. Fwiw, I'll go ahead and try to revoke said downvote and be ethical about it.
The OP has no signs of timeout in his code and doesn't state that the task is asynchronous. $rootScope.$digest() has no side effects in specs when called outside of scope digest. The reason why it is not recommended in production is because it doesn't have the safeguards that $apply has.
$rootScope.$digest is effectively the same as $rootScope.$apply (and $scope.$apply for that matter). source
$timeout.flush will flush non-$timeout based functions just as well. It is not exclusive to $timeout based functions.
Plunker to showcase how it just works™:
plunker

chai-as-promised erroneously passes tests

I'm trying to write tests for a method that returns an angular promise ($q library).
I'm at a loss. I'm running tests using Karma, and I need to figure out how to confirm that the AccountSearchResult.validate() function returns a promise, confirm whether the promise was rejected or not, and inspect the object that is returned with the promise.
For example, the method being tested has the following (simplified):
.factory('AccountSearchResult', ['$q',
function($q) {
return {
validate: function(result) {
if (!result.accountFound) {
return $q.reject({
message: "That account or userID was not found"
});
}
else {
return $q.when(result);
}
}
};
}]);
I thought I could write a test like this:
it("it should return an object with a message property", function () {
promise = AccountSearchResult.validate({accountFound:false});
expect(promise).to.eventually.have.property("message"); // PASSES
});
That passes, but so does this (erroneously):
it("it should return an object with a message property", function () {
promise = AccountSearchResult.validate({accountFound:false});
expect(promise).to.eventually.have.property("I_DONT_EXIST"); // PASSES, should fail
});
I am trying to use the chai-as-promised 'eventually', but all my tests pass with false positives:
it("it should return an object", function () {
promise = AccountSearchResult.validate();
expect(promise).to.eventually.be.an('astronaut');
});
will pass. In looking at docs and SO questions, I have seen examples such as:
expect(promise).to.eventually.to.equal('something');
return promise.should.eventually.equal('something');
expect(promise).to.eventually.to.equal('something', "some message about expectation.");
expect(promise).to.eventually.to.equal('something').notify(done);
return assert.becomes(promise, "something", "message about assertion");
wrapping expectation in runs() block
wrapping expectation in setTimeout()
Using .should gives me Cannot read property 'eventually' of undefined. What am I missing?
#runTarm 's suggestions were both spot on, as it turns out. I believe that the root of the issue is that angular's $q library is tied up with angular's $digest cycle. So while calling $apply works, I believe that the reason it works is because $apply ends up calling $digest anyway. Typically I've thought of $apply() as a way to let angular know about something happening outside its world, and it didn't occur to me that in the context of testing, resolving a $q promise's .then()/.catch() might need to be pushed along before running the expectation, since $q is baked into angular directly. Alas.
I was able to get it working in 3 different ways, one with runs() blocks (and $digest/$apply), and 2 without runs() blocks (and $digest/$apply).
Providing an entire test is probably overkill, but in looking for the answer to this I found myself wishing people had posted how they injected / stubbed / setup services, and different expect syntaxes, so I'll post my entire test.
describe("AppAccountSearchService", function () {
var expect = chai.expect;
var $q,
authorization,
AccountSearchResult,
result,
promise,
authObj,
reasonObj,
$rootScope,
message;
beforeEach(module(
'authorization.services', // a dependency service I need to stub out
'app.account.search.services' // the service module I'm testing
));
beforeEach(inject(function (_$q_, _$rootScope_) {
$q = _$q_; // native angular service
$rootScope = _$rootScope_; // native angular service
}));
beforeEach(inject(function ($injector) {
// found in authorization.services
authObj = $injector.get('authObj');
authorization = $injector.get('authorization');
// found in app.account.search.services
AccountSearchResult = $injector.get('AccountSearchResult');
}));
// authObj set up
beforeEach(inject(function($injector) {
authObj.empAccess = false; // mocking out a specific value on this object
}));
// set up spies/stubs
beforeEach(function () {
sinon.stub(authorization, "isEmployeeAccount").returns(true);
});
describe("AccountSearchResult", function () {
describe("validate", function () {
describe("when the service says the account was not found", function() {
beforeEach(function () {
result = {
accountFound: false,
accountId: null
};
AccountSearchResult.validate(result)
.then(function() {
message = "PROMISE RESOLVED";
})
.catch(function(arg) {
message = "PROMISE REJECTED";
reasonObj = arg;
});
// USING APPLY... this was the 'magic' I needed
$rootScope.$apply();
});
it("should return an object", function () {
expect(reasonObj).to.be.an.object;
});
it("should have entered the 'catch' function", function () {
expect(message).to.equal("PROMISE REJECTED");
});
it("should return an object with a message property", function () {
expect(reasonObj).to.have.property("message");
});
// other tests...
});
describe("when the account ID was falsey", function() {
// example of using runs() blocks.
//Note that the first runs() content could be done in a beforeEach(), like above
it("should not have entered the 'then' function", function () {
// executes everything in this block first.
// $rootScope.apply() pushes promise resolution to the .then/.catch functions
runs(function() {
result = {
accountFound: true,
accountId: null
};
AccountSearchResult.validate(result)
.then(function() {
message = "PROMISE RESOLVED";
})
.catch(function(arg) {
reasonObj = arg;
message = "PROMISE REJECTED";
});
$rootScope.$apply();
});
// now that reasonObj has been populated in prior runs() bock, we can test it in this runs() block.
runs(function() {
expect(reasonObj).to.not.equal("PROMISE RESOLVED");
});
});
// more tests.....
});
describe("when the account is an employee account", function() {
describe("and the user does not have EmployeeAccess", function() {
beforeEach(function () {
result = {
accountFound: true,
accountId: "160515151"
};
AccountSearchResult.validate(result)
.then(function() {
message = "PROMISE RESOLVED";
})
.catch(function(arg) {
message = "PROMISE REJECTED";
reasonObj = arg;
});
// digest also works
$rootScope.$digest();
});
it("should return an object", function () {
expect(reasonObj).to.be.an.object;
});
// more tests ...
});
});
});
});
});
Now that I know the fix, it is obvious from reading the $q docs under the testing section, where it specifically says to call $rootScope.apply(). Since I was able to get it working with both $apply() and $digest(), I suspect that $digest is really what needs to be called, but in keeping with the docs, $apply() is probably 'best practice'.
Decent breakdown on $apply vs $digest.
Finally, the only mystery remaining to me is why the tests were passing by default. I know I was getting to the expectations (they were being run). So why would expect(promise).to.eventually.be.an('astronaut'); succeed? /shrug
Hope that helps. Thanks for the push in the right direction.

How to resolve promises in AngularJS, Jasmine 2.0 when there is no $scope to force a digest?

It seems that promises do not resolve in Angular/Jasmine tests unless you force a $scope.$digest(). This is silly IMO but fine, I have that working where applicable (controllers).
The situation I'm in now is I have a service which could care less about any scopes in the application, all it does it return some data from the server but the promise doesn't seem to be resolving.
app.service('myService', function($q) {
return {
getSomething: function() {
var deferred = $q.defer();
deferred.resolve('test');
return deferred.promise;
}
}
});
describe('Method: getSomething', function() {
// In this case the expect()s are never executed
it('should get something', function(done) {
var promise = myService.getSomething();
promise.then(function(resp) {
expect(resp).toBe('test');
expect(1).toEqual(2);
});
done();
});
// This throws an error because done() is never called.
// Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
it('should get something', function(done) {
var promise = myService.getSomething();
promise.then(function(resp) {
expect(resp).toBe('test');
expect(1).toEqual(2);
done();
});
});
});
What is the correct way to test this functionality?
Edit: Solution for reference. Apparently you are forced to inject and digest the $rootScope even if the service is not using it.
it('should get something', function($rootScope, done) {
var promise = myService.getSomething();
promise.then(function(resp) {
expect(resp).toBe('test');
});
$rootScope.$digest();
done();
});
You need to inject $rootScope in your test and trigger $digest on it.
there is always the $rootScope, use it
inject(function($rootScope){
myRootScope=$rootScope;
})
....
myRootScope.$digest();
So I have be struggling with this all afternoon. After reading this post, I too felt that there was something off with the answer;it turns out there is. None of the above answers give a clear explanation as to where and why to use $rootScope.$digest. So, here is what I came up with.
First off why? You need to use $rootScope.$digest whenever you are responding from a non-angular event or callback. This would include pure DOM events, jQuery events, and other 3rd party Promise libraries other than $q which is part of angular.
Secondly where? In your code, NOT your test. There is no need to inject $rootScope into your test, it is only needed in your actual angular service. That is where all of the above fail to make clear what the answer is, they show $rootScope.$digest as being called from the test.
I hope this helps the next person that comes a long that has is same issue.
Update
I deleted this post yesterday when it got voted down. Today I continued to have this problem trying to use the answers, graciously provided above. So, I standby my answer at the cost of reputation points, and as such , I am undeleting it.
This is what you need in event handlers that are non-angular, and you are using $q and trying to test with Jasmine.
something.on('ready', function(err) {
$rootScope.$apply(function(){deferred.resolve()});
});
Note that it may need to be wrapped in a $timeout in some case.
something.on('ready', function(err) {
$timeout(function(){
$rootScope.$apply(function(){deferred.resolve()});
});
});
One more note. In the original problem examples you are calling done at the wrong time. You need to call done inside of the then method (or the catch or finally), of the promise, after is resolves. You are calling it before the promise resolves, which is causing the it clause to terminate.
From the angular documentation.
https://docs.angularjs.org/api/ng/service/$q
it('should simulate promise', inject(function($q, $rootScope) {
var deferred = $q.defer();
var promise = deferred.promise;
var resolvedValue;
promise.then(function(value) { resolvedValue = value; });
expect(resolvedValue).toBeUndefined();
// Simulate resolving of promise
deferred.resolve(123);
// Note that the 'then' function does not get called synchronously.
// This is because we want the promise API to always be async, whether or not
// it got called synchronously or asynchronously.
expect(resolvedValue).toBeUndefined();
// Propagate promise resolution to 'then' functions using $apply().
$rootScope.$apply();
expect(resolvedValue).toEqual(123);
}));

Resources