Testing asynchronous Ajax call - javascript

I am trying to have the function GetUsername to call the actual implementation and return the username if it is found back into the variable result. I am using Jasmine's done function but the test is not correct. It keeps on passing even if the expected and actual value are not the same. Any help or suggestion would be great! Thanks in advance.
Here are my objects:
var Git = {
VerifyGitUser: function (username, callback) {
$.ajax({
url: 'https://api.github.com/users/' + username
})
.done(function (data) {
callback.call(this, data);
})
.fail(function (data) {
callback.call(this, data);
})
}
}
var User = {
GetUsername: function (username) {
Git.VerifyGitUser(username, function (data) {
if (data.login) {
return data.login;
} else {
return null;
}
});
}
}
Here is my test:
describe('User', function () {
it('should return username', function (done) {
spyOn(Git, 'VerifyGitUser');
spyOn(User, 'GetUsername').and.callThrough();
var result = User.GetUsername('test');
done();
expect(Git.VerifyGitUser).toHaveBeenCalledWith('test');
expect(User.GetUsername).toHaveBeenCalled();
expect(result).toEqual('test');
})
});

There is currently no way to retrieve the data from User.GetUsername whenever it completes, so it will just return undefined. Plus your call to done() is completing the test before you assert anything with expect.
You can have User.GetUsername take a callback, just like you are doing for Git.VerifyGitUser:
var User = {
GetUsername: function (username, callback) {
Git.VerifyGitUser(username, function (data) {
if (data.login) {
callback(data.login);
} else {
callback(null);
}
});
}
}
Now you know when User.GetUserName has completed. So in your test, you can pass User.GetUserName a callback which can call done():
describe('User', function () {
it('should return username', function (done) {
spyOn(Git, 'VerifyGitUser');
spyOn(User, 'GetUsername').and.callThrough();
User.GetUsername('test', function(result) {
expect(Git.VerifyGitUser).toHaveBeenCalledWith('test');
expect(User.GetUsername).toHaveBeenCalled();
expect(result).toEqual('test');
done();
});
})
});
Other thoughts:
Do you need to call the actual API during this test? You can look into returning mock API data from VerifyGitUser using Jasmine spies.
In VerifyGitUser I don't see why you need to force context using callback.call(this, data). It seems like callback(data) is enough.
You may want to look into returning promises from async functions, instead of having them take in callbacks.

Related

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

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.

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

Callback function is not called only once in Service using $timeout

I'm trying to create a service similar to this example. My code is the following:
app.service('Poller', function ($q, $http, $timeout) {
var notification = {};
notification.poll = function (callback, error) {
return $http.get('https://someapi.com').then(function (response) {
if (typeof response.data === 'object') {
if (callback){
callback(response.data);
console.log('tick');
}
} else {
if (error) {
error(response.data);
}
}
$timeout(notification.poll, 10000);
});
}
notification.poll();
return notification;
});
And I try to use it in my controller like this:
Poller.poll(
function(jsonAPI) {
console.log(jsonAPI);
},
function(error) {
console.log('Error:', error);
}
);
The data are being fetched correctly but there seems to be two problems.
The callback function is called only once and not according to the $timeout. I added the conditionals in the service for callback end error because without them it throws an error callback is not a function. When I refresh or change the view the callback function is again called.
The $timeout seems to be triggered twice every 10 seconds and not once.
Use
$timeout(function () {
notification.poll(callback, error);
}, 10000);
instead of
$timeout(notification.poll, 10000);

Angular Factory returns null

I'm trying to build a database in my angular factory:
angular.module("App")
.factory("DatabaseFactory", function () {
var database = null;
var factory = {};
factory.getDatabase = function () {
if (database == null) {
window.sqlitePlugin.openDatabase({
name: "myDB.db",
androidDatabaseImplementation: 2,
androidLockWorkaround: 1
}, function (db) {
database = db;
database.transaction(function (transaction) {
transaction.executeSql(create_user, [], function (transaction, result) {
console.log("table user created: " + JSON.stringify(result));
}, function (error) {
console.log("table user error: " + error.message);
});
}, function (error) {
console.log("transaction error: " + error.message);
}, function () {
console.log("transaction ok");
return database;
});
});
} else {
return database;
}
}
return factory;
});
The creation of the database works, the transaction is also ok. I now provide a service with a function to init the database:
angular.module("App")
.service("DatabaseService", function (DatabaseFactory) {
var database;
function initDatabase() {
console.log("init before database: " + JSON.stringify(database));
database = DatabaseFactory.getDatabase();
console.log("intit after database: " + JSON.stringify(database));
}
return {
initDatabase: function () {
initDatabase();
}
};
});
It gets called on device ready:
angular.module("App", ["ionic", "ngCordova", "App.Home"])
.config(function ($stateProvider, $urlRouterProvider) {
$stateProvider.state("app", {
url: "/app",
abstract: true,
templateUrl: "templates/main.html"
});
$urlRouterProvider.otherwise("/app/home");
})
.run(function ($rootScope, $ionicPlatform, DatabaseService) {
$ionicPlatform.ready(function () {
console.log("ionic Ready");
if (window.cordova && window.cordova.plugins.Keyboard) {
cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
}
if (window.StatusBar) {
StatusBar.styleDefault();
}
DatabaseService.initDatabase();
});
});
The log output:
init before database: + undefined
init after database: + undefined
So the return of the database in my factory returns undefined, but I don't know why. It should return the database since it is correctly initialized.
You can't return the database from the function, because the function that receives it is an asynchronous callback.
You can only use the return statement if the entire function is synchronous (e.g. doesn't do any async work, such as reading from files, connecting to databases, network requests, sockets etc).
In your case, window.sqlitePlugin.openDatabase does some asynchronous work and asks for a callback as the second argument. This callback will be called after the database connection has opened, which will be after your getDatabase function has returned a value.
window.sqlitePlugin.openDatabase({
name: "myDB.db",
androidDatabaseImplementation: 2,
androidLockWorkaround: 1
}, function (db) {
database = db
// this happens at some point in the future
});
// this happens straight away
// at this point database is still undefined
return database;
A good way to test this for future reference is to use console.log to see at what time and in what order your code is run.
window.sqlitePlugin.openDatabase({
// ...
}, function (db) {
database = db
console.log('A');
});
console.log('B');
return database;
You would see that rather than being executed in the order the statements are written, B is logged first, then A second.
If you make your getDatabase method take a callback argument, you can pass the db object into it as soon as it is ready.
factory.getDatabase = function (callback) {
window.sqlitePlugin.openDatabase({
// ...
}, function (db) {
// do some stuff with db, when you are ready
// pass it to the callback, with null as the
// first argument (because there isn't an error
callback(null, db);
});
Then you would rewrite your code to make use of the callback.
DatabaseFactory.getDatabase(function(err, db) {
console.log("intit after database: " + JSON.stringify(database));
});
You might be wondering why the callback has an err argument too.
In node.js, it is considered standard practice to handle errors in asynchronous functions by returning them as the first argument to the current function's callback. If there is an error, the first parameter is passed an Error object with all the details. Otherwise, the first parameter is null.
(From NodeJitsu)
I think you should replace
var database = null;
var factory = {};
by
var factory = {};
and do
return factory.database
in your factory.getDatabase

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

Categories