I would like to unit test a function in an Angular controller that relies on an asynchronous API call (a promise).
I have a function in a controller that calls a service and sets a $scope variable in the controller based on the result of the service:
$scope.display = 'loading';
$scope.loadData = function() {
ApiService.getSummary().then(
function (response) {
$scope.displayData(response.data);
$scope.display = 'boxes';
},
function (error) {
$scope.display = 'error';
}
);
};
I have the following Jasmine unit test
var FunctionFixtures = {
successFunc: function() {
var deferred = $q.defer();
var response = {
'data': dataVar
};
deferred.resolve(response);
return deferred.promise;
},
errorFunc: function() {
var deferred = $q.defer();
deferred.reject();
return deferred.promise;
}
};
it('should show the boxes on data load success', function(){
spyOn(ApiService, 'getSummary').and.callFake(FunctionFixtures.successFunc);
$scope.loadData();
expect($scope.display).toBe('boxes');
});
When I run the test, I get the following error:
Error: Expected 'loading' to be 'boxes'.
I essentially want to fake the api call and return either a success or a failure and ensure that $scope.display is set to the appropriate string i.e. 'boxes' for success and 'error' failure. Any thoughts on how to do this?
It looks like you're missing a call to $rootScope.apply() after the call to loadData() but before the call to expect.
Because of how Angular promises are integrated with the $rootScope, you need to run a digest cycle for resolved promises to call their callbacks, as shown at https://code.angularjs.org/1.2.26/docs/api/ng/service/$q#testing.
Related
I have a controller that performs a http request.
This request can take anywhere between 2 seconds to 4 minutes to return some data .
I have added a button, that users should click to cancel the request if searches take too long to complete.
Controller:
$scope.search = function() {
myFactory.getResults()
.then(function(data) {
// some logic
}, function(error) {
// some logic
});
}
Service:
var myFactory = function($http, $q) {
return {
getResults: function(data) {
var deffered = $q.dafer();
var content = $http.get('someURL', {
data: {},
responseType: json
)}
deffered.resolve(content);
returned deffered.promise;
}
}
}
Button click:
$scope.cancelGetResults = function() {
// some code to cancel myFactory.getResults() promise
}
How can I implement a button click to cancel the myFactory.getResults() promise?
The question uses deferred antipattern which usually should be avoided, but it fits the cancellation case:
getResults: function(data) {
var deffered = $q.defer();
$http.get('someURL', {
data: {},
responseType: json
}).then(deffered.resolve, deferred.reject);
deffered.promise.cancel = function () {
deferred.reject('CANCELLED')
};
returned deffered.promise;
}
getResult is a service in which we are implementing cancellation.
getResult = function(){
var deferred = $q.defer();
$http.get(url).success(function(result){
deffered.resolve(result);
}).error(function(){
deffered.reject('Error is occured!');
});
return deferred.promise;
};
where url variable is used in place of any Restful API url. You can use it with given code.
getResult().then(function (result) { console.log(result); };
You could use .resolve() method which should be available.
Pass the promise in the controller to the variable.
Create e.g. cancel method which takes the promise as an argument in you factory. Then call this method in the cancelGetResults() function in the controller.
In the cancel method you just call .resolve on the passed promise.
This should actually do.
https://www.bennadel.com/blog/2731-canceling-a-promise-in-angularjs.htm
I'm fairly new to AngularJS and have just begun to grasp many of the concepts I especially like the MVC design pattern. But I am having a difficult time implementing the Service layer in my application.
What I am finding is that after my Controller calls a method within the service, it continues with code execution before the service returns the data; so that by the time the service does return the data, it isn't of any use to the controller.
To give a better example of what I'm saying, here is the code:
var InsightApp = angular.module('InsightApp', ['chart.js']);
// Module declaration with factory containing the service
InsightApp.factory("DataService", function ($http) {
return {
GetChartSelections: function () {
return $http.get('Home/GetSalesData')
.then(function (result) {
return result.data;
});
}
};
});
InsightApp.controller("ChartSelectionController", GetAvailableCharts);
// 2nd Controller calls the Service
InsightApp.controller("DataController", function($scope, $http, DataService){
var response = DataService.GetChartSelections();
// This method is executed before the service returns the data
function workWithData(response){
// Do Something with Data
}
}
All the examples I've found seem to be constructed as I have here or some slight variation; so I am not certain what I should be doing to ensure asynchronous execution.
In the Javascript world, I'd move the service to the inside of the Controller to make certain it executes async; but I don't how to make that happen in Angular. Also, it would be counter intuitive against the angular injection to do that anyway.
So what is the proper way to do this?
http return a promise not the data, so in your factory your returning the $http promise and can use it like a promise with then, catch, finally method.
see: http://blog.ninja-squad.com/2015/05/28/angularjs-promises/
InsightApp.controller("DataController", function($scope, $http, DataService){
var response = DataService.GetChartSelections()
.then(function(res) {
// execute when you have the data
})
.catch(function(err) {
// execute if you have an error in your http call
});
EDIT pass params to model service:
InsightApp.factory("DataService", function ($http) {
return {
GetChartSelections: function (yourParameter) {
console.log(yourParameter);
return $http.get('Home/GetSalesData')
.then(function (result) {
return result.data;
});
}
};
});
and then :
InsightApp.controller("DataController", function($scope, $http, DataService){
var response = DataService.GetChartSelections('only pie one')
.then(function(res) {
// execute when you have the data
})
.catch(function(err) {
// execute if you have an error in your http call
});
You should proceed like this :
DataService.GetChartSelections().then(function (data) {
workWithData(data);
}
Actually $http.get returns a Promise. You can call the method then to handle the success or failure of your Promise
Should it not be like this, when your $http returns a promise or you pass a callback.
With passing callback as a param.
InsightApp.factory("DataService", function ($http) {
return {
GetChartSelections: function (workWithData) {
return $http.get('Home/GetSalesData')
.then(function (result) {
workWithData(result.data);
});
}
};
});
Controller code:
InsightApp.controller("DataController", function($scope, $http, DataService){
var response = DataService.GetChartSelections(workWithData);
// This method is executed before the service returns the data
function workWithData(response){
// Do Something with Data
}
}
Or use then or success:
var response = DataService.GetChartSelections().then(function(res){
//you have your response here... which you can pass to workWithData
});
Return the promise to the controller, dont resolve it in the factory
var InsightApp = angular.module('InsightApp', ['chart.js']);
// Module declaration with factory containing the service
InsightApp.factory("DataService", function ($http) {
return {
GetChartSelections: function () {
return $http.get('Home/GetSalesData');
}
};
});
In the controller,
var successCallBk =function (response){
// Do Something with Data
};
var errorCallBK =function (response){
// Error Module
};
var response = DataService.GetChartSelections().then(successCallBk,errorCallBK);
I've got a service that has the following method (among others), which returns an $http promise
function sessionService($http, serviceRoot) {
return {
getAvailableDates: function () {
return $http.get(serviceRoot + '/session/available_dates');
}
};
};
angular.module('app').service('sessionService', ['$http', 'serviceRoot', sessionService]);
And then another factory that kinda wraps it and caches/adds data to localStorage. This returns a regular promise
angular.module('app')
.factory('AvailableDates', AvailableDates);
AvailableDates.$inject = ['sessionService', '$window', '$q'];
function AvailableDates(sessionService, $window, $q) {
var availableDates = [];
return {
getAvailableDates: getAvailableDates
};
function getAvailableDates() {
var deferred = $q.defer();
var fromStorage = JSON.parse($window.sessionStorage.getItem('validDates'));
if (availableDates.length > 0) {
deferred.resolve(availableDates);
} else if (fromStorage !== null) {
deferred.resolve(fromStorage);
} else {
sessionService.getAvailableDates()
.success(function (result) {
availableDates = result;
$window.sessionStorage.setItem('validDates', JSON.stringify(availableDates));
deferred.resolve(availableDates);
});
}
return deferred.promise;
}
}
This all works fine. My problem is I can't figure out how to test this thing while mocking the sessionService. I've read all the related stackoverflow questions, and tried all kinds of different things, to no avail.
Here's what my test currently looks like:
describe('testing AvailableDates factory', function () {
var mock, service, rootScope, spy, window, sessionStorageSpy, $q;
var dates = [ "2014-09-27", "2014-09-20", "2014-09-13", "2014-09-06", "2014-08-30" ];
var result;
beforeEach(module('app'));
beforeEach(function() {
return angular.mock.inject(function (_sessionService_, _AvailableDates_, _$rootScope_, _$window_, _$q_) {
mock = _sessionService_;
service = _AvailableDates_;
rootScope = _$rootScope_;
window = _$window_;
$q = _$q_;
});
});
beforeEach(inject(function () {
// my service under test calls this service method
spy = spyOn(mock, 'getAvailableDates').and.callFake(function () {
return {
success: function () {
return [ "2014-09-27", "2014-09-20", "2014-09-13", "2014-09-06", "2014-08-30" ];
},
error: function() {
return "error";
}
};
});
spyOn(window.sessionStorage, "getItem").and.callThrough();
}));
beforeEach(function() {
service.getAvailableDates().then(function(data) {
result = data;
// use done() here??
});
});
it('first call to fetch available dates hits sessionService and returns dates from the service', function () {
rootScope.$apply(); // ??
console.log(result); // this is printing undefined
expect(spy).toHaveBeenCalled(); // this passes
expect(window.sessionStorage.getItem).toHaveBeenCalled(); // this passes
});
});
I've tried various things but can't figure out how to test the result of the AvailableDates.getAvailableDates() call. When I use done(), I get the error:
Timeout - Async callback was not invoked withing timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL (I've tried overriding this value, no luck).
If I take out the done(), and just call rootScope.$apply() after the .then is called, I get an undefined value as my result.
What am I doing wrong?
I see more issues in your example.
The main problem is the success definition in the mock. Success is a function, which has a function as a parameter - callback. Callback is called when data is received - data is passed as the first argument.
return {
success: function (callback) {
callback(dates);
}
};
Simplified working example is here http://plnkr.co/edit/Tj2TZDWPkzjYhsuSM0u3?p=preview
In this example, mock is passed to the provider with the module function (from ngMock) - you can pass the object with a key (service name) and value (implementation). That implementation will be used for injection.
module({
sessionService:sessionServiceMock
});
I think test logic should be in one function (test), split it into beforeEach and test is not a good solution. Test is my example; it's more readable and has clearly separated parts - arrange, act, assert.
inject(function (AvailableDates) {
AvailableDates.getAvailableDates().then(function(data) {
expect(data).toEqual(dates);
done();
});
rootScope.$apply(); // promises are resolved/dispatched only on next $digest cycle
expect(sessionServiceMock.getAvailableDates).toHaveBeenCalled();
expect(window.sessionStorage.getItem).toHaveBeenCalled();
});
The test in this code does not succeed. I can't seem to successfully test the return of an asynchronous function.
describe('mocking services', function () {
var someService, deferred;
beforeEach(function () {
module(function($provide){
$provide.factory('someService', function($q){
return{
trySynch: function(){
return 33;
},
tryAsynch: function(){
deferred = $q.defer();
return deferred.promise;
}
};
});
});
inject(function (_someService_) {
someService = _someService_;
});
});
it('should be able to test values from both functions', function () {
expect(someService.trySynch()).toEqual(33);
var retVal;
someService.tryAsynch().then(function(r){
retVal = r;
});
deferred.resolve(44);
expect(retVal).toEqual(44);
});
});
When I run it I get the following error:
Chrome 36.0.1985 (Mac OS X 10.9.4) mocking services should be able to test values from both functions FAILED
Expected undefined to equal 44.
Error: Expected undefined to equal 44.
at null.<anonymous> (/Users/selah/WebstormProjects/macrosim-angular/test/spec/services/usersAndRoles-service-test.js:34:24)
How can I make this test pass?
When mocking async calls with $q, you need to use $rootScope.$apply() because of how $q is implemented.
Specifically, the .then method does not get called synchronously, it is designed to always be async, regardless of how it was called - sync or async.
To achieve that, $q is integrated with $rootScope. Therefore, in your unit tests, you need to notify the $rootScope that something was changed (ie - trigger a digest cycle). To do that, you call $rootScope.$apply()
See here (specifically the "Differences between Kris Kowal's Q and $q section")
Working code looks like this:
describe('mocking services', function () {
var someService, deferred, rootScope;
beforeEach(function () {
module(function($provide){
$provide.factory('someService', function($q){
return{
trySynch: function(){
return 33;
},
tryAsynch: function(){
deferred = $q.defer();
return deferred.promise;
}
};
});
});
inject(function ($injector) {
someService = $injector.get('someService');
rootScope = $injector.get('$rootScope');
});
});
it('should be able to test values from both functions', function () {
expect(someService.trySynch()).toEqual(33);
var retVal;
someService.tryAsynch().then(function(r){
retVal = r;
});
deferred.resolve(44);
rootScope.$apply();
expect(retVal).toEqual(44);
});
});
$q's deferred is still resolving asynchronously.
Quick test, albeit in an older version of Angular: http://plnkr.co/edit/yg2COXG0TWBYniXOwJYb
This test should work:
it('should be able to test values from both functions', function (done) {
expect(someService.trySynch()).toEqual(33);
someService.tryAsynch().then(function(r){
expect(r).toEqual(44);
done();
});
deferred.resolve(44);
});
If I run rootScope.$apply() before my expect clause that tests my asynchronous function then the test succeeds. Also, it fails if I supply an incorrect value, as I would expect it to.
So my test is functional, but I don't however understand why rootScope.$apply() is important here, so if anyone wants to copy my code and provide an explanation I will gladly mark your answer as the correct answer!
My working test code looks like this:
describe('mocking services', function () {
var someService, deferred, rootScope;
beforeEach(function () {
module(function($provide){
$provide.factory('someService', function($q){
return{
trySynch: function(){
return 33;
},
tryAsynch: function(){
deferred = $q.defer();
return deferred.promise;
}
};
});
});
inject(function ($injector) {
someService = $injector.get('someService');
rootScope = $injector.get('$rootScope');
});
});
it('should be able to test values from both functions', function () {
expect(someService.trySynch()).toEqual(33);
var retVal;
someService.tryAsynch().then(function(r){
retVal = r;
});
deferred.resolve(44);
rootScope.$apply();
expect(retVal).toEqual(44);
});
});
I would like to create a chained promise for my service provider:
this.$get = function($q, $window, $rootScope) {
var $facebook=$q.defer();
$rootScope.$on("fb.load", function(e, FB) {
$facebook.resolve(FB);
});
$facebook.api = function () {
var args=arguments;
args[args.length++] = function(response) {
$facebook.resolve(response);
};
$facebook.promise.then(function(FB) {
FB.api.apply(FB, args);
});
return $facebook.promise;
};
return $facebook;
};
Than I call to the promise: $scope.user=$facebook.api("/me");
The problem is that because the deferred was already resolved its not wait until the api method will resolve it..
How can I chain them in a way the last promise will wait until the last promise will resolved?
It seems like you need two separate promise objects:
One for the fb.load event and another one for the result of the API call.
Try chaning your code to read -
this.$get = function($q, $window, $rootScope) {
var apiLoaded=$q.defer();
$rootScope.$on("fb.load", function(e, FB) {
apiLoaded.resolve(FB);
});
// You should reject the promise if facebook load fails.
$facebook.api = function () {
var resultDefer = $q.defer(),
args=arguments;
args[args.length++] = function(response) {
$rootScope.$apply(function() {
resultDefer.resolve(response);
// you should reject if the response is an error
});
};
return apiLoaded.promise.then(function(FB) {
FB.api.apply(FB, args);
return resultDefer.promise;
});
};
return $facebook;
};
Also note that whenever you call resolve() from non-angularish code, you will need to wrap it with $rootScope.$apply(), otherwise then promise 'then' handlers will not get executed. Good luck!