jasmine test does not move into the then part of the promise - javascript

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.

Related

Javascript - finally executes before try block finishes with AngularJS

I'm using AngularJS application. I have a form. On submit I'm calling a function. I have used the javascript try/catch/finally block
$scope.save = function() {
try {
//Block of code to try
$scope.submit.text = "Submitting";
$scope.submit.disable = true;
$timeout(function(){
alert('successfully saved');
}, 5000);
}
catch(err) {
//Block of code to handle errors
}
finally {
alert("finally");
$scope.submit.text = "Submit";
$scope.submit.disable = false;
}
}
I used a timer for now. But later I may use AJAX call. the problem is
finally block gets executed before the time finishes. How to solve this?
The reason why the finally method gets executed before the time is finished is that javascript methods are not running asynchronous. Promises will solve this problem.
Here is how promise is defined in q.js which is used in angular:
A promise is an object that represents the return value or the thrown
exception that the function may eventually provide. A promise can also
be used as a proxy for a remote object to overcome latency.
One of the guarantees promises make is that the either the success or the error callback will be invoked, but never both. What happens if you need to ensure a specific function executes regardless of the result of the promise? You can do this by registering that function on the promise using the finally() method.
Suppose you have a function getData() where you are making some $http request and obtain some data from backend. Then you can use:
var promise = getData()
.then(function(data) {
console.log(data)
}, function(error) {
console.error(error)
})
.finally(function() {
console.log()
})
})

Mocked 404 preventing $http promise resolution/rejection

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');
});
});

Jasmine async test using promises

I am doing some Jasmine testing using angular promises and have a question related to timing. Found an answer here Unit-test promise-based code in Angular, but need clarification about how this works. Given that the then method is always handled in an asynchronous way how is the following test guaranteed to pass. Isn't there are risk that the expect will run ahead of the then block being executed and run the expect before the value has been assigned. Or... does the digest cycle guarantee that the value will be assigned before the expect runs. Meaning, a digest cycle will effectively behave like a blocking call that guarantees that the promises are all resolved before the code is allowed to proceed.
function someService(){
var deferred = $q.defer();
deferred.resolve(myObj);
return deferred.promise;
}
it ('testing promise', function() {
var res;
var res2;
someService().then(function(obj){
res = "test";
});
someService().then(function(obj){
res2 = "test2";
});
$rootScope.$apply();
expect(res).toBe('test');
expect(res2).toBe('test2');
});
a digest cycle will effectively behave like a blocking call that guarantees that the promises are all resolved before the code is allowed to proceed.
Yes, although more accurately it would guarantee that the success callbacks of resolved promises would have run.
There is a very similar example that shows how the digest cycle is tied to the success callbacks of promises in the docs for $q
While Michal's answer points to the right idea, the key here is that $apply() is called on the pertinent scope. Here's the example from the Angular docs:
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);
}));

variable inside controller not getting resolved to data resolved by promise

//inside a service PService
this.getPTypes = function(){
var types = PTypesFactory.get({});
return types.$promise.then(function(result)
{
console.log(result.groups);
return result.groups;
});
}
//inside a controller
$scope.groups = PService.getPTypes();
console log shows correct fetched REST data, but when I do
console.log($scope.groups);
I get
Object {then: function, catch: function, finally: function}
which is promise API instead of the correct resolved data.
The problem is that you trying to use a asynchronous function like it was a synchronous one.
then is a method which returns a promise.
when invoking it with a callback that callback would not be invoked immediately, only when the response get back from the server.
You can write it like so:
Service
this.getPTypes = function(callback){
PTypesFactory.get({}).then(callback);
}
Controller
PService.getPTypes(function(res){
$scope.groups = res.data;
});
Promises are used to handle asynchronous operations. The function you pass to the then method is called at some indeterminable point in time. You can't return a value from within it to some other point in execution.
Instead of calling then in your service, just return the promise:
this.getPTypes = function(){
return PTypesFactory.get({}).$promise;
}
and handle its resolution in the controller:
$scope.groups = PService.getPTypes().then(function(result) {
console.log(result.groups);
});

Unit testing an asynchronous service in angularjs

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.

Categories

Resources