I'm starting to use Jasmine for unit-testing of a JavaScript library that relies heavily on promises. I need to fail a test case asynchronously, and would like to write something like the following:
describe("An async test suite", function () {
it("should fail asynchronously", function (done, fail) {
var promise = myLibraryCall();
promise.then(done, function(reason) { fail(reason); });
});
});
However, there is nothing like a fail call available from what I can see. And I can't throw an exception in the asynchronous error case because it's not caught by Jasmine - all I get is an eventual generic timeout. What is the best way to address this?
Short of modifying Jasmine itself, the simple solution is to create a wrapper around a combination of expect and a custom matcher to fail with a given message.
function endTestAfter(promise, done) {
var customMatchers = {
toFailWith: function () {
return {
compare: function (actual, expected) {
return {
pass: false,
message: "Asynchronous test failure: " + JSON.stringify(expected)
};
}
}
}
};
jasmine.addMatchers(customMatchers);
promise.done(done, function (reason) {
expect(null).toFailWith(reason);
done();
});
}
This yields the following test suite code:
describe("An async test suite", function () {
it("should fail asynchronously", function (done, fail) {
var promise = myLibraryCall();
endTestAfter(promise, done);
});
});
Related
I'd like to know why this promises implemented with Angular $q (tested on version 1.5.3) won't execute the "catch" neither "finally" promise functions if an error is thrown (it is being catched by the outer try catch in the example below). Whereas if I do the same with the "new Promise()" it will (Im testing this in the latest version of Chrome by the way).
Run the following code where you can inject $q (like a controller) to try it for yourself. You will notice how angular promise outputs the try/catch console log (and never excecutes the finally func.) whereas the standar promise properly catches the error and runs the catch() and finally() promise functions:
var angularPromise = function (data) {
var defered = $q.defer();
var promise = defered.promise;
var emptyvar = null;
if (data == "fail") {
//generate the exception
console.log("code reaches this point");
var fail = emptyvar.fakeproperty;
console.log("code will never reach this point due to the exception");
defered.reject("failed");//neither this...
}
return promise;
}
var standardPromise = function (data) {
return new Promise((resolve, reject) => {
var emptyvar = null;
if (data == "fail") {
//generate the exception
var fail = emptyvar.fakeproperty;
//in this scenario this error is thrown
//and captured by the promise catch()
//just as if I would call reject()
//which is the expected behaviour
}
});
}
try {
angularPromise("fail")
.then(
function (success) {
console.log("angularPromise: oka", success)
}
)
.catch(
function (fail) {
console.log("angularPromise: fail", fail);
}
).finally(
function (fail) {
console.log("angularPromise: 'finally' gets excecuted...");
}
);
} catch (e) {
console.log("angularPromise: exception catched with try/catch and not captured in promise 'catch'. Also 'finally' is never excecuted...");
}
try {
standardPromise("fail")
.then(
function (success) {
console.log(" standardPromise oka", success)
}
)
.catch(
function (fail) {
console.log("standardPromise: catched as expected", fail);
}
).finally(
function () {
console.log("standardPromise: 'finally' gets excecuted...");
}
);
} catch (e) {
console.log("standardPromise exception catched outside promise", e);
}
Wrapping your promise execution in a try/catch is not necessary—Angular $q's catch would catch any errors, so they would not bubble up to the catch in the try/catch.
Essentially, assuming you abstract your business logic out into a service, your service code would need to look something like this:
angular.module('app')
.factory('myService', function ($q) {
function doSomething(shouldSucceed) {
const deferred = $q.defer();
if (shouldSucceed) {
deferred.resolve('I succeeded! 😃');
} else {
deferred.reject('I failed! ☹️');
}
return deferred.promise;
}
return { doSomething };
});
Then, your controller would call that service and handle it accordingly:
angular.module('app')
.controller('MainCtrl', function ($scope, myService) {
$scope.shouldSucceed = function (shouldSucceed) {
myService
.doSomething(shouldSucceed)
.then(alertMessage)
.catch(alertMessage)
.finally(function () {
alert('FINALLY!');
});
};
function alertMessage(message) {
alert(message);
}
});
I've made a simple view that would allow you to click buttons and toggle each message:
<div ng-app="app" ng-controller="MainCtrl">
<button ng-click="shouldSucceed(false)">Fail</button>
<button ng-click="shouldSucceed(true)">Succeed</button>
</div>
I have provided a working example on CodePen of how to implement Angular's $q promises including .then, .catch, and .finally blocks.
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 have the following code:
var p1 = Q($.ajax({
url: "/api/test1"
}));
p1.then(function () {
console.log("success1");
});
p1.then(function () {
throw "some error";
console.log("success2");
});
p1.then(function () {
console.log("success3");
});
p1.catch(function () {
console.log("failure1");
});
p1.catch(function () {
console.log("failure2");
});
p1.finally(function () {
console.log("finally1");
});
p1.finally(function () {
console.log("finally2");
});
I am expecting the following output assuming that I get some data back from api/test1 - "success1, failure1, failure2, finally1, finally2"
What I actually get is
"success1, success3, finally1, finally3"
"Success3" is puzzling me - why is the code propagating to the 2nd then when an exception occurred?
How can I write this so that I can catch any exceptions that might occur in the then part(s)?
Ta
The reason "Success3" is showing is because your promises are not chained.
Basically the "then" calls (as well as the "catch" and "finally" for that matter) are interdependent of each other. If you think of it as a tree structure, they are all children of the p1 promise. If you want them to be dependent, they would be to have a child, grandchild, great-grandchild, etc relationship. If you want to chain them together, you can do something like:
var p1 = Q($.ajax({
url: "/api/test1"
}));
p1.then(function () {
console.log("success1");
}).then(function() {
throw "some error";
console.log("success2");
}).catch(function () {
console.log("failure");
});
Which is basically implicitly returning the promise after each function runs. The subsequent "then" (or catch/finally) is applied to that (implicit) return value.
I have this case where I think I want to have nested it() test cases in a Mocha test. I am sure this is wrong, and I don't see any recommendations to do what I am doing, but I don't really know of a better way at the moment -
basically, I have a "parent" test, and inside the parent test there's a forEach loop with all the "child" tests:
it('[test] enrichment', function (done) {
var self = this;
async.each(self.tests, function (json, cb) {
//it('[test] ' + path.basename(json), function (done) {
var jsonDataForEnrichment = require(json);
jsonDataForEnrichment.customer.accountnum = "8497404620452729";
jsonDataForEnrichment.customer.data.accountnum = "8497404620452729";
var options = {
url: self.serverURL + ':' + self.serverPort + '/event',
json: true,
body: jsonDataForEnrichment,
method: 'POST'
};
request(options,function (err, response, body) {
if (err) {
return cb(err);
}
assert.equal(response.statusCode, 201, "Error: Response Code");
cb(null);
});
//});
}, function complete(err) {
done(err)
});
});
as you can see, two separate lines are commented out - I want to include them so that I can easily see the results of each separate test, but then I have this awkward situation of firing the callback for the test alongside the callback for async.each.
Has anyone seen this time of situation before and know of a good solution where the tester can easily see the results of each test in a loop?
Don't nest it calls. Call them synchronously.
Nested it calls are never okay in Mocha. Nor are it calls performed asynchronously. (The test can be asynchronous, but you cannot call it asynchronously.) Here's a simple test:
describe("level 1", function () {
describe("first level 2", function () {
it("foo", function () {
console.log("foo");
it("bar", function () {
console.log("bar");
});
});
setTimeout(function () {
it("created async", function () {
console.log("the asyncly created one");
});
}, 500);
});
describe("second level 2", function () {
// Give time to the setTimeout above to trigger.
it("delayed", function (done) {
setTimeout(done, 1000);
});
});
});
If you run this you won't get the nested test bar will be ignored and the test created asynchronously (delayed) will also be ignored.
Mocha has no defined semantics for these kinds of calls. When I ran my test with the latest version of Mocha at the time of writing (2.3.3), it just ignored them. I recall that an earlier version of Mocha would have recognized the tests but would have attached them to the wrong describe block.
I think the need for dynamic tests are relatively common (data-driven tests?), and there is common use for dynamic it and test cases.
I think it could be easier to manage testcase completion if tests could be executed in series. This way you wouldn't have to worry about managing nested async done's. Since request is async (i'm assuming), your test cases will still mainly be executing concurrently.
describe('[test] enrichment', function () {
var self = this;
_.each(self.tests, function (json, cb) {
it('[test] ' + path.basename(json), function (done) {
var jsonDataForEnrichment = require(json);
jsonDataForEnrichment.customer.accountnum = "8497404620452729";
jsonDataForEnrichment.customer.data.accountnum = "8497404620452729";
var options = {
url: self.serverURL + ':' + self.serverPort + '/event',
json: true,
body: jsonDataForEnrichment,
method: 'POST'
};
request(options,function (error, response, body) {
if (error) {
cb(error);
}
else{
assert.equal(response.statusCode, 201, "Error: Response Code");
cb(null);
}
done();
});
});
}
});
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