I am trying to unit test a service which has asynchronous methods but am having no luck.
I have tried to implement with promises by using the $q support in angularjs.
Any help would be appreciated.
http://jsfiddle.net/9pBze/37/
angular.module('myapp', ['myservice']);
angular.module('myservice', []).factory('myservice', function($q) {
var ls = {};
ls.DoIt = function() {
var deferred = $q.defer();
setTimeout(function(){
deferred.resolve(5);
},3000);
return deferred.promise;
}
return ls;
});
describe('services', function () {
beforeEach(module('myservice'));
it("should equal 2", inject(function(myservice) {
myservice.DoIt().then(function(returned) {
expect(returned).toEqual(2);
});
}));
});
First of all, the setTimeout is particularly tricky to test since it hard to mock. Fortunately AngularJS has a wrapper around it ($timeout) that plays the same role but can be easily mocked:
ls.DoIt = function() {
var deferred = $q.defer();
$timeout(function(){
deferred.resolve(5);
},3000);
return deferred.promise;
}
The mock provided for $timeout allows us to easily simulate elapsed time (with $timeout.flush()) which means our tests can run fast, without really waiting for the async event to complete (please note that the production code is still using async API!).
The changed tests would look like:
it("should equal 5", inject(function(myservice, $timeout) {
var valueToVerify;
myservice.DoIt().then(function(returned) {
valueToVerify = returned;
});
$timeout.flush();
expect(valueToVerify).toEqual(5);
}));
And finally the working jsFiddle: http://jsfiddle.net/v9L9G/1/
It's not related to Angular itself, but to Jasmine async tests.
If you need a setTimeout use Angular $timeout. And if you wish to have a fine control over setTimeout/$timeout executions, use mocked Clock.
Related
I would like to test this function:
function initializeView() {
var deferred = $q.defer();
if(this.momentArray) {
core.listMoments(constants.BEST_MOMENT_PREFIX, '').then(function(moments) {
//Ommitted
deferred.resolve(moments);
}, function(error) {
console.log("ERROR");
deferred.reject(error);
});
}
else {
deferred.resolve();
}
return deferred.promise;
};
The function calls core.listMoments:
function listMoments(prefix, startAfter) {
// var deferred = $q.defer();
var promises = [];
return awsServices.getMoments(prefix, startAfter).then(function(moments) { //Mocked
console.log("getMoments Returned"); //Does not print
for(var i = 0; i < moments.length; i++) {
// moments[i].Key = constants.IMAGE_URL + moments[i].Key;
promises.push(getMomentMetaData(moments[i]));
}
return $q.all(promises);
});
};
Here is my test function:
it('Should correctly initialize the view', function(done) {
spyOn(awsServices, 'getMoments').and.callFake(function() {
console.log("getMoments Has been mocked"); //This prints
return $q.resolve(mock_moment);
});
service.initializeView().then(function() {
done();
})
});
The problem is with the awsServices 'getMoments' mock. The call to awsServices.getMoments is in the listMoments function. I would like to mock out this function but when I do it does not execute the "then" part of the promise.
So based on my console logs it would print the 'getMoments Has been mocked' log but it would not print 'getMoments Returned' log. So the function is mocked but for some reason it is not moving into the then statement and my test just times out.
In order to get the .then() part of a promise to work in such a test, you need to use a $rootScope.$apply(). This is needed whether the promise is in your test code or in a referenced library that is being tested. Think of it like the flush() function for $http or $timeout calls.
The Testing example from the Angular documentation's $q page shows how to use it:
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);
}));
Note that they inject $rootScope.
$q promises can be synchronous (when they are resolved synchronously) and depend on digest cycles.
There should generally be no asynchronous done callback in Angular tests.
Angular tests are supposed to be synchronous, so are $q promises. In order to achieve that a digest should be triggered manually when an existing promise (the ones that is returned from getMoments and initializeView) is chained with then. If done callback is placed inside then and a digest is not triggered, this will result in spec timeout.
spyOn(awsServices, 'getMoments').and.callFake(function() {
console.log("getMoments Has been mocked"); //This prints
return $q.resolve(mock_moment);
});
service.initializeView();
$rootScope.$digest();
The thing that can be improved here is isolation. There are several units (methods) involved in a single test. This will affect troubleshooting when one of them fails.
Usually unit testing implies that only one unit is tested at time, while the rest are mocked or stubbed. In this case in one test service.listMoments is called and awsServices.getMoments is mocked, and in another test service.initializeView is called and service.listMoments is mocked.
Hello everyone out there. I've hit a brick wall. I'm trying to research Angular async method calls, promise objects and implement a solution to a particular issue. After a few hours of trial and error and restructuring my code, I'm certain at this point the answer is right under my nose but I cannot see it.
I have a scenario where I must use $http to make a backend service call, which yields a promise. In the results of promise I will receive a data object with one string property and an array of 100 IDs. This backend service will only deliver payloads of 100 IDs at a time, so I must make x number of $http calls to reach the end of the list, as provided by this backend service. As I understand promises, I must evaluate the $http response in the promise's .then() method and if the string property is 'N' then I know I must call the backend service again for another batch of 100 IDs. After all IDs have been delivered the string will contain a 'Y' indicating that the end of this has been sent and don't call again. No comment required on this scheme, I know it's fairly lame, and unfortunately out of my control. ;-)
Everything I've studied regarding promises just seem to illustrate chaining of promises, in a linear fashion, if more async calls are needed. Whether the promise chain is nested or flat, it seems like I can only make a "known" or fixed number of sequential calls, e.g. Promise1 to Promise2, to Promise3, etc. Forgive my limited experience with promises here.
Here is a code example, you can see where I'm stuck here, nesting downward. I can't just "hope" after 3 or 5 or 10 calls I'll get all the IDs. I need to dynamically evaluate the result of each call and call again.
Here is the Angular service which invokes $http
(function() {
'use strict';
angular
.module('myapp')
.factory('IDservice', IDservice);
function IDservice($http) {
var service = {
model: {
error: {
state: false,
message: ''
},
ids: [],
end: '',
},
GetIDs: function() {
var request = 'some params...';
var url = 'the url...';
return $http.post(url, request).then(reqComplete).catch(reqFailed);
function reqComplete(response) {
service.model.ids = response.data.idList;
service.model.end = response.data.end;
return service.model;
}
function getIntradayMsrFailed(error) {
service.model.error.state = true;
service.model.error.message = error;
return service.model;
}
}
};
return service;
}
})();
Here is the Controller which invokes the Angular service, which ultimately drives some UI elements on the respective view:
(function() {
'use strict';
angular
.module('myapp')
.controller('AvailableIDsController', AvailableIDsController);
function AvailableIDsController($scope, $location, $timeout, IDservice) {
var vm = this;
vm.completeIDList = [];
activate();
function activate() {
IDservice.GetIDs().then(function(response){
vm.completeIDList = response.ids;
console.log('promise1 end');
if(response.end === 'N'){
IDservice.GetIDs().then(function(response){
angular.forEach(response.ids,function(nextID){
vm.comleteIDList.push(nextID);
});
console.log('promise2 end');
if(response.end === 'N'){
IDservice.GetIDs().then(function(response){
angular.forEach(response.ids,function(nextID){
vm.comleteIDList.push(nextID);
});
console.log('promise3 end');
});
}
});
}
});
console.log('mainthread end');
}
}
})();
You can see where that's going... and it's very, very ugly.
I need a way, inside the activate() method, to call a method which will take care of invoking the service call and return the result back up to activate(). Now, still in the activate() method, evaluate the result and determine whether to call again, etc. Where I'm stuck is once the main processing thread is done, you're left with program control in that first promise. From there, you can perform another promise, etc. and down the rabbit hole you go. Once I'm in this trap, all is lost. Clearly I'm not doing this right. I'm missing some other simple piece of the puzzle. Any suggestions would be so greatly appreciated!
You're looking for plain old recursion:
function AvailableIDsController($scope, $location, $timeout, IDservice) {
var vm = this;
vm.completeIDList = [];
return activate(0);
function activate(i) {
return IDservice.GetIDs().then(function(response) {
[].push.apply(vm.completeIDList, response.ids); // simpler than the loop
console.log('promise'+i+' end');
if (response.end === 'N'){
return activate(i+1);
}
});
}
}
Don't forget the returns so that the promises chain together and you can wait for the end of the requests.
I am trying to return different values on multiple sypOn calls in Jasmine 1.4.1. I have the following code based on the answer here:
beforeEach(function (done) {
loadFixtures('fixture.html');
spyOn(dynamicContent, 'get').and.returnValues($ver1.getHtml(done),
$ver2.getHtml(done));
});
EDIT
I realise the above is for Jasmine 2.4 and above.
I also tried the other answers for older version but I get the same issue.
var alreadyCalled = false;
spyOn(dynamicContent, "get").and.callFake(function() {
if (alreadyCalled) return $ver1.getHtml(done);
alreadyCalled = true;
return $ver2.getHtml(done);
});
I should point out that I'm using jasmine-jquery.
My getHtml functions look like this and are identical for test purposes:
function getHtml(done) {
var deferred = $.Deferred();
$.get('some.html').then(function (data) {
// will only make it this far for the first call to getHtml()
deferred.resolve(data);
done();
});
return deferred.promise();
}
The problem is that only the first call to spyOn succeeds.
I'm wondering if it is due to the way I pass done to the function and resolve it within getHtml()
How can I make sure both async calls get returned on subsequent calls to the spy?
Disclaimer: there actually two questions being asked here but I feel like they are closely related.
I'm trying to pass a promise object to a directive and I want to run some initialization code in the directive as soon as the promise resolves.
In a controller I have:
$scope.item = $http.get(...)
.success(function (result) {
$scope.item = result.item;
});
$scope.item is passed to a directive (via an attribute in an isolated scope called item).
The directive link function finally do something like:
Promise.resolve(scope.item)
.then(function () {
initialize();
});
This works fine in Firefox but when I run it on IE I get an error because Promise is not defined. This problem made me realize I need to probably use the AngularJS $q service in order to provide consistency among browsers and while I was looking at the documentation I discovered another problem, which seemed small at first but it is actually giving me headaches: the success() function is deprecated and I should use then(successCallback) instead. Easy peasy, I thought, BUT as soon as I change the success call in the controller the code stop working in Firefox too! I cannot figure out why. So this is the first question.
The second question is that (even if I leave the success call in the controller) if I modify the code in the directive link function to use $q with what I thought was the equivalent:
$q.resolve(scope.item, function() { initialize(); });
this still doesn't work at all. Any suggestion?
You need to use Angular's $q not only because it works across browsers - but also because it is deeply linked to Angular's digest cycles. Other promise libraries can accomplish this feat but native promises cannot easily do so.
What $q.when does (the $q version of Promise.resolve) is convert a value or a promise to a $q promise. You don't need to do it since you're already using Angular's own $http API which returns promises already.
I warmly recommend that you put your web calls in services and don't affect the scope directly - and then call those services to update the scope.
The pattern is basically:
$http.get(...) // no assign
.success(function (result) { // old API, deprecated
$scope.item = result.item; // this is fine
});
Or with the better then promise API that has the benefits of promises over callbacks like chaining and error handling:
$http.get(...).then(function (result) {
$scope.item = result.data;
});
You are correct about the .success method being deprecated. The .then method returns data differently than the .success method.
$scope.httpPromise = $http.get(...)
.then(function (result) {
$scope.item = result.data.item;
return result.data;
});
You need to return the data to chain from it.
$scope.httpPromise.then ( function (data) {
//Do something with data
initialize();
});
For more information on the deprecation of the .success method see AngularJS $http Service API Reference -- deprecation notice.
Since scope.item is a promise all you have to do is:
scope.item.resolve.then(function() { initialize(); });
Make sure $q is injected in your directive.
improved answer
As #benjamin-gruenbaum mentioned correctly, I used an anti-pattern in my answer. So the solution is basically to pass the promise to your directive and use it there (as already mentioned in Benjamins answer).
Working jsFiddle: https://jsfiddle.net/eovp82qw/1/
old answer, uses anti-pattern
Sorry if I give you a solution that perhaps differs too much from your code. But maybe you can adopt it to your solution.
My approach would be to create a second promise I handover to your directive. This is imho a cleaner way for resolving the waiting state of the directive and don't reuse the same scope variable for two different tasks.
HTML
<body ng-app="myApp">
<div ng-controller="MyCtrl">
<my-directive promise="promiseFromController"></my-directive>
</div>
</body>
JS
function MyCtrl($scope, $q, $http) {
function init() {
var deferredCall = $q.defer();
// simulated ajax call to your server
// the callback will be executed async
$http.get('/echo/json/').then(function(data) {
console.log('received', data);
deferredCall.resolve(data); //<-- this will resolve the promise you'll handover to your directive
});
// we're return our promise immediately
$scope.promiseFromController = deferredCall.promise;
}
init();
}
angular.module('myApp',[])
.controller('MyCtrl', MyCtrl)
.directive('myDirective', function() {
return {
scope: {
promise: '='
},
controller: function($scope) {
console.log($scope);
$scope.promise.then(function(data) {
console.log('received data in directive', data);
});
}
}
})
Working jsFiddle: https://jsfiddle.net/1ua4r6m0/
(There is no output, check your browser console ;) )
I've been looking over and over for an example on how to cancel an ongoing REST-call using Angular's $resource. I haven't found any solution yet, but from the Angular documentation I got the impression that it should be possible.
From the documentation:
Usage:
$resource(url[, paramDefaults][, actions]);
One of the actions defined in the documentation:
timeout – {number|Promise} – timeout in milliseconds, or promise that should abort the request when resolved.
Does anyone have a working example showing how to use this timeout action with promise to cancel an ongoing request? Is it possible?
My code example:
var canceler = {};
$scope.doSomething = function() {
canceler = $q.defer();
$http.post('url', data, {timeout: canceler.promise}).
success(function(data) {
}).
error(function() {
});
};
function cancelPost() {
canceler.resolve(); //aborts request
}
}
Yes this is possible. You have to create a defere and set the promise as parameter:
var timeoutPromise = $q.defer();
{timeout: timeoutPromise.promise}
Then you can resolve the promise at any time:
timeoutPromise.resolve();
It should also be possible to call $timeout.cancel(timeoutPromise). What should be equal to timeoutPromise.reject().
$timeout $q