I have a factory which wraps $http for custom template loading, and I'm trying to write unit tests for it. Valid templates work because they're pre-loaded into $templateCache, so the underlying http promise never needs to be called.
However, I'm trying to test our promise gets rejected if the file path is wrong.
I'm telling ngMock to respond with a 404:
$httpBackend.when('GET', '/invalid').respond(404, '');
My test never fires the then/catch/finally code. The underlying http promise is never resolved/rejected.
I've reduced the test to pure $http calls while resolving this issue.
it('"get" rejects with invalid path', function(done) {
var deferred = $http.get('/invalid');
deferred.catch(function(error) {
console.log(error);
done(error);
});
deferred.then(function(response) {
console.log(response);
done();
});
deferred.finally(function() {
console.log('finally!');
});
// Trigger http promise resolution
$rootScope.$digest();
});
In order to trigger the responses to requests, call $httpBackend.flush();. This will also trigger a digest cycle, so you won't need to call .$apply();.
Also, the response from an $http is a modified promise. I've updated the code below to handle your use case.
Lastly, make sure that you are using the ngMock module and not the ngMockE2E. They each give you a slightly different $httpBackend object, but the ngMock module is meant for unit testing. See the differences here (ngMock's $httpBackend vs ngMockE2E's $httpBackend)
Here is a simplified fiddle showing how it works.
Your updated code would be:
it('"get" rejects with invalid path', function() {
$httpBackend.whenGET('/invalid').respond(404, '');
var wasRejected = false;
$http.get('/invalid')
.error(function (data, status, headers, config) {
console.log(status);
wasRejected = true;
});
// Trigger http promise resolution
$httpBackend.flush();
// Make your assertion...
expect(wasRejected).toBeTrue();
});
afterEach(function(){
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
You have a couple problems with your code. First $http promises are resolved using success, error and then. From the angular documentation here:
$http Returns a promise object with the standard then method and two http specific methods: success and error. The then method takes two arguments a success and an error callback which will be called with a response object. The success and error methods take a single argument - a function that will be called when the request succeeds or fails respectively. The arguments passed into these functions are destructured representation of the response object passed into the then method.
The second is that you need to call $httpBackend.flush(); to trigger the return of your request. This is to simulate an asynchronous behavior in a synchronous test.
it('Should return status Not Found', function() {
$httpBackend.expectGET('/invalid').respond(404, 'Not Found');
var result = '';
$http.get('/invalid')
.success(function(data){
result = data;
}).error(function(reason){
result = reason;
});
$httpBackend.flush();
expect(result).toEqual('Not Found');
});
});
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.
This question already has answers here:
AngularJS Promises, $q, defer
(2 answers)
Closed 5 years ago.
I am new to angularjs.I saw $q in restful api calls to check the promise.
$q.defer() was used to retain the promise object.
I read about the promises but I didn't get anything.
although I can make the api call without $q, however it is used somewhere in articles.
So I want to know the exact use of $q and difference in making api calls without $q.
Kindly help.
thanks
I think the article I wrote about $q might help you.
Introduction to $q
$q is an angular defined service. It’s the same as new Promise(). But $q takes things to the next level by enhancing additional feature that developers can use to perform complex tasks more simply.
This is a sample for creating a promise using $q
angular.module("app",[])
.controller("ctrl",function($scope,$q){
var work = "resolve";
var promise = $q(function(resolve, reject) {
if (work === "resolve") {
resolve('response 1!');
} else {
reject('Oops... something went wrong');
}
});
promise.then(function(data) {
alert(data)
})
})
$q.defer()
$q.defer() return the instance of the promise constructor. Once you create a defer object there are following methods and properties that you can access from that object
resolve(value) – resolves the derived promise with the value. If the value is a rejection constructed via $q.reject, the promise will be rejected instead.
reject(reason) – rejects the derived promise with the reason. This is equivalent to resolving it with a rejection constructed via $q.reject.
notify(value) - provides updates on the status of the promise's execution. This may be called multiple times before the promise is either resolved or rejected.
promise – {Promise} – promise object associated with this deferred
See the example
angular.module("app",[])
.controller("ctrl",function($scope,$q){
var work = "resolve";
function getData(){
var obj = $q.defer();
if (work === "resolve") {
obj.resolve('response 1!');
} else {
obj.reject('Oops... something went wrong');
}
return obj.promise;
}
getData().then(function(data) {
alert(data)
})
})
$q.all()
If a user need to send multiple request one shot,then the user can use $q.all() service.
$q.all([$http.get('data1.json'),$http.get('data2.json')])
.then(function(response){
console.log(response[0].data) // data1.json response
console.log(response[1].data) // data1.json response
})
In here,there are two http request sent simultaneously to two separate JSON files to get data. The response comes as an array and response order is same as the HTTP request order.
$q.race()
$q.race() is very similar to $q.all(). But instead of sending response of each request, it will only return the one request response. Specifically, only return the response of first request that been executed. That does not mean it’s not going to send other requests. All the requests are sending but it's only return the response of the first request that executed.
$q.race([$http.get('data1.json'),$http.get('data2.json')])
.then(function(response){
console.log(response[0].data) // return one response
})
In here response can be either data1.Json or data2.json. That's the downfall of using this method. Since its return the response of the first executed request, can’t be sure which request response will resolved by the promise. This method useful for bulk requests which you don’t want to see the response of all the requests
Conclusion
Use $q for constructing promises from non-promise Objects/callbacks, and utilize $q.all() and $q.race() to work with existing promises.
I like this question. Because, I too faced this.
This is a service that helps you run functions asynchronously, and use their return values when they are done processing.
Brief Description
Refer example
Promise with $q
Example :
app.service("githubService", function($http, $q) {
var deferred = $q.defer();
this.getAccount = function() {
return $http.get('https://api.github.com/users/haroldrv')
.then(function(response) {
// promise is fulfilled
deferred.resolve(response.data);
// promise is returned
return deferred.promise;
}, function(response) {
// the following line rejects the promise
deferred.reject(response);
// promise is returned
return deferred.promise;
});
};
});
Please see below the structure of my angular application:
I have a page called 'firm.html' which contains a button. Clicking this button executes the code below.
Controller
The controller calls a Service function. The generationInProgress variable is used in an ng-show to toggle the visibility of a loading gif on the HTML page
$scope.generationInProgress = true;
firmService.processFirm(firmRequest).then(function(response) {
window.location.href = "firm/process";
$scope.generationInProgress = false;
});
Firm Service
This is a service that handles the Firm operations with the following function called above
this.processFirm = function(firmRequest) {
return httpService.put('firm/process', firmRequest);
};
HTTP Service
This is a service that handles all calls to the service. It is used by multiple services, including the firmService above. Here is the put method as called above
this.put = function(url, data) {
return promise = $http.post(url, data).success(function(response) {
return response;
}).error(function(response) {
console.log("error");
});
};
If a HTTP error code is returned by the server, obviously the .error function is executed. If I had a dedicated error page, I could just redirect to that page.
However, I need to display the error on the 'firm.html' page while also setting the $scope.generationInProgress back to false so that the loading gif is no longer displayed. None of the code that does this can be located within the httpService because it is a common service used by many different components.
I am unsure how to propagate the error back to the controller in order to accomplish this. Do I just put return response; in both the .success and .error and use an IF statement in the controller to test for the HTTP code? Is there an alternative method?
Any advice is appreciated.
The .success and .error methods have been deprecated. Instead, use the .then and .catch methods.
To chain a successful promise, return data to the .then method. To chain a rejected promise, throw the error response:
this.put = function(url, data) {
//Use .then method
return promise = $http.post(url, data).then(function(response) {
//return to chain success
return response;
//Use .catch method
}).catch(function(response) {
console.log("error");
//throw to chain rejection
throw response;
});
};
From the Docs1:
Deprecation Notice
The $http legacy promise methods .success and .error have been deprecated. Use the standard .then method instead.
You can handle rejected state in controller, than present it as you like in html.
$scope.generationInProgress = true;
$scope.error = "";
firmService.processFirm(firmRequest).then(function(response) {
window.location.href = "firm/process";
$scope.generationInProgress = false;
}, function(err) {
$scope.generationInProgress = false;
$scope.error = "Your customized error message. Caused by: " + err;
});
I have gone through a few replies about using $http service for accessing the properties file, but now sure how it would fit in this scenario
I have created a service that returns the hostnames from the poperties file, the calling client to this service should make a blocking call to the service and proceed only if the property file is read.
var serviceMod = angular.module('serviceModule',[])
.factory('configService', function($http){
return {
getValue: function(key){
$http.get("js/resources/urls.properties").success(function(response){
console.log('how to send this response to clients sync??? ' + response)
})
return ????
}
}
})
someOtherControllr.js
var urlValue = configService.getValue('url')
The problem I am facing is to do with the aync nature of the $http service. By the time the response is received by the callback, the main thread is already finished executing the someOtherController.js
You need to resolve the promise returned by the service. We can just return the $http call and resolve it in our controller (since return $http.get be a promise itself). Check out the AngularJS $q and $http docs for a bettering understanding of the underlying mechanics going on, and observe the following change...
.factory('configService', function($http) {
return {
getValue: function(key) {
return $http.get('js/resources/urls.properties');
}
}
});
var urlValue;
// --asynchronous
configService.getValue('url').then(function(response) {
urlValue = response.data; // -- success logic
});
console.log('be mindful - I will execute before you get a response');
[...]
Simple way - use callback (it will still be async. In fact you cant make it sync) :
getValue: function(key, onSuccess){
$http.get("js/resources/urls.properties").success(function(response){
onSuccess(response);
})
The value for myIdentity is created only once and is persistant saved in the local storage after first usage of the following Angular.js controller.
The function $scope.createIdentity() is a complex function in a more global controller and saves the result to $localstorage.myIdentity.
This perfectly works!!! the createIdentity() function works with Q promise inside for itself.
But in the AppCtrl I have an issue with race conditions because $localstorage.myIdentity is not yet resolved before
my $http XHR Request is fired. So it doesn’t contain any value for myId.
This occurs only the first time the controller is used.
But I need to start the socket.io connection at the first call of the AppCtrl and this is here an bigger problem for me.
At a second usage of AppCtrl the correct value for myIdentity is yet available in $localStorage.
TimeacleControllers.controller('StatusCtrl', ['$scope', '$http', 'Socket', '$localStorage',
function ($scope, $http, Socket, $localStorage) {
if ($localStorage.myIdentity === undefined) {
$scope.createIdentity();
}
var myParams = {
myId: $localStorage.myIdentity
};
$http
.post('http://example.org', myParams)
.success(function (data) {
console.log('received data: ' + data);
Socket.connect();
Socket.on('connected', function () {
console.log("Connection!");
});
Socket.on('message', function (msg) {
console.log("Message: " + msg);
});
})
.error(function (err) {
// Handle login errors here
console.log("Error - " + err);
});
}]);
So what can you do here to make the Ajax Request wait until the $localstorage.myIdentity can be resolved? Please help.
You mentioned about Q promise but having problem with async sequence...?
If createIdentity is returning a promise, just put the http call inside .then
$scope.createIdentity().then(function() {
$http.... // or you can wrap it inside a function
})
Edit: If you have no access to the code that populate the variable (normally happens inside directive), you can setup a one time watch to monitor the value change.
var unwatch = $scope.$watch(function(){
return $localStorage.myIdentity;
}, function(newValue){
if (newValue === undefined) return;
$http...
unwatch();
});
You said $scope.createIdentity() uses a promise (and presumably is an async operation). Make it return a promise for your code to observe. When it resolves, make it resolve the identity.
Promises are chainable. When you return a promise from then, the next then listens for that promise instead of the original promise. In this case, we make the next then listen to the AJAX.
$scope.createIdentity().then(function(identity){
// When createIdentity resolves, use the data for the AJAX.
// Return the AJAX promise for the next chained then.
return $http.get(...);
}).then(function(data){
// When the AJAX completes, use the data for the socket call
Socket.connect();
...
});