Controller methods call Jasmine unit test suite with Karma in AngularJs - javascript

I am pretty new to Karma and Jasmine which I am using to write unit tests for my AngularJs application.
While running the test suite functions which I have called to initialize data in my controller is getting called by default. Is there any way to avoid this call while running the test suite?
Find the code snippet below:
BaseCtrl.js
App.controller('BaseCtrl', ['$scope', '$rootScope', 'Data', function($scope, $rootScope, Data){
//initialize
$rootScope.loader = false;
$rootScope.data = false;
$scope.init = function(){
$rootScope.loader = true;
Data.init(function(data){
$rootScope.loader = false;
$rootScope.data = data;
});
};
$scope.init();
}])
Data.js
App.service('Data', ['$http', function($http){
return {
init:function(callback){
$http.get('/url').success(function(response){
callback(response);
});
}
};
}]);
BaseCtrl.spec.js
describe('BaseCtrl', function(){
beforeEach(module('app'));
var $controller, $rootScope, $scope;
beforeEach(inject(function(_$controller_, _$rootScope_){
$controller = _$controller_;
$rootScope = _$rootScope_.$new();
}));
describe('Loader', function(){
var controller, $scope = {};
beforeEach(function(){
controller = $controller('BaseCtrl', {$scope:$scope, $rootScope:$rootScope});
});
it('should be false by default', function(){
// Check the default value
expect($rootScope.loader).toBe(false);
// Some code to determine the call, then
expect($rootScope.loader).toBe(true);
// Some code to determine the call is done, then
expect($rootScope.loader).toBe(false);
});
});
});

Yes, use $httpBackend to create a mock and return whatever you want.
$httpBackend.when('GET', '/url').respond({});

Related

AngularJS testing controller with jasmine and spyOn gives error 'method undefined'

So I am writing unit tests for an application in angularJS using Jasmine.
I have a controller with an "init" method which calls "secondMethod" and "thirdMethod"
I want to test with a jasmine spyOn whether "secondMethod" is called correctly.
My controller looks like this:
function init() {
secondMethod().then(function () {
thirdMethod();
});
}
init();
function secondMethod(){
//do something
}
function thirdMethod(){
//do something
}
and my test file looks like this:
describe("nameOfTheController", function () {
var $rootScope,
$controller,
$scope,
controller;
beforeEach(function () {
angular.mock.module("myModule");
inject(function (_$controller_, _$rootScope_) {
$rootScope = _$rootScope_;
$controller = _$controller_;
$scope = $rootScope.$new();
controller = $controller('nameOfTheController', {
'$scope': $scope
});
});
});
describe("init", function(){
it('should run secondMethod', function(){
spyOn(controller, 'secondMethod');
expect(controller.secondMethod).toHaveBeenCalled();
});
it('should run thirdMethod', function(){
spyOn(controller, 'thirdMethod');
expect(controller.thirdMethod).toHaveBeenCalled();
});
As you can see I inject the controller in beforeEach but I get error that method "secondMethod" and "thirdMethod" are not defined and I am not quite sure why.
I have also tried doing something like the following but to no avail
controller:
var vm = this;
vm.init = function() {
vm.secondMethod().then(function () {
vm.thirdMethod();
});
}
vm.init();
vm.secondMethod = function(){
//do something
}
vm.thirdMethod = function(){
//do something
}
testfile:
describe("nameOfTheController", function () {
var $rootScope,
$controller,
$scope,
controller;
beforeEach(function () {
angular.mock.module("myModule");
inject(function (_$controller_, _$rootScope_) {
$rootScope = _$rootScope_;
$controller = _$controller_;
$scope = $rootScope.$new();
controller = $controller('nameOfTheController', {
'$scope': $scope
});
});
});
describe("init", function(){
it('should run secondMethod', function(){
spyOn(controller, 'secondMethod');
expect(controller.secondMethod).toHaveBeenCalled();
});
it('should run thirdMethod', function(){
spyOn(controller, 'thirdMethod');
expect(controller.thirdMethod).toHaveBeenCalled();
});
Does anyone know why second and third method are undefined?
EDIT:
The reason why second and third method returned undefined when prefixing with "vm." was that the init function was called before the second and third method had been defined.
Moving the call of init to be below the second and third method definitions solved the problem. Now I just have the problem that the spy expects the method to be called but it doesn't get called
var vm = this;
vm.init = function() {
vm.secondMethod().then(function () {
vm.thirdMethod();
});
}
vm.secondMethod = function(){
//do something
}
vm.thirdMethod = function(){
//do something
}
vm.init();
The controller is initiated in beforeEach(), and that is when you init() => seconMethod(), while you spy on it only in the it() block.
On the other hand, you can't spy before, since you won't have the controller object.
IMO, the solution will be to revise your code and call init() explicitally:
it('should run secondMethod', function() {
spyOn(controller, 'secondMethod');
expect(controller.secondMethod).not.toHaveBeenCalled();
controller.init();
expect(controller.secondMethod).toHaveBeenCalled();
});
https://jsfiddle.net/ronapelbaum/v9vyLpws/

Better way to reference $scope in AngularJS

This is how I have my JS set up:
Basically I have a page, and on that page there is a chart. I want to have a loading spinner show while the chart data is loading.
angular.module('myApp', [])
.service('chartService', ['$http', function($http) {
var svc = {};
svc.updateChartData = function($scope) {
$scope.loading = true;
$http({method: 'GET', url: 'http://example.com/getjson'})
.success(function(response) {
var data = google.visualization.arrayToDataTable(JSON.parse(response));
var options = {
...
};
var chart = new google.visualization.ComboChart(document.getElementById('chart_div'));
chart.draw(data, options);
$scope.loading = false;
});
}
return svc;
}])
.controller('PageController', ['$scope', '$http', 'chartService', function($scope, $http, chartService) {
$scope.loading = true;
// When select option changes
$scope.updateData = function() {
chartService.updateChartData($scope);
};
}])
.controller('ChartController', ['$scope', '$http', 'chartService', function($scope, $http, chartService) {
// On load
chartService.updateChartData($scope);
}]);
I am using ng-hide="loading" and `ng-show="loading" to make sure the spinner and the chart show at the correct times.
However, I've noticed that the call below // On load - doesn't actually turn the loading to false. Another message on SO suggested there is a better way to achieve this than by passing $scope around so any suggestions would be appreciated. Thank you.
It is not a good practice to pass your scope object to a service, a service is meant to be stateless. Instead utilize the callbacks of the $http:
chartService.updateChartData().finally(function(){
$scope.loading = false;
});
And, as Grundy mentioned below, return your $http from your service to enable callbacks:
svc.updateChartData = function($scope) {
return $http({ //.. the options });
}
I see some more bad practices though. You shouldn't add the data to your DOM from your service, instead utilize also for this the callbacks:
svc.updateChartData = function($scope) {
return $http({method: 'GET', url: 'http://example.com/getjson'});
}
controller:
// When select option changes
$scope.updateData = function() {
chartService.updateChartData().then(function(data) {
// success
// do something with the return data from the http call
}, function (error) {
// error
// handle error
}).finally (function() {
// always
$scope.loading = false;
});
};
For your google chart it would make most sense to create a directive.
first,you have two controllers,I'm assuming they are nested relations.
PageController include ChartController.
you want to change the value of the parent controller in the child controller.
You must use a reference type rather than a value type.
$scope.loading =true;
change to
$scope.loading ={status:true};
and if you want to set false,Should be
$scope.loading.status =false;
NOT
$scope.loading ={status:false};
second, you can pass a callback function to service.
like this
svc.updateChartData = function(callback) {
....
.success(){
callback();
}
}
controller code change to
.controller('ChartController', ['$scope', '$http', 'chartService',
function($scope, $http, chartService) {
// On load
chartService.updateChartData(function(){
$scope.loading =true;
});
}]);

Trying to test Angular with Karma/Jasmine, but can't get the Jasmine tests to set $scope correctly

My Angular script looks like this:
var app = angular.module("myApp", []);
app.controller('TestController', function ($scope) {
$scope.test= "TEST";
});
My test file looks like this:
describe('first test', function() {
var $scope;
beforeEach(function (){
module('myApp');
inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
ctrl = $controller('TestController', {
$scope: $scope
});
});
it('scope should have test', function() {
expect($scope.test).toEqual("TEST");
});
});
This test fails saying $scope.test is undefined. I debugged in Chrome and saw that $scope has a bunch of properties on it, but none of them are test. I've looked through several examples online, and they all look pretty similar to this. i'm not quite sure what i'm doing wrong here and i'm stuck....
edit
I tried adding $controller to inject, but i'm still having the same problem.
You need to pass $controller service alongside with $rootScope:
inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
ctrl = $controller('TestController', {
$scope: $scope
});
});

Jasmine angularjs - spying on a method that is called when controller is initialized

I am currently using Jasmine with Karma(Testacular) and Web Storm to write unit test. I am having trouble spying on a method that gets called immediately when the controller is initialized. Is it possible to spy on a method that is called when the controller is initialized?
My controller code, the method I am attempting to spy on is getServicesNodeList().
myApp.controller('TreeViewController', function ($scope, $rootScope ,$document, DataServices) {
$scope.treeCollection = DataServices.getServicesNodeList();
$rootScope.viewportHeight = ($document.height() - 100) + 'px';
});
And here is the test spec:
describe("DataServices Controllers - ", function () {
beforeEach(angular.mock.module('myApp'));
describe("DataServicesTreeview Controller - ", function () {
beforeEach(inject(function ($controller, $rootScope, $document, $httpBackend, DataServices) {
scope = $rootScope.$new(),
doc = $document,
rootScope = $rootScope;
dataServices = DataServices;
$httpBackend.when('GET', '/scripts/internal/servicedata/services.json').respond(...);
var controller = $controller('TreeViewController', {$scope: scope, $rootScope: rootScope, $document: doc, DataServices: dataServices });
$httpBackend.flush();
}));
afterEach(inject(function($httpBackend){
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
}));
it('should ensure DataServices.getServicesNodeList() was called', inject(function ($httpBackend, DataServices) {
spyOn(DataServices, "getServicesNodeList").andCallThrough();
$httpBackend.flush();
expect(DataServices.getServicesNodeList).toHaveBeenCalled();
}));
});
});
The test is failing saying that the method has not been called. I know that I should mock the DataServices and pass that into the test controller. But it seems like I would still have the same problem when spying on that method whether it is a mock or not. Anyone have any ideas or could point me to resources on the correct way to handle this?
When writing unit tests, you should isolate each piece of code. In this case, you need to isolate your service and test it separately. Create a mock of the service and pass it to your controller.
var mockDataServices = {
getServicesNodeList: function () {
return <insert your sample data here > ;
}
};
beforeEach(inject(function ($controller, $rootScope, $document) {
scope = $rootScope.$new(),
doc = $document,
rootScope = $rootScope;
var controller = $controller('TreeViewController', {
$scope: scope,
$rootScope: rootScope,
$document: doc,
DataServices: mockDataServices
});
}));
If it is your service that is making the $http request, you can remove that portion from your unit controller test. Write another unit test that tests that the service is making the correct http calls when it is initialized.

Inject environment details into AngularJS controller

Let's take the example from AngularJS tutorial
function PhoneListCtrl($scope, $http) {
$http.get('phones/phones.json').success(function(data) {
$scope.phones = data;
});
$scope.orderProp = 'age';
}
//PhoneListCtrl.$inject = ['$scope', '$http'];
Now, lets say I don't want to hard code the url 'phones/phones.json' and would prefer the page which hosts this controller to inject it, what should be the right way of doing the same in Angular JS?
There are a lot of ways to do this... the simplest way would just be to use $window, so you'd inject the $window service, and that's basically just the global $window that's been injected. Then you can register those paths as window.path = 'whatever.json'; and you'll be fine:
window.path = 'some/path.json';
function PhoneListCtrl($scope, $http, $window) {
$http.get($window.path).success(function(data) {
$scope.phones = data;
});
$scope.orderProp = 'age';
}
A more advanced way would be to create a module with a service that you inject into your app, in this case each page would have it's own module:
//create your module.
angular.module('configData', [])
.factory('pathService', function () {
return {
path: 'some/path.json'
};
});
//then inject it into your app
var app = angular.module('myApp', ['configData']);
app.controller('PhoneListCtrl', function($scope, $http, pathService) {
$http.get(pathService.path).success(function(data) {
$scope.phones = data;
});
$scope.orderProp = 'age';
});
You can, of course, do anything in between the two. I would suggest taking the path that is the most maintainable while still easy to test.

Categories