Unit Testing: Karma-Jasmine not resolving Promise without implicitly calling $rootScope.$digest(); - javascript

I have an Angular service that makes a call to the server and fetch the user list. The service returns a Promise.
Problem
Promise is not being resolved until and unless I call $rootScope.$digest(); either in the service or in the test itself.
setTimeout(function () {
rootScope.$digest();
}, 5000);
Apparently, Calling $rootScope.$digest(); is a workaround and I cannot call it in the angular service so I am calling it in the unit test with an interval of 5 seconds which I think is a bad practice.
Request
Please suggest an actual solution for this.
Given below is the test that I have written.
// Before each test set our injected Users factory (_Users_) to our local Users variable
beforeEach(inject(function (_Users_, $rootScope) {
Users = _Users_;
rootScope = $rootScope;
}));
/// test getUserAsync function
describe('getting user list async', function () {
// A simple test to verify the method getUserAsync exists
it('should exist', function () {
expect(Users.getUserAsync).toBeDefined();
});
// A test to verify that calling getUserAsync() returns the array of users we hard-coded above
it('should return a list of users async', function (done) {
Users.getUserAsync().then(function (data) {
expect(data).toEqual(userList);
done();
}, function (error) {
expect(error).toEqual(null);
console.log(error.statusText);
done();
});
///WORK AROUND
setTimeout(function () {
rootScope.$digest();
}, 5000);
});
})
service
Users.getUserAsync = function () {
var defered = $q.defer();
$http({
method: 'GET',
url: baseUrl + '/users'
}).then(function (response) {
defered.resolve(response);
}, function (response) {
defered.reject(response);
});
return defered.promise;
}

You can cause the promises to flush with a call to $timeout.flush(). It makes your tests a little bit more synchronous.
Here's an example:
it('should return a list of users async', function (done) {
Users.getUserAsync().then(function (data) {
expect(data).toEqual(userList);
done();
}, function (error) {
expect(error).toEqual(null);
console.log(error.statusText);
done();
});
$timeout.flush();
});
Aside: the failback won't be handled so it adds additional complexity to the test.

Related

Testing async function with jasmine

We are facing an unexpected behavior while testing async code with Jasmine. As far as we know, when you are using the done function, expectations are not called until done is executed. But, that's not happening because the second expectation is failing, hence the $ctrl.todos assignment never happened
Not working test
it('initializes the data when $onIinit', (done) => {
const expected = 'some result';
const response = Promise.resolve(expected);
spyOn(myService, 'getAll').and.returnValue(response);
// This method calls myService.getAll
$ctrl.$onInit();
expect(myService.getAll).toHaveBeenCalled();
expect($ctrl.todos).toEqual(false);
response.then(done);
});
Output: Expected undefined to equal false
On the other hand, this is working:
it('initializes the data when $onIinit', (done) => {
const expected = 'some result';
const response = Promise.resolve(expected);
spyOn(myService, 'getAll').and.returnValue(response);
// This method calls myService.getAll
$ctrl.$onInit();
expect(myService.getAll).toHaveBeenCalled();
response
.then(() => expect($ctrl.todos).toBe(expected))
.then(done);
});
Output: test pass
Controller method:
$ctrl.$onInit = () => {
myService.getAll().then((data) => {
$ctrl.todos = data;
});
};
I found a solution. I don't really need another then. Specs with done will not complete until it's done is called. Also, it should be always placed after expectations.
Working code:
it('initializes the data when $onIinit', (done) => {
const expected = 'some result';
const response = Promise.resolve(expected);
spyOn(myService, 'getAll').and.returnValue(response);
$ctrl.$onInit();
expect(myService.getAll).toHaveBeenCalled();
response
.then(() => {
expect($ctrl.todos).toBe(expected);
done();
});
});
Your aproach seems to be correct, and probably calling done within afterEach will make it works.
afterEach(function(done) {
done();
}, 1000);
But, I would recommend using $httpBackend, the fake HTTP backend implementation suitable for unit testing applications that use the $http service. We are using angularjs anyway, right? So, why not take advantage of?
With $httpBackend we can make the requests, then response with mock data without really sending the request to a real server. Here an ilustrative example
it('initializes the data when $onIinit', () => {
const mockData = { data: { expected: 'some result' } };
spyOn(myService, 'getAll').and.callThrough();
$httpBackend.expect('POST', '/my-service/url').respond(200, mockData);
// This method calls myService.getAll
$ctrl.$onInit();
//flush pending request
$httpBackend.flush();
expect(myService.getAll).toHaveBeenCalled();
expect($ctrl.todos).toBeDefined();
});
Some explanations, $httpBackend.expect('POST', '/my-service/url'), here note that 'POST' need to match the method used by your service in myService.getAll, and '/my-service/url' is the url also used by your service in myService.getAll.
It is required to call $httpBackend.flush();, it will release all pending requests.
You will need to inject $httpBackend into your tests, an easy way would be
describe('$httpBackend service in module ngMock', () => {
let $httpBackend;
beforeEach(inject(['$httpBackend', (_$httpBackend) => {
$httpBackend = _$httpBackend;
}]));
it('$httpBackend is defined', () => {
// here we can use $httpBackend
expect($httpBackend).toBeDefined();
});
});
Also, note that $httpBackend is part of the ngMock module.
More info about testing angularjs code here
Hope it helps
Promise is from outside Angular world - you have to wait for result that will be available in next event queue tick - dirty (almost dirty) hack is to use setTimeout
angular.module('test', [])
.controller('underTest', function($scope, myService) {
$scope.$onInit = function() {
myService.getAll().then(function(data) {
$scope.todos = data
})
}
})
describe('Controller: somethingChanged', function() {
var scope, myService
beforeEach(function() {
module('test')
})
beforeEach(function() {
module(function($provide) {
$provide.value('myService', {
getAll: function() {}
})
})
})
beforeEach(inject(function($controller, _$rootScope_, _myService_) {
myService = _myService_
scope = _$rootScope_.$new()
$controller('underTest', {
$scope: scope
})
}))
it('initializes the data when $onIinit', function(done) {
const expected = 'some result'
const response = Promise.resolve(expected)
spyOn(myService, 'getAll').and.returnValue(response)
scope.$onInit()
expect(myService.getAll).toHaveBeenCalled();
setTimeout(function() {
expect(scope.todos).toEqual(expected)
done()
})
});
});
<script src="https://cdn.jsdelivr.net/jasmine/2.6.1/jasmine.js"></script>
<script src="https://cdn.jsdelivr.net/jasmine/2.6.1/jasmine-html.js"></script>
<script src="https://cdn.jsdelivr.net/jasmine/2.6.1/boot.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/jasmine/2.6.1/jasmine.css" />
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular-mocks.js"></script>

How to reload a http.get request after performing a function

I am trying to delete a post from a list. The delete function is performing by passing serially to a delete function showed below.
$scope.go = function(ref) {
$http.get("api/phone_recev.php?id="+ref)
.success(function (data) { });
}
After performing the function, I need to reload the http.get request which used for listing the list.
$http.get("api/phone_accept.php")
.then(function (response) { });
Once the function performed. The entire list will reload with new updated list. Is there any way to do this thing.
Try this
$scope.go = function(ref) {
$http.get("api/phone_recev.php?id="+ref)
.success(function (data) {
//on success of first function it will call
$http.get("api/phone_accept.php")
.then(function (response) {
});
});
}
function list_data() {
$http.get("api/phone_accept.php")
.then(function (response) {
console.log('listing');
});
}
$scope.go = function(ref) {
$http.get("api/phone_recev.php?id="+ref)
.success(function (data) {
// call function to do listing
list_data();
});
}
Like what #sudheesh Singanamalla says by calling the same http.get request again inside function resolved my problem.
$scope.go = function(ref) {
$http.get("api/phone_recev.php?id="+ref).success(function (data) {
//same function goes here will solve the problem.
});}
});
You can use $q - A service that helps you run functions asynchronously, and use their return values (or exceptions) when they are done processing.
https://docs.angularjs.org/api/ng/service/$q
Inside some service.
app.factory('SomeService', function ($http, $q) {
return {
getData : function() {
// the $http API is based on the deferred/promise APIs exposed by the $q service
// so it returns a promise for us by default
return $http.get("api/phone_recev.php?id="+ref)
.then(function(response) {
if (typeof response.data === 'object') {
return response.data;
} else {
// invalid response
return $q.reject(response.data);
}
}, function(response) {
// something went wrong
return $q.reject(response.data);
});
}
};
});
function somewhere in controller
var makePromiseWithData = function() {
// This service's function returns a promise, but we'll deal with that shortly
SomeService.getData()
// then() called when gets back
.then(function(data) {
// promise fulfilled
// something
}, function(error) {
// promise rejected, could log the error with: console.log('error', error);
//some code
});
};

jasmine 2.0 testing angularjs factory method which returns a promise

when i am trying to test a function which returns a promise i get the following error:
"Error: Timeout - Async callback was not invoked within timeout
specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
"
my spec is as follows:
describe('async promise test', function () {
beforeEach(module('app'));
beforeEach(function () {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 6 * 1000;
})
it('should match the name', function (done) {
inject(function ($rootScope,promiseTest) {
$rootScope.$apply(function(){
var promise =promiseTest.checkPromise();
promise.then(function(data){
console.log(data);
done();
})
})
})
})
});
please check the plunker link for the complete code plunker link
You are using the mock module, which overrides $timeout, so that tests are repeatable (ref)!
You have to use $timeout.flush(1000); (and the $apply() is redundant):
it('should match the name', function (done) {
inject(function (promiseTest,$timeout) {
var promise =promiseTest.checkPromise();
promise.then(function(data){
console.log(data);
done();
});
$timeout.flush(1000);
});
});
See forked plunk: http://plnkr.co/edit/J1EmU7yuCETBd8w9mS1R?p=preview
You have to inject the $timeout into your test, and flush it:
inject(function ($rootScope,promiseTest, $timeout) {
$rootScope.$apply(function(){
var promise =promiseTest.checkPromise();
console.log("Created promise")
promise.then(function(data){
console.log(data);
done();
})
});
$timeout.flush();
See the modified plunker here:
http://plnkr.co/edit/psBl6nROkXHrD1iExKAQ?p=preview

pass data from a service to controller

i have a factory am passing it to controller LoginCtrl
.factory('Fbdata', function(){
var service = {
data: {
apiData: []
},
login: function () {
facebookConnectPlugin.login(["email"],
function() {
facebookConnectPlugin.api("me/?fields=id,email,name,picture", ["public_info","user_birthday"],
function (results) {
service.data.apiData = results;
console.log(service.data.apiData);
return results;
},
function (error) {
console.error('FB:API', error);
});
},
function(err) {
console.error('FB:Login', err);
});
}
};
return service;
})
LoginCtrl:
.controller('LoginCtrl', function($scope, Fbdata){
$scope.login = function(){
if (!window.cordova) {
var appId = "appId";
facebookConnectPlugin.browserInit(appId);
}
$scope.loginData = Fbdata.login();
console.log(Fbdata.data.apiData);
// got empty array []
$scope.retVal= angular.copy(Fbdata.data.apiData);
};
})
the Fbdata.data.apiData return empty array and i only could see the returned data from the login success function in the console .
my template which is has LoginCtrl as controller:
<div class="event listening button" ng-click="login();">Login with Facebook</div>
<h2>{{loginData.name}}</h2>
<h2>{{retVal.name}}</h2>
There is a variety of ways to achieve this, example:
Now I have never used Cordova Facebook Plugin so I'm not sure if you need to run the api function after the log in, or how those procedures need to be ordered. But I wanted to show you an example of how to retrieve the data from the factory using your code sample. Hope that helps
Edit 2:
I have changed my code to using promises that way we make sure that we don't call one without the other being completed, I am not a fan of chaining the login and api functions within one function since it is possible(?) that you may need to call login() but don't want to call api(), please try my code and paste in your console logs in the bottom of your question.
Factory:
// Let's add promises to our factory using AngularJS $q
.factory('Fbdata', ['$q', function($q){
// You could also just replace `var service =` with `return` but I thought this
// would make it easier to understand whats going on here.
var service = {
// I generally nest result to keep it clean when I log
// So results from functions that retrieve results are stored here
data: {
login: [],
api: []
},
api: function() {
var q = $q.defer();
facebookConnectPlugin.api("me/?fields=id,email,name,picture", ["public_info","user_birthday"],
function (results) {
// assign to the object being returned
service.data.api = results;
// The data has returned successfully so we will resolve our promise
q.resolve(results);
},
function (error) {
// We reject here so we can inform the user within through the error/reject callback
q.reject(error);
console.error('FB:API', error);
});
// Now that we have either resolved or rejected based on what we received
// we will return the promise
return q.promise;
},
login: function () {
var q = $q.defer();
facebookConnectPlugin.login(["email"], function (results) {
// assign to the object being returned
service.data.login = results;
q.resolve(results);
}, function(err) {
q.reject(error);
console.error('FB:Login', err);
});
return q.promise;
}
};
return service;
}])
Controller:
.controller('LoginCtrl', function($scope, Fbdata){
$scope.login = function(){
if (!window.cordova) {
var appId = "appid";
facebookConnectPlugin.browserInit(appId);
}
// By using the promises in our factory be can ensure that API is called after
// login by doing the following
// then(success, reject) function allows us to say once we have a return, do this.
Fbdata.login().then(function () {
$scope.loginData = Fbdata.data.login;
// Check what was returned
console.log('loginData', $scope.loginData);
Fbdata.api().then(function () {
$scope.apiData = Fbdata.data.api;
console.log('apiData', $scope.apiData);
}, function () {
// Tell the user that the api failed, if necessary
});
}, function () {
// Tell the user that the log in failed
});
};
});

Setting a timeout handler on a promise in angularjs

I'm trying to set a timeout in my controller so that if a response isn't received in 250ms it should fail. I've set my unit test to have a timeout of 10000 so that this condition should be met,Can anyone point me in the right direction? ( EDIT I'm trying to achieve this without using the $http service which I know provides timeout functinality)
(EDIT - my other unit tests were failing because I wasn't calling timeout.flush on them, now I just need to get the timeout message kicking in when an undefined promise is returned by promiseService.getPromise(). I've removed the early code from the question) .
promiseService (promise is a test suite variable allowing me to use different behaviour for the promise in each test suite before apply, eg reject in one, success in another)
mockPromiseService = jasmine.createSpyObj('promiseService', ['getPromise']);
mockPromiseService.getPromise.andCallFake( function() {
promise = $q.defer();
return promise.promise;
})
Controller function that's being tested -
$scope.qPromiseCall = function() {
var timeoutdata = null;
$timeout(function() {
promise = promiseService.getPromise();
promise.then(function (data) {
timeoutdata = data;
if (data == "promise success!") {
console.log("success");
} else {
console.log("function failure");
}
}, function (error) {
console.log("promise failure")
}
)
}, 250).then(function (data) {
if(typeof timeoutdata === "undefined" ) {
console.log("Timed out")
}
},function( error ){
console.log("timed out!");
});
}
Test (normally I resolve or reject the promise in here but by not setting it I'm simulating a timeout)
it('Timeout logs promise failure', function(){
spyOn(console, 'log');
scope.qPromiseCall();
$timeout.flush(251);
$rootScope.$apply();
expect(console.log).toHaveBeenCalledWith("Timed out");
})
First, I would like to say that your controller implementation should be something like this:
$scope.qPromiseCall = function() {
var timeoutPromise = $timeout(function() {
canceler.resolve(); //aborts the request when timed out
console.log("Timed out");
}, 250); //we set a timeout for 250ms and store the promise in order to be cancelled later if the data does not arrive within 250ms
var canceler = $q.defer();
$http.get("data.js", {timeout: canceler.promise} ).success(function(data){
console.log(data);
$timeout.cancel(timeoutPromise); //cancel the timer when we get a response within 250ms
});
}
Your tests:
it('Timeout occurs', function() {
spyOn(console, 'log');
$scope.qPromiseCall();
$timeout.flush(251); //timeout occurs after 251ms
//there is no http response to flush because we cancel the response in our code. Trying to call $httpBackend.flush(); will throw an exception and fail the test
$scope.$apply();
expect(console.log).toHaveBeenCalledWith("Timed out");
})
it('Timeout does not occur', function() {
spyOn(console, 'log');
$scope.qPromiseCall();
$timeout.flush(230); //set the timeout to occur after 230ms
$httpBackend.flush(); //the response arrives before the timeout
$scope.$apply();
expect(console.log).not.toHaveBeenCalledWith("Timed out");
})
DEMO
Another example with promiseService.getPromise:
app.factory("promiseService", function($q,$timeout,$http) {
return {
getPromise: function() {
var timeoutPromise = $timeout(function() {
console.log("Timed out");
defer.reject("Timed out"); //reject the service in case of timeout
}, 250);
var defer = $q.defer();//in a real implementation, we would call an async function and
// resolve the promise after the async function finishes
$timeout(function(data){//simulating an asynch function. In your app, it could be
// $http or something else (this external service should be injected
//so that we can mock it in unit testing)
$timeout.cancel(timeoutPromise); //cancel the timeout
defer.resolve(data);
});
return defer.promise;
}
};
});
app.controller('MainCtrl', function($scope, $timeout, promiseService) {
$scope.qPromiseCall = function() {
promiseService.getPromise().then(function(data) {
console.log(data);
});//you could pass a second callback to handle error cases including timeout
}
});
Your tests are similar to the above example:
it('Timeout occurs', function() {
spyOn(console, 'log');
spyOn($timeout, 'cancel');
$scope.qPromiseCall();
$timeout.flush(251); //set it to timeout
$scope.$apply();
expect(console.log).toHaveBeenCalledWith("Timed out");
//expect($timeout.cancel).not.toHaveBeenCalled();
//I also use $timeout to simulate in the code so I cannot check it here because the $timeout is flushed
//In real app, it is a different service
})
it('Timeout does not occur', function() {
spyOn(console, 'log');
spyOn($timeout, 'cancel');
$scope.qPromiseCall();
$timeout.flush(230);//not timeout
$scope.$apply();
expect(console.log).not.toHaveBeenCalledWith("Timed out");
expect($timeout.cancel).toHaveBeenCalled(); //also need to check whether cancel is called
})
DEMO
The behaviour of "failing a promise unless it is resolved with a specified timeframe" seems ideal for refactoring into a separate service/factory. This should make the code in both the new service/factory and controller clearer and more re-usable.
The controller, which I've assumed just sets the success/failure on the scope:
app.controller('MainCtrl', function($scope, failUnlessResolvedWithin, myPromiseService) {
failUnlessResolvedWithin(function() {
return myPromiseService.getPromise();
}, 250).then(function(result) {
$scope.result = result;
}, function(error) {
$scope.error = error;
});
});
And the factory, failUnlessResolvedWithin, creates a new promise, which effectively "intercepts" a promise from a passed in function. It returns a new one that replicates its resolve/reject behaviour, except that it also rejects the promise if it hasn't been resolved within the timeout:
app.factory('failUnlessResolvedWithin', function($q, $timeout) {
return function(func, time) {
var deferred = $q.defer();
$timeout(function() {
deferred.reject('Not resolved within ' + time);
}, time);
$q.when(func()).then(function(results) {
deferred.resolve(results);
}, function(failure) {
deferred.reject(failure);
});
return deferred.promise;
};
});
The tests for these are a bit tricky (and long), but you can see them at http://plnkr.co/edit/3e4htwMI5fh595ggZY7h?p=preview . The main points of the tests are
The tests for the controller mocks failUnlessResolvedWithin with a call to $timeout.
$provide.value('failUnlessResolvedWithin', function(func, time) {
return $timeout(func, time);
});
This is possible since 'failUnlessResolvedWithin' is (deliberately) syntactically equivalent to $timeout, and done since $timeout provides the flush function to test various cases.
The tests for the service itself uses calls $timeout.flush to test behaviour of the various cases of the original promise being resolved/rejected before/after the timeout.
beforeEach(function() {
failUnlessResolvedWithin(func, 2)
.catch(function(error) {
failResult = error;
});
});
beforeEach(function() {
$timeout.flush(3);
$rootScope.$digest();
});
it('the failure callback should be called with the error from the service', function() {
expect(failResult).toBe('Not resolved within 2');
});
You can see all this in action at http://plnkr.co/edit/3e4htwMI5fh595ggZY7h?p=preview
My implementation of #Michal Charemza 's failUnlessResolvedWithin with a real sample.
By passing deferred object to the func it reduces having to instantiate a promise in usage code "ByUserPosition". Helps me deal with firefox and geolocation.
.factory('failUnlessResolvedWithin', ['$q', '$timeout', function ($q, $timeout) {
return function(func, time) {
var deferred = $q.defer();
$timeout(function() {
deferred.reject('Not resolved within ' + time);
}, time);
func(deferred);
return deferred.promise;
}
}])
$scope.ByUserPosition = function () {
var resolveBy = 1000 * 30;
failUnlessResolvedWithin(function (deferred) {
navigator.geolocation.getCurrentPosition(
function (position) {
deferred.resolve({ latitude: position.coords.latitude, longitude: position.coords.longitude });
},
function (err) {
deferred.reject(err);
}, {
enableHighAccuracy : true,
timeout: resolveBy,
maximumAge: 0
});
}, resolveBy).then(findByPosition, function (data) {
console.log('error', data);
});
};

Categories