I am attempting to test some asynchronous JavaScript (that was once TypeScript) using Jasmine. I've had a hard time getting this to work correctly, and with this simple example it never makes it to the then(function( code block and I get the following error:
Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
My test looks something like:
it("Should work", function(done){
dataService.ready = true;
dataService.isReady().then(function(result){
console.log(result);
expect(result).toBe(true);
done();
});
});
And the service I'm testing looks something like (before compiled to JavaScript):
public isReady(): angular.IPromise<any> {
var deferred = this.q.defer();
if (this.ready) {
setTimeout(() => { return deferred.resolve(true); }, 1);
} else {
// a bunch of other stuff that eventually returns a promise
}
return deferred.promise;
}
I am sure I am just misusing done() but I feel like this should work! Any suggestions?
UPDATE:
For further debugging, I added a few console logs within the isReady() function. It now looks like:
public isReady(): angular.IPromise<any> {
var deferred = this.q.defer();
console.log("in isReady()"); // new line to add logging
if (this.ready) {
console.log("this.ready is true"); // new line to add logging
setTimeout(() => {
console.log("returning deferred.resolve"); // new line to add logging
return deferred.resolve(true);
}, 1);
} else {
// a bunch of other stuff that eventually returns a promise
}
return deferred.promise;
}
isReady() works as expected when I manually test in a browser. When running the test, my logs include:
LOG: 'in isReady()'
LOG: 'this.ready is true'
LOG: 'returning deferred.resolve'
Within my test, it appears to never be resolved (code block within then() is never executed) but when running my app this function works just fine. This example is in a controller:
DataService.isReady().then(() => {
console.log("I work!");
});
UPDATE: And more debugging...
In my test:
it("Should work", function(done){
console.log("calling dataService.isReady()");
var prom = dataService.isReady();
console.log("promise before");
console.log(prom);
setTimeout(function(){
console.log("promise after");
console.log(prom);
},1000);
prom.then(function(result){
// never makes it here
done();
}, function(reason) {
// never makes it here either
});
}
Now, in my console, I see:
LOG: 'calling dataService.isReady()'
LOG: 'in isReady()'
LOG: 'this.ready is true'
LOG: 'promise before'
LOG: Object{$$state: Object{status: 0}}
LOG: 'returning deferred.resolve'
LOG: 'promise after'
LOG: Object{$$state: Object{status: 1, pending: [...], value: true, processScheduled: true}}
So, my promise looks like it should. Why isn't then() being invoked?
What was actually happening:
So, it turns out my question should have been something like "Why doesn't my angular promise resolve in my Jasmine test?"
$digest
After a bit of digging and looking at other solutions, I found some good information about when/how promises are resolved. I needed to call $digest() on the $rootScope in order to resolve the promise and execute the then() code block (and therefore call done() to satisfy the spec).
No more request expected error
Adding $rootScope.$digest() got me most of the way there, but then I started discovering a No more request expected error that was causing my tests to fail. This was because the service I am using is sending off various POST and GET requests for another aspect of my application. Stubbing out a whenGET and whenPOST response seemed to solve that issue.
The Final Solution:
Long story short, my spec file now looks like:
describe("Async Tests", function(){
var dataService;
var rootScope;
var httpBackend;
beforeEach(module("myangularapp"));
beforeEach(inject(function(_$httpBackend_, _DataService_, $rootScope){
dataService = _DataService_;
rootScope = $rootScope;
httpBackend = _$httpBackend_;
// solves the 'No more request expected' errors:
httpBackend.whenGET('').respond([]);
httpBackend.whenPOST('').respond([]);
}));
it("Should work", function(done){
dataService.ready = true;
dataService.isReady().then(function(result){
console.log(result);
expect(result).toBe(true);
// still calls done() just as before
done();
});
// digest the scope every so often so we can resolve the promise from the DataService isReady() function
setInterval(rootScope.$digest, 100);
});
});
This solution seems more complex than it needs to be, but I think it'll do the trick for now. I hope this helps anyone else who may run into challenges with testing async code with Angular and Jasmine.
Another way to handle this is to always use .finally(done) when testing promises and then call $timeout.flush() afterwards.
"use strict";
describe('Services', function() {
var $q;
var $timeout;
// Include your module
beforeEach(module('plunker'));
// When Angular is under test it provides altered names
// for services so that they don't interfere with
// outer scope variables like we initialized above.
// This is nice because it allows for you to use $q
// in your test instead of having to use _q , $q_ or
// some other slightly mangled form of the original
// service names
beforeEach(inject(function(_$q_, _$timeout_) {
$q = _$q_;
// *** Use angular's timeout, not window.setTimeout() ***
$timeout = _$timeout_;
}));
it('should run DataService.isReady', function(done) {
// Create a Dataservice
function DataService() {}
// Set up the prototype function isReady
DataService.prototype.isReady = function () {
// Keep a reference to this for child scopes
var _this = this;
// Create a deferred
var deferred = $q.defer();
// If we're ready, start a timeout that will eventually resolve
if (this.ready) {
$timeout(function () {
// *** Note, we're not returning anything so we
// removed 'return' here. Just calling is needed. ***
deferred.resolve(true);
}, 1);
} else {
// a bunch of other stuff that eventually returns a promise
deferred.reject(false);
}
// Return the promise now, it will be resolved or rejected in the future
return deferred.promise;
};
// Create an instance
var ds = new DataService();
ds.ready = true;
console.log('got here');
// Call isReady on this instance
var prom = ds.isReady();
console.log(prom.then);
prom.then(function(result) {
console.log("I work!");
expect(result).toBe(true);
},function(err) {
console.error(err);
}, function() {
console.log('progress?');
})
// *** IMPORTANT: done must be called after promise is resolved
.finally(done);
$timeout.flush(); // Force digest cycle to resolve promises;
});
});
http://plnkr.co/edit/LFp214GQcm97Kyv8xnLp
Related
I am testing an angular service using karma/jasmine and one of my service functions is as follows. I need to get coverage to 100%, but can't seem to figure out how to test both success and error cases..
function getAccount(accountId) {
var defer = $q.defer(), myService;
myService = Restangular.all('Some/Url/Path');
myService.get('', {}, {
'header-info': 'bla'
})
.then(function onSuccess(response) {
defer.resolve(response);
}, function onError() {
someMethodCall();
});
return defer.promise;
}
In my corresponding .spec test file, I have:
it('should succeed in getting account', function() {
httpBackend.whenGET('Some/Url/Path').respond(200, mockResponse);
var promise = myServices.getAccount('account123');
promise.then(function(response) {
expect(response).toEqual(mockResponse);
});
it('should error out in getting account', function() {
httpBackend.whenGET('Some/Url/Path').respond(500, '');
var promise = myServices.getAccount('account123');
promise.then(function() {
expect(someMethodCall).toHaveBeenCalled();
});
Right now, both cases "pass", but I'm not getting the branch coverage for the onError case. Something seems fishy about the onSuccess case passing too.
Basically I am asking what is the correct syntax and way of writing the test cases such that I can hit both success and on error cases when I make a 200 and a 500 call to my API
Since you don't have any calls to $http in your service, I would recommend mocking Restangular instead of using httpBackend. This way your test doesn't have to know anything about the implementation details of Restangular, other than what it returns, just like your service.
Mock example:
var Restangular = {
all: function() {
return {
get: function() {
restangularDeferred = $q.defer();
return restangularDeferred.promise;
}
};
}
};
Now you can easily either resolve or reject restangularDeferred depending on what you want to test.
Set up your module to use the mock:
module('myApp', function($provide) {
$provide.value('Restangular', Restangular);
});
Example test of success case:
it('success', function() {
// If you want you can still spy on the mock
spyOn(Restangular, 'all').and.callThrough();
var mockResponse = {};
var promise = myServices.getAccount('account123');
promise.then(function(response) {
expect(response).toEqual(mockResponse);
expect(Restangular.all).toHaveBeenCalledWith('Some/Url/Path');
});
restangularDeferred.resolve(mockResponse);
// Trigger the digest loop and resolution of promise callbacks
$rootScope.$digest();
});
Example test of error case:
it('error', function() {
spyOn(anotherService, 'someMethodCall');
var mockResponse = {};
myServices.getAccount('acount123');
restangularDeferred.reject(mockResponse);
$rootScope.$digest();
expect(anotherService.someMethodCall).toHaveBeenCalled();
});
Note that I moved someMethodCall into anotherService in the example.
Demo: http://plnkr.co/edit/4JprZPvbN0bYSXFobgmu?p=preview
I'm stuck with testing promies in Chai and Sinon. Generally I got service with is wrapper for xhr request and it returns promises. I tried to test it like that:
beforeEach(function() {
server = sinon.fakeServer.create();
});
afterEach(function() {
server.restore();
});
describe('task name', function() {
it('should respond with promise error callback', function(done) {
var spy1 = sinon.spy();
var spy2 = sinon.spy();
service.get('/someBadUrl').then(spy1, spy2);
server.respond();
done();
expect(spy2.calledOnce).to.be.true;
expect(sp2.args[0][1].response.to.equal({status: 404, text: 'Not Found'});
});
});
My notes about this:
// spy2 is called after expect finish assertion
// tried with var timer = sinon.useFakeTimers() and timer.tick(510); with no results
// tried with chai-as-promised - don’t know how use it :-(
// cannot install sinon-as-promised only selected npm modules available in my environment
Any any ideas how fix this code/ test this service module?
There's various challenges here:
if service.get() is asynchronous, you need to wait for its completion before checking your assertions;
since the (proposed) solution checks the assertions in a promise handler, you have to be careful with exceptions. Instead of using done(), I would opt for using Mocha's (which I assume you're using) built-in promise support.
Try this:
it('should respond with promise error callback', function() {
var spy1 = sinon.spy();
var spy2 = sinon.spy();
// Insert the spies as resolve/reject handlers for the `.get()` call,
// and add another .then() to wait for full completion.
var result = service.get('/someBadUrl').then(spy1, spy2).then(function() {
expect(spy2.calledOnce).to.be.true;
expect(spy2.args[0][1].response.to.equal({status: 404, text: 'Not Found'}));
});
// Make the server respond.
server.respond();
// Return the result promise.
return result;
});
I'm new to Mocha but I read they support promises now but I can't seem to find any documentation that solves my problem. I have an authenticate method that returns a promise. In my test I need to wait till that gets done in order to pass/fail it.
Here is my Authenticate factory:
(function() {
'use strict';
angular.module('app.authentication').factory('authentication', authentication);
/* #ngInject */
function authentication($window, $q, $location, authenticationData, Session) {
var authService = {
authenticate: authenticate
};
return authService;
function authenticate() {
var token = authenticationData.getToken();
var deferral = $q.defer();
if (!Session.userId && token) {
authenticationData.getUser(token).then(function(results) {
Session.create(results.id, results.userName, results.role);
deferral.resolve();
});
}
else{
deferral.resolve();
}
return deferral.promise;
}.........
Here is my Test:
describe('authentication', function() {
beforeEach(function() {
module('app', specHelper.fakeLogger);
specHelper.injector(function($q, authentication, authenticationData, Session) {});
});
beforeEach(function() {
sinon.stub(authenticationData, 'getUser', function(token) {
var deferred = $q.defer();
deferred.resolve(mockData.getMockUser());
return deferred.promise;
});
});
describe('authenticate', function() {
it('should create Session with userName of TestBob', function() {
authentication.authenticate().then(function(){
console.log('is this right?');
expect(Session.userName).to.equal('TesaatBob');
}, function(){console.log('asdfasdf');});
});
});
});
When I run this, the test passes because it never makes it inside the promise and never hits the expect. If I put "return authenication.authenticate ...." then it errors with a timeout.
Thank you
Angular promises do not get resolved until the next digest cycle.
See http://brianmcd.com/2014/03/27/a-tip-for-angular-unit-tests-with-promises.html:
One thing that you'll quickly run into when unit testing Angular
applications is the need to hand-crank the digest cycle in certain
situations (via scope.$apply() or scope.$digest()). Unfortunately, one
of those situations is promise resolution, which is not very obvious
to beginning Angular developers.
I believe adding a $rootScope.$apply() should resolve your issue and force the promise resolution without the need for an asynchronous test.
I'm trying to test a service that I build which uses Angular's $q implementation of Promises. I'm using a combination of Karma, Mocha, Chai, Sinon, Sinon Chai and Chai as Promised.
All the tests that I wrote and return promises are passing but the ones that reject or uses $q.all([ ... ]). I have tried all I could think of but I cannot seem to find where the issue is.
The following is a slim version of what I am testing:
"use strict";
describe("Promise", function () {
var $rootScope,
$scope,
$q;
beforeEach(angular.mock.inject(function (_$rootScope_, _$q_) {
$rootScope = _$rootScope_;
$q = _$q_;
$scope = $rootScope.$new();
}));
afterEach(function () {
$scope.$apply();
});
it("should resolve promise and eventually return", function () {
var defer = $q.defer();
defer.resolve("incredible, this doesn't work at all");
return defer.promise.should.eventually.deep.equal("incredible, this doesn't work at all");
});
it("should resolve promises as expected", function () {
var fst = $q.defer(),
snd = $q.defer();
fst
.promise
.then(function (value) {
value.should.eql("phew, this works");
});
snd
.promise
.then(function (value) {
value.should.eql("wow, this works as well");
});
fst.resolve("phew, this works");
snd.resolve("wow, this works as well");
var all = $q.all([
fst.promise,
snd.promise
]);
return all.should.be.fullfiled;
});
it("should reject promise and eventually return", function () {
return $q.reject("no way, this doesn't work either?").should.eventually.deep.equal("no way, this doesn't work either?");
});
it("should reject promises as expected", function () {
var promise = $q.reject("sadly I failed for some stupid reason");
promise
["catch"](function (reason) {
reason.should.eql("sadly I failed for some stupid reason");
});
var all = $q.all([
promise
]);
return all.should.be.rejected;
});
});
The 3rd, last and the first test are the ones that fail. Actually it does not fail, it just resolves after the timeout is exceeded and I get a Error: timeout of 2000ms exceeded.
EDIT: I have just tried to test with Kris Kowal's implementation of the promises and it works just fine with that.
P.S. I actually found that there is some time spent somewhere in the bowls of either Mocha, Chai or Chai As Promised and the afterEach hook gets called later than the timeout.
afterEach() is used for cleanup, not for executing code after your preparations but before your tests. $scope.$apply() is not cleanup either.
You need to be doing the following:
// setup async behavior
var all = $q.all(x.promise, y.promise)
// resolve your deferreds/promises
x.reject(); y.reject();
// call $scope.$apply() to 'digest' all the promises
$scope.$apply();
// test the results
return all.should.be.rejected;
You're doing an $apply() AFTER your tests are done, not in between setup and evaluation.
I've tried to find out why the tests are not passing even though at first glance they should. Of course I would have to move the $scope.$apply(); from afterEach since that is not the place to call as #proloser mentioned.
Even though I have done that, the tests are still not passing. I've also opened issues on chai-as-promised and angular to see if I get any input/feedback and I've actually been told that it's most likely not to work. The reason is probably because of Angular $q's dependency on the digest phase which is not accounted for in the chai-as-promsied library.
Therefore I've checked the tests with Q instead of $q and it worked just fine, thus strengthening my hypothesis that the fault was not in the chai-as-promised library.
I've eventually dropped chai-as-promised and I've rewritten my test using Mocha's done callback instead (even though behind the scenes, chai-as-promised does the same):
"use strict";
describe("Promise", function () {
var $rootScope,
$scope,
$q;
beforeEach(angular.mock.inject(function (_$rootScope_, _$q_) {
$rootScope = _$rootScope_;
$q = _$q_;
$scope = $rootScope.$new();
}));
it("should resolve promise and eventually return", function (done) {
var defer = $q.defer();
defer
.promise
.then(function (value) {
value.should.eql("incredible, this doesn't work at all");
done();
});
defer.resolve("incredible, this doesn't work at all");
$scope.$apply();
});
it("should resolve promises as expected", function (done) {
var fst = $q.defer(),
snd = $q.defer();
fst
.promise
.then(function (value) {
value.should.eql("phew, this works");
});
snd
.promise
.then(function (value) {
value.should.eql("wow, this works as well");
});
fst.resolve("phew, this works");
snd.resolve("wow, this works as well");
var all = $q.all([
fst.promise,
snd.promise
]);
all
.then(function () {
done();
});
$scope.$apply();
});
it("should reject promise and eventually return", function (done) {
$q
.reject("no way, this doesn't work either?")
.catch(function (value) {
value.should.eql("no way, this doesn't work either?");
done();
});
$scope.$apply();
});
it("should reject promises as expected", function (done) {
var promise = $q.reject("sadly I failed for some stupid reason");
promise
["catch"](function (reason) {
reason.should.eql("sadly I failed for some stupid reason");
});
var all = $q.all([
promise
]);
all
.catch(function () {
done();
});
$scope.$apply();
});
});
The above tests will all pass as expected. There might be other ways to do it, but I could not figure out how else, so if anyone else does, it would be great to have it posted so others can benefit from it.
How do I delay execution of a function until after all of my $resources have resolved? My goal here is to be able to parse though the log array after all $resources have resolved and push a single success notification to the UI instead of pushing one notification per each success.
I've based my code below off of this question angular -- accessing data of multiple http calls - how to resolve the promises. I realize that $scope.promises is empty because item.$save() doesn't return anything but I hope you can see that I'm trying to push the unresolved promise to the promises array.
$scope.save = function () {
$scope.promises = [];
$scope.log = [];
angular.forEach($scope.menuItems, function(item) {
$scope.promises.push(item.$save(function(menu){
debugger; // execution gets here 2nd
console.debug("success");
$scope.log.push(msg: 'success');
}));
}, this);
$q.all($scope.promises).then(function() {
debugger; // execution gets here 1st
console.debug("all promises resolved");
});
};
Since $save does not return a promise, you will need an intermediate one:
angular.forEach($scope.menuItems, function(item) {
var d = $q.defer(); // <--- the intermediate promise
item.$save(
function(menu){
debugger;
console.debug("success");
$scope.log.push(msg: 'success');
d.resolve(menu); // <--- resolving it, optionally with the value
},
function(error){
d.reject(error); // <--- rejecting it on error
}
);
$scope.promises.push(d.promise);
}, this);
By the way, do not forget to throw away the array of promises, or you will keep garbage:
$q.all($scope.promises).then(...).always(function() {
$scope.promises = null;
});
And, if $scope.promises is NOT exposed to the view, it does not need to be in the scope; it can be just a var.