What should I test for my angular controller? - angularjs

I've learning to use Karma to test my angularjs app. However, not a few of my controllers use multiple services which are http requests that retrieve json which is then loaded to the page. I've been stuck because of two questions I can't answer. 1) How do I mock these services 2) what exactly should I test for my controller Question 2 I find difficult to answer because controller functions depend on services which I use in other controllers. Anyway, let me show one of my controllers and then, the "library" housing my services:
One of my controllers
angular.module('ccApp')
.controller('CountriesCtrl', ['$scope', '$routeParams', '$location','countryInfo', 'getCountries', 'countriesCache', 'getNeighbors',
'buildCountry', '$timeout', '$q',
function($scope, $routeParams, $location, countryInfo, getCountries, countriesCache, getNeighbors,
buildCountry, $timeout, $q){
getCountries.countriesObject.then(function(response){
$scope.geocountries = response.data.geonames;
},
function(response){
alert("error");
});
$scope.toCountry = function(geocountry){
getNeighbors(geocountry.geonameId)
.then(function(response){
buildCountry(geocountry, response);
var path = '/countries/'+countryInfo.name+'/capital';
$location.path(path);
}),
function(response){
alert('Error');
};
};
$scope.goHome = function(){
$location.path('/');
};
}]);
What here should I test in the controller spec?
Here's the library where the services are housed:
angular.module('library', [])
.service('countryInfo', function(){
var country = {
name: '',
pop: '',
area: '',
capital: '',
code: '',
capPop: '',
numNeigh: 0,
neighbors: []
};
return country;
})
.factory('countriesCache', ['$cacheFactory', function($cacheFactory){
return $cacheFactory('countriesCached');
}])
.factory('getCountries', ['$http', function($http){
var request = {
username: 'vman'
};
return { countriesObject : $http({
method: 'GET',
url: 'http://api.geonames.org/countryInfoJSON',
params: request
})};
}])
.factory('getCountry', ['$http', function($http){
return function(countryCode){
return $http({
method: 'GET',
url: 'http://api.geonames.org/countryInfoJSON',
params: { username: 'vman', country: countryCode }
});
};
}])
.factory('getNeighbors', ['$http', function($http){
return function(geonameId){
return $http({
method: 'GET',
url: 'http://api.geonames.org/neighboursJSON',
params: { username: 'vman', geonameId: geonameId }
});
};
}])
.factory('buildCountry', ['countryInfo', '$q', '$timeout', function(countryInfo, $q, $timeout){
return function(geocountry, response){
countryInfo.name = geocountry.countryName;
countryInfo.code = geocountry.countryCode;
countryInfo.pop = geocountry.population;
countryInfo.area = geocountry.areaInSqKm;
countryInfo.capital = geocountry.capital;
countryInfo.neighbors = response.data.geonames;
countryInfo.numNeigh = response.data.geonames.length;
};
}])
.run(['$rootScope', '$location', function($rootScope, $location) {
$rootScope.$on('$routeChangeError', function() {
$location.path('/error');
});
}]);

The following snippet creates a mock of the above service:
module(function($provide) {
$provide.service('demoService', function() {
this.isDemoApi = jasmine.createSpy('isDemoApi');
});
});
//Getting reference of the mocked service
var mockDemoSvc;
inject(function(demoService) {
mockDemoSvc = demoService;
});
To test the controller, firstly you will have to mock all the services which this controller is using by using the above code.
This would help to test controller APIs seperatly. Then you can go ahead to test APIs which are binded with the scope, like: toCountry()

Related

Error: unable to get property 'call' of undefined or null reference

I am using AngularJs. When getting data from controller.js to service.js, I am getting the error. Below is the code used:
//controller.js
angular.module('testApp.controllers', []).
controller('testController', function ($scope, testAPIService, $timeout, $window, $location, $anchorScroll, $http) {
$scope.show = function() {
testAPIService.getDataSummary().success(function (response) {
console.log(response);
}).error(function (response) {
alert(response.responseText);
});
}
});
In Service.js
angular.module('testApp.services', []).
factory('testAPIService', ['$http', function ($http) {
var testAPIService = {};
testAPIService.getDataSummary = function () {
var request = {
url: urlBase + 'GetDataSummary',
method: 'Get',
headers: {
'accept': 'application/json'
}
}
return $http(request);
};
return testAPIService;
}]);
How to fix this? Thanks
This might be the result of including any of your app javascript file before the angularjs file.
Make sure you include angularjs file before the rest of your app files.
You're creating two different modules:
The first module testApp.controllers is created when you create the controller
Another module testApp.services is created when you create the service.
So the controller and the service are never part of the same module!
Try attaching to the same testApp module as follows:
app.js
// With [] means to create a new module.
angular.module('testApp', []);
controller.js
// Without [] means to retrieve an existing module.
angular.module('testApp').
controller('testController', function($scope, testAPIService, $timeout, $window, $location, $anchorScroll, $http) {
$scope.show = function() {
testAPIService.getDataSummary().success(function(response) {
console.log(response);
}).error(function(response) {
alert(response.responseText);
});
}
});
service.js
// Without [] means to retrieve an existing module.
angular.module('testApp').
factory('testAPIService', ['$http', function($http) {
var testAPIService = {};
testAPIService.getDataSummary = function() {
var request = {
url: urlBase + 'GetDataSummary',
method: 'Get',
headers: {
'accept': 'application/json'
}
}
return $http(request);
};
return testAPIService;
}]);
index.html
And change your ng-app directive to point to the testApp module
<html ng-app="testApp">

Sharing data with $resource and an optional parameter

I am trying to share data between controllers - my side nav controller with a list of tables and my main controller witch controls the table itself..
my service is pretty simple:
.factory('TableData', ['$resource',
function($resource) {
return $resource('api/db/:table', {table: '#table'}, {
'save': {method:'POST'},
'update': { method:'PUT' },
'delete':{method:'DELETE'},
'list': {method: 'GET', isArray: true },
'getTable': {method: 'GET', isArray: true }
});
}])
and the controllers:
.controller('SideNavController', function ($scope, $http, $state, $auth, Account, $stateParams, Tables, $rootScope, TableData) {
TableData.list().$promise.then(function(tables){
$scope.tables = tables;
});
$scope.getTable = function(table){
TableData.getTable({table:table}).$promise.then(function(table){
$scope.selectedTable = table;
});
}
});
and the main controller:
.controller('ManagerCtrl', function($scope, $auth, Account, $http, $rootScope, ParamData, UserData, DocParamData, DocTypeData, ParamTypeData, ParamValueData, SysParamValuesData, TableData ) {
$scope.selectedTable = TableData.getTable();
})
how would i share this data? how can i pass th table parameter?
One way you can associate table with $rootScope and since it is the parent scope of all the $scope in angular therefore table can be accessible both of the controllers.
Code:
.controller('SideNavController', function ($scope, $http, $state, $auth, Account, $stateParams, Tables, $rootScope, TableData) {
TableData.list().$promise.then(function(tables){
$scope.tables = tables;
});
$scope.getTable = function(table){
TableData.getTable({table:table}).$promise.then(function(table){
$rootScope.selectedTable = table;
});
}
});
Now this $rootScope.selectedTable can be accessed in both of the controllers.
Second way, You can write one angular service where you can put getTable() method.
Code:
app.service('MyService', function($http, TableData) {
this.getTable = function(table, callback){
TableData.getTable({table:table}).$promise.then(function(table){
callback(table);
});
}
});
Now you can inject this service and call getTable() method which would pass table obj to callback.
.controller('ManagerCtrl', function($scope, $auth, Account, $http, $rootScope, ParamData, UserData, DocParamData, DocTypeData, ParamTypeData, ParamValueData, SysParamValuesData, TableData, MyService ) {
MyService.getTable(table, function(table) {
$scope.selectedTable = table;
});
})

AngularJS pass parameter from Controller to Service

I don't know how to pass data from the controller to the service as method arguments... I need to set in the controller the headers variable which is later added to the service which adds this later on to the http call
controller
angular.module('userControllers', []).
controller('userController', ['$scope', '$q', 'Users', '$location',
function($scope, $q, Users, $location) {
$scope.viewUser = function(userId) {
$location.path('/users/'+userId);
};
$scope.createUser = function () {
$location.path('/users/create');
};
headers = {service:'json',resource:'users'};
$scope.users = Users.query(headers);
$scope.users.$promise.then(function(result){
$scope.users = result;
});
}]);
service
angular.module('userServices', ['ngResource']).
factory('Users', ['$resource', '$q', function($resource, $q){
var factory = {
query: function (headerParams) {
var data = $resource('http://localhost:8080/gateway', {}, {
query: {
method: 'GET',
isArray: true,
headers: headerParams
}
});
var deferred = $q.defer();
deferred.resolve(data);
return deferred.promise;
}
}
return factory;
}]);
In this setup I'm getting Error: [$injector:unpr] Unknown provider: UserProvider <- User ... not really sure how to fix this one out ...
Later edit : code cleanup... posted full code and new error
TypeError: Cannot read property 'then' of undefined
at new <anonymous> (http://localhost/frontend-pdi-angular-js/js/controllers/users.js:16:38)
at invoke (https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular.js:3918:17)
at Object.instantiate (https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular.js:3929:23)
at https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular.js:7216:28
at link (https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular-route.js:913:26)
at nodeLinkFn (https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular.js:6648:13)
at compositeLinkFn (https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular.js:6039:13)
at publicLinkFn (https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular.js:5934:30)
at boundTranscludeFn (https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular.js:6059:21)
at controllersBoundTransclude (https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular.js:6669:18) <div ng-view="" class="ng-scope">
Later edit #2 : some working code, but using no ngResource whatsoever
angular.module('userControllers', ['userServices']).
controller('userController',
['$scope', '$q', 'Users', '$location', '$http',
function($scope, $q, Users, $location, $http) {
headerParams = {service:'json',resource:'users'};
$scope.users = $http({method: 'GET', url: 'http://localhost:8080/gateway', headers: headerParams, isArray:true}).
success(function(data, status, headers, config) {
$scope.users = data;
console.log(data);
}).
error(function(data, status, headers, config) {
console.log(status);
});
}]);
As others mentioned, you are using a non-existent reference to a provider ($promise) in your code. By simply setting the deferred to $scope.users and then acting on the 'then' of that deferred, you can solve this problem.
You can also simplify it a bit. You are setting the deferred on the $scope object and then you're overwriting that with the resolved value of the deferred. So you can do this instead:
Users.query(headers).then(function(result){
$scope.users = result;
});
if you want to assign the deferred to a separate variable:
var deferred = Users.query(headers);
deferred.then(function(result){
$scope.users = result;
});
angular.module('userControllers', ['userServices']).
controller('userController',
['$scope', '$q', 'Users', '$location',
function($scope, $q, Users, $location) {
headers = {service:'json',resource:'users'};
Users.query(headers).then(function(result){
$scope.users = result;
});
}])
angular.module('userServices', ['ngResource']).
factory('Users', ['$resource', '$q', function($resource, $q){
var factory = {
query: function (headerParams) {
var data = $resource('http://localhost:8080/gateway', {}, {
query: {
method: 'GET',
isArray: true,
headers: headerParams
}
});
var deferred = $q.defer();
deferred.resolve(data);
return deferred.promise;
}
}
return factory;
}]);
Try this. Your not include the userServices module into the userControllers module and you can omit $scope.users = Users.query(headers); part because if you put this this will call the query method.
replace controller with this code:
angular.module('userControllers', []). controller('userController', ['$scope', '$q', 'Users', '$location', function($scope, $q, Users, $location) {
$scope.viewUser = function(userId) {
$location.path('/users/'+userId);
};
$scope.createUser = function () {
$location.path('/users/create');
};
headers = {service:'json',resource:'users'};
Users.query(headers).$promise.then(function(result){
$scope.users = result;
});
}]);
and factory with this code :
angular.module('userServices', ['ngResource']).
factory('Users', ['$resource', '$q', function($resource, $q){
var factory = {
query: function (headerParams) {
return $resource('http://localhost:8080/gateway', {}, {
query: {
method: 'GET',
isArray: true,
headers: headerParams
}
})
}
}
return {
query: factory.query
}
}]);
i think this will help you.

AngularJS: How to wait for $resource to finish?

I have a controller:
app.controller('ProductDetailCtrl', ['$scope', '$q', '$resource', 'Product', function($scope, $q, $resource, Product) {
$scope.product = {};
$scope.init = function(id)
{
$scope.product = Product.get({productId: id});
}
$q.all([$scope.product.$promise]).then(function() {
$scope.selected_color = $scope.product.products_colors[0];
});
// $scope.selected_color = $scope.product.products_colors[0];
}]);
And a factory:
app.factory('Product', ['$resource', function($resource) {
return $resource("/api/products/:productId", {}, {
query: {method: 'GET', isArray: true},
});
}]);
However, when I load the page it gives me this:
Error: $scope.product.products_colors is undefined #http://localhost:3000/assets/products/controllers/productscontroller.js?body=1:11:5 qFactory/defer/deferred.promise.then/wrappedCallback#http://localhost:3000/assets/angular.js?body=1:11499:15 qFactory/ref/<.then/<#http://localhost:3000/assets/angular.js?body=1:11585:11 $RootScopeProvider/this.$get</Scope.prototype.$eval#http://localhost:3000/assets/angular.js?body=1:12609:9 $RootScopeProvider/this.$get</Scope.prototype.$digest#http://localhost:3000/assets/angular.js?body=1:12421:15 $RootScopeProvider/this.$get</Scope.prototype.$apply#http://localhost:3000/assets/angular.js?body=1:12713:13 bootstrap/doBootstrap/<#http://localhost:3000/assets/angular.js?body=1:1420:9 invoke#http://localhost:3000/assets/angular.js?body=1:3919:7 bootstrap/doBootstrap#http://localhost:3000/assets/angular.js?body=1:1419:1 bootstrap#http://localhost:3000/assets/angular.js?body=1:1432:5 angularInit#http://localhost:3000/assets/angular.js?body=1:1345:5 #http://localhost:3000/assets/angular.js?body=1:21818:5 jQuery.Callbacks/fire#http://localhost:3000/assets/jquery.js?body=1:3100:1 jQuery.Callbacks/self.fireWith#http://localhost:3000/assets/jquery.js?body=1:3212:7 .ready#http://localhost:3000/assets/jquery.js?body=1:3424:3 completed#http://localhost:3000/assets/jquery.js?body=1:3454:3
return logFn.apply(console, args);
How would I wait until the $resource finishes loading? I want to set one of my $scope variables based on the result of Product.get(..). I know that Product.get(..) is returning and object with a products_colors property because console.log says so
Use a callback:
Product.get({productId: id}, function(data) {
$scope.product = data;
});

AngularJS Service Passing Data Between Controllers

When using an AngularJS service to try and pass data between two controllers, my second controller always receives undefined when trying to access data from the service. I am guessing this is because the first service does a $window.location.href and I'm thinking this is clearing out the data in the service? Is there a way for me to change the URL to a new location and keep the data persisted in the service for the second controller? When I run the code below the alert in the second controller is always undefined.
app.js (Where Service is Defined)
var app = angular.module('SetTrackerApp', ['$strap.directives', 'ngCookies']);
app.config(function ($routeProvider)
{
$routeProvider
.when('/app', {templateUrl: 'partials/addset.html', controller:'SetController'})
.when('/profile', {templateUrl: 'partials/profile.html', controller:'ProfileController'})
.otherwise({templateUrl: '/partials/addset.html', controller:'SetController'});
});
app.factory('userService', function() {
var userData = [
{yearSetCount: 0}
];
return {
user:function() {
return userData;
},
setEmail: function(email) {
userData.email = email;
},
getEmail: function() {
return userData.email;
},
setSetCount: function(setCount) {
userData.yearSetCount = setCount;
},
getSetCount: function() {
return userData.yearSetCount;
}
};
});
logincontroller.js: (Controller 1 which sets value in service)
app.controller('LoginController', function ($scope, $http, $window, userService) {
$scope.login = function() {
$http({
method : 'POST',
url : '/login',
data : $scope.user
}).success(function (data) {
userService.setEmail("foobar");
$window.location.href = '/app'
}).error(function(data) {
$scope.login.error = true;
$scope.error = data;
});
}
});
appcontroller.js (Second controller trying to read value from service)
app.controller('AppController', function($scope, $http, userService) {
$scope.init = function() {
alert("In init userId: " userService.getEmail());
}
});
Define your service like this
app.service('userService', function() {
this.userData = {yearSetCount: 0};
this.user = function() {
return this.userData;
};
this.setEmail = function(email) {
this.userData.email = email;
};
this.getEmail = function() {
return this.userData.email;
};
this.setSetCount = function(setCount) {
this.userData.yearSetCount = setCount;
};
this.getSetCount = function() {
return this.userData.yearSetCount;
};
});
Check out Duncan's answer here:
AngularJS - what are the major differences in the different ways to declare a service in angular?

Resources