I am trying to spy on an asynchronous service call, however so far not successful. Though the synchronus calls are able to Mock.
commonService.getResource(ResourceConst).then(function (result) {
$scope.resource = result.data;
}, function (error) {
loggerService.log(error);
});
Actual Get Resource Function
function getResource(resourceURL) {
var deferred = $q.defer();
var url = resourceURL + ".json";
$http.get(url).then(function (data) {
deferred.resolve(data);
},
function (error) {
deferred.reject(error);
});
return deferred.promise;
}
Even though I was able to spy on synchronus methods in the same service like below I was not able to do the same for this function call.
spyOn(commonService, 'hasData').and.callFake(function(args) {
return true;
});
The above code success fully mocked the synchronus call ,
$scope.hasData = commonService.hasData($scope.resource.Data);
How can I return a result
{'name': "Name"} to the asynchronous call getResource
Since the service is supposed to return a promise, return value should be mocked with a promise:
spyOn(commonService, 'hasData').and.returnValue($q.resolve({'name': "Name"} ));
The code above uses deferred antipattern. It should be instead:
function getResource(resourceURL) {
var url = resourceURL + ".json";
return $http.get(url);
}
Related
I'm trying to test what the following method returns in it's promise (functionToTest and asyncGet are both methods defined in an angularJS service):
var functionToTest = function (param) {
var deferred = $q.defer();
asyncGet(param).then(function (result) {
//business login involving result
if (something)
return deferred.resolve(true);
else
return deferred.resolve(false);
});
return deferred.promise;
}
The unit test looks like this:
it ('should return true', function (done) {
var asyncGetResult = {};
spyOn(asyncGet).and.returnValue($q.resolve(asyncGetResult));
var param = {};
functionToTest(param).then(function (result) {
expect(result).toBe(true);
done();
});
$scope.$apply();
});
When running the test I am getting a timeout error:
Async callback was not invoked within timeout specified by
jasmine.DEFAULT_TIMEOUT_INTERVAL.
I tried putting a console.log() right after the expect() but it does not print anything, so it seems like the callback for the functionToTest(param).then() is never executed.
Any help would be appreciated.
Remove the $q.deferred anti-pattern:
var functionToTest = function (param) {
̶v̶a̶r̶ ̶d̶e̶f̶e̶r̶r̶e̶d̶ ̶=̶ ̶$̶q̶.̶d̶e̶f̶e̶r̶(̶)̶;̶
return asyncGet(param).then(function (result) {
//business login involving result
if (something)
return true;
else
return false;
});
̶r̶e̶t̶u̶r̶n̶ ̶d̶e̶f̶e̶r̶r̶e̶d̶.̶p̶r̶o̶m̶i̶s̶e̶;̶
}
Also provide an error handler:
functionToTest(param).then(function (result) {
expect(result).toBe(true);
done();
}).catch(function(err) {
console.log(err);
done();
});
Deferred anti-patterns can hang if the code is written erroneously. Avoid the anti-pattern to prevent this problem.
Include a .catch handler to see any errors.
This question already has answers here:
Is this a "Deferred Antipattern"?
(3 answers)
Closed 5 years ago.
I create $http service and using $q.
Here is my $http service:
function dashboardService($http, $log, $q, config) {
var service = {
getClientId: getClientIDByLayout,
};
return service;
function getClientIDByLayout(layoutId) {
var deferred = $q.defer();
return $http.get(config.baseUrl + "api/ClientLayoutMercator?layoutId=" + layoutId).then(function (result) {
deferred.resolve(result.data);
}, function (result) {
deferred.reject(result);
});
return deferred.promise;
}
}
And here is how I call service above inside controller:
dashboardService.getClientId(layoutId).then(function (data) {
var t = data;//undifined
});
But result I get in this row var t = data is undefined.
Any idea why I get undefined from the service?
Basically you have two return statement inside your getClientIDByLayout function and both are returning promise itself. As I can see with your current implementation your're creating new promise & managing rejection / resolve manually. But the problem is the 1st return statement (return $http.get() is making other return statement(return deferred.promise) redundant. Hence 1st promise returned to subscription from controller. Eventually $http.get doesn't return anything so you get undefined in successCallback of then.
You can easily fix this issue by removing 1st return statement as shown below.
function getClientIDByLayout(layoutId) {
var deferred = $q.defer();
//removed `return` from below code.
$http.get(config.baseUrl + "api/ClientLayoutMercator?layoutId=" + layoutId).then(function (result) {
deferred.resolve(result.data);
}, function (result) {
deferred.reject(result);
});
//custom promise should get return
return deferred.promise;
}
Ideally creating promise overhead considered as antipattern, rather you can utilize the promise returned by $http.get. Just return a data from its success callback to chain the promise.
Code
function getClientIDByLayout(layoutId) {
̶v̶a̶r̶ ̶d̶e̶f̶e̶r̶r̶e̶d̶ ̶=̶ ̶$̶q̶.̶d̶e̶f̶e̶r̶(̶)̶;̶
return $http.get(config.baseUrl + "api/ClientLayoutMercator?layoutId=" + layoutId)
.then(function (result) {
//returning data from promise, it will provide it to subsequent `.then`
return result.data;
}, function (error) {
͟r͟e͟t͟u͟r͟n͟ $q.reject(error);
}
);
}
Instead of using $q.defer, simply return or throw to the handler functions in the .then method:
function dashboardService($http, $log, ̶$̶q̶,̶ config) {
var service = {
getClientId: getClientIDByLayout,
};
return service;
function getClientIDByLayout(layoutId) {
̶v̶a̶r̶ ̶d̶e̶f̶e̶r̶r̶e̶d̶ ̶=̶ ̶$̶q̶.̶d̶e̶f̶e̶r̶(̶)̶;̶
return $http.get(config.baseUrl + "api/ClientLayoutMercator?layoutId=" + layoutId).then(function (result) {
̶d̶e̶f̶e̶r̶r̶e̶d̶.̶r̶e̶s̶o̶l̶v̶e̶(̶r̶e̶s̶u̶l̶t̶.̶d̶a̶t̶a̶)̶;̶
return result.data;
}, function (result) {
̶d̶e̶f̶e̶r̶r̶e̶d̶.̶r̶e̶j̶e̶c̶t̶(̶r̶e̶s̶u̶l̶t̶)̶;̶
throw result;
});
̶r̶e̶t̶u̶r̶n̶ ̶d̶e̶f̶e̶r̶r̶e̶d̶.̶p̶r̶o̶m̶i̶s̶e̶;̶
}
}
The .then method returns a new promise which is resolved or rejected via the return value of the successCallback or errorCallback (unless that value is a promise, in which case it is resolved with the value which is resolved in that promise using promise chaining).1
By erroneously returning a promise with a then method that contained functions that lacked return or throw statements the service was returning a promise that resolved as undefined.
For more information, see You're Missing the Point of Promises.
I'm using $q service to make an async calls. I can't resolve 'then' and 'defer' in unit tests using karma.
The below is my controller code.
scope.filterUrls = [{url:'page1'}, {url: 'page2'}, {url:'page-error'}];
scope.bindFilters = function () {
angular.forEach(scope.filterUrls, function (data) {
scope.getFilterData(data.url, '').then(function (result) {
if (data.url === 'page1') {
scope.moduleData.index = result.data;
} else if (data.url === 'page2') {
scope.moduleData.page2 = result.data;
}
});
});
}
scope.getFilterData = function (filterUrls, params) {
// $q service object
var deferred = q.defer();
// regular ajax request
http({
method: 'GET',
url: app.api.root + filterUrls,
params: params
})
.success(function (result) {
// promise resolve
deferred.resolve(result);
})
.error(function (result) {
// called asynchronously if an error occurs
// or server returns response with an error status.
deferred.reject('Erreur request : ' + result);
});
return deferred.promise;
};
test spec:
it('should call getFilterData() in bindFilters()', function () {
spyOn(scope, 'getFilterData');
scope.bindFilters();
expect(scope.getFilterData).toHaveBeenCalled();
});
I'm getting an error called "TypeError: Cannot read property 'then' of undefined".
How can I write an unit test for those two methods using karma.
Update:
1.how do we can test the success and error of scope.getFilterData()
2 .then function in scope.bindFilters() function.
Please help..
If you only need to find out whether getFilterData is called or not, try returning a fake promise by faking the function:
With jasmine 1.3, we could use andCallFake:
it('should call getFilterData() in bindFilters()', function () {
spyOn(scope, 'getFilterData').andCallFake(function(){//replace with a fake function
var deferred = $q.defer(); //assume that you already inject $q service in beforeEach and save it as a variable.
return deferred.promise; //returns a fake promise
});
scope.bindFilters();
expect(scope.getFilterData).toHaveBeenCalled();
});
With jasmine 2.0, we could use and.callFake instead.
Another solution is to use andReturnValue and $q.when():
it('should call getFilterData() in bindFilters()', function () {
spyOn(scope, 'getFilterData').andReturnValue($q.when());
scope.bindFilters();
expect(scope.getFilterData).toHaveBeenCalled();
});
With jasmine 2.0, we could use and.returnValue instead.
var newservices = angular.module('newservices', []);
newservices.service('newservice', function ($http) {
return{
newdata: function(parameter){
return $http.get('/devicedetails/'+parameter).success(function(data) {
console.log(data)
return data
});
},
}
});
The above service is included in one of my controllers
data=newService.newdata($scope.dummy)
console.log(data)
while trying to print data what i get is $http function object as shown below
Object {then: function, catch: function, finally: function, success: function, error: function}
why is this so??
What you see is not an error. It's a Promise.
You did an $http GET request, which is asynchronous. $http.getreturns a promise that will be resolved when the remote request is completed. In that moment, you'll get the final value.
See this example, where getShops would be your method newData
this.getShop = function (id, lang) {
var promise = $http.get(appRoot + 'model/shops_' + lang + '.json');
return promise;
};
In a controller you can use it like this:
Shops.getShop($routeParams.id).then(function (response) {
console.log("data is", response.data);
$scope.shop = response.data[$routeParams.id];
});
When the data is ready, assign it to a scope.
In your case:
var data;
newService.newdata($scope.dummy).then(function (response) {
data = response.data;
});
Your service is returnig a promise
You should use some what like this, not tested though it should work.
data = newService.newdata($scope.dummy).then(function (response) {
return response.data;
},
function (error) {
return error;
});
You are using it wrong.
This work in promises. so in you controller you need to consume the promisses.
newService.newData($scope.dummy)
.then(function (data){
$scope.data = data;
console.log(data);
});
Try this.
I'm currently using a custom AngularJS service to enable calling remote methods on my server-side. The wrapper service is intended to simplify calling these methods in my controllers, and returns a promise object.
Within the service, i'm calling a method from a provided API that takes the following arguments: 1) the name of the remote method, 2) a variable number of parameters and 3) as last parameter the callback function that is called after completion.
So this is the function that I'm wrapping with the service:
callRemoteAPI("nameOfRemoteMethod", [param1, param2, ... , paramN,] callBack() );
The callBack function returns the promise and implements the resolve() and reject() methods.
I've currently setup different functions within the service to handle different number of parameters: callNoParam, call1Param, call2Param, etc. to be able to call different remote methods that require a different number of parameters.
This works, but obviously is not the proper Object Oriented way of doing this, and I'm sure there is a better way. But so far I've been unsuccesfull of making this work in a dynamic way.
What is the best way of handling this dynamic # of params within the angular service?
Here's the code for the service:
app.factory('remoteAction', function($q) {
return {
callNoParams: function(method, p1) {
var deferred = $q.defer();
callRemoteApi(
method,
function(result) {
if (result.succes) deferred.resolve(result);
else deferred.reject(result);
}
);
return deferred.promise;
},
call1Param: function(method, p1) {
var deferred = $q.defer();
callRemoteApi(
method,
p1,
function(result) {
if (result.succes) deferred.resolve(result);
else deferred.reject(result);
}
);
return deferred.promise;
},
call2Param: function(method, p1, p2) {
var deferred = $q.defer();
callRemoteApi(
method,
p1,p2,
function(result) {
if (result.succes) deferred.resolve(result);
else deferred.reject(result);
}
);
return deferred.promise;
},
call3Param: function(method, p1,p2,p3) {
var deferred = $q.defer();
callRemoteApi(
method,
p1,p2,p3,
function(result) {
if (result.succes) deferred.resolve(result);
else deferred.reject(result);
}
);
return deferred.promise;
}
};
// Could add more methods when needing more params,
// but 1 dynamic function would be so much nicer...
});
You can call the javascript function apply (MDN).
More on the arguments object on MDN
app.factory('remoteAction', function($q) {
return {
call: function() { //funcArgs should be an array
var deferred = $q.defer();
var parameters = arguments;
parameters.push(function(result) {
if (result.succes) deferred.resolve(result);
else deferred.reject(result);
});
callRemoteApi.apply(this,parameters);
return deferred.promise;
}
};
});