How to test $scope.$watch properly with Jasmine in AngularJS - angularjs

I'm new to AngularJS and unit testing,
I'm testing a list that gets changing by selected category.
The test is passing but only if I use the httpBackend.expectGET() that expects the XMLHttpRequest from the "getSomethingElse" method.
I also tried to use the scope.$digest() but I got the same results...
The Controller:
app.controller('mainCtrl', ['$scope', 'myService', function($scope,
myService) {
$scope.category = null;
myService.getSomethingElse().then(function(res) {
$scope.somethingElse = res.data;
});
$scope.$watch('category', function() {
if ($scope.category !== null) {
myService.getListByCat($scope.category.name).then(function(res) {
$scope.list = res.data;
});
}
else {
myService.getLongList().then(function(res) {
$scope.list = res.data;
});
}
});
}]);
The Service:
app.service('myService', ['$http', function($http) {
this.getListByCat = function(category) {
return $http.get('getting-list?cat=' + category);
};
this.getLongList = function() {
return $http.get('getting-long-list');
};
this.getSomethingElse = function() {
return $http.get('getting-something-else');
};
}]);
The Test
describe('Testing mainCtrl', function() {
var scope, ctrl;
var myServiceMock = {
getSomethingElse: jasmine.createSpy().and.returnValue(1),
getListByCat: jasmine.createSpy().and.returnValue(2)
};
beforeEach(function() {
module('app');
inject(function($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('mainCtrl', {
$scope: scope,
myService: myServiceMock
});
});
});
it('should update the list by selected category', function() {
expect(scope.category).toBeNull();
expect(scope.list).toBeUndefined();
scope.category = {
id: 1,
name: 'Jobs'
};
scope.$apply();
expect(myServiceMock.getSomethingElse).toHaveBeenCalled();
expect(myServiceMock.getListByCat).toHaveBeenCalled();
});
});

The test is passing but only if I use the httpBackend.expectGET() that expects the XMLHttpRequest from the "getSomethingElse" method.
This is because your myServiceMock is not replacing the original myService. You have various ways to test this - one of them is given below. Here we are replacing myService with the service mock:-
beforeEach(function() {
module('app');
module(function($provide){
$provide.factory('myServiceMock',
function(){
return myServiceMock;
);
});
inject(function($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('mainCtrl', {
$scope: scope,
myService: myServiceMock
});
});
});

You can add your watcher like this.
$scope.categoryWatcher = categoryWatcher;
$scope.$watch('category', categoryWatcher);
function categoryWatcher() {
if ($scope.category !== null) {
myService.getListByCat($scope.category.name).then(function(res) {
$scope.list = res.data;
});
}
else {
myService.getLongList().then(function(res) {
$scope.list = res.data;
});
}
}
and in Unit testing just create new it construct for that handler
it('should test categoryWatcher for null value', function(){
$scope.category = null;
$scope.categoryWatcher();
// your expectations
});
it('should test categoryWatcher for "desiredValue" value', function(){
$scope.category = "desiredValue";
$scope.categoryWatcher();
// your expectations
});
that way, if&else clauses will be taken in the test.

Related

karma test returning validateAmount is not a function

I'm trying to test scope function which will check due amount, but while running the test I'm getting validateAmount is not a function.
app.js
var ManagmentApp = angular.module("ManagemntApp", ['ngRoute', 'angularModalService', 'ng-fusioncharts']);
ManagmentApp.config(['$routeProvider', function ($routeProvider){
$routeProvider.when('/', {
templateUrl: 'templates/CandidateForm.html',
controller: 'cntrlrCandidate'
}).when('/CandidateList',{
templateUrl: 'templates/CandidateList.html',
controller: 'cntrlrCandidateList'
}).when('/CandidatesProfile', {
templateUrl: 'templates/CandidateProfiles.html',
controller: 'cntrlrCandidateProfile'
}).when('/HostelStatistics', {
templateUrl: 'templates/HostelStatistics.html',
controller: 'cntrlHostelStatistics'
}).when('/:id', {
templateUrl: 'templates/CandidateForm.html',
controller: 'cntrlrCandidate'
});
}]);
cntrlrCandidate.js
ManagmentApp.controller("cntrlrCandidate", ["$scope", "$routeParams", "HostelManagementService", "$filter","HostelManagementIndexDBService", function ($scope, $routeParams, HostelManagementService, $filter,HostelManagementIndexDBService) {
$scope.validateAmount = function () {
var vrAmount = $scope.Candidate.Amount;
var vrTotalAmount = $scope.PerMonthCharge;
if (+vrAmount < +vrTotalAmount) {
$scope.IsValidAmount = false;
$scope.Candidate.DueAmount = (+vrTotalAmount - +vrAmount);
} else {
$scope.IsValidAmount = true;
$scope.Candidate.DueAmount = 0;
}
}
}]);
test.js
describe("ManagemntApp ", function() {
beforeEach(module('ManagemntApp'));
var scope;
var cntrlrCandidate,
$location;
beforeEach(inject(function ($controller,$rootScope){
scope = $rootScope.$new();
cntrlrCandidate = function() {
return $controller('cntrlrCandidate', {
'$scope': scope
});
};;
}));
it('test amount', function() {
scope.Candidate={};
scope.Candidate.Amount=2000;
scope.validateAmount();
expect(scope.IsValidAmount).toEqual(true);
});
});
I couldn't figure out what making notice.
This is the error I'm getting.
Update1:
When I wrote like this the error message is below.
beforeEach(inject(function ($controller,$rootScope){
scope = $rootScope.$new();
// cntrlrCandidate = function() {
cntrlrCandidate= $controller('cntrlrCandidate', {
'$scope': scope
});
// };;
}));
Please check this error:
Update 2:
I tried this way, please correct me if I did anything wrong.
describe("ManagemntApp ", function() {
var HostelManagementIndexDBService,HostelManagementService;
beforeEach(module('ManagemntApp'));
var scope;
var cntrlrCandidate,
$location;
beforeEach(inject(function ($controller,$rootScope){
scope = $rootScope.$new();
// cntrlrCandidate = function() {
HostelManagementService = {};
HostelManagementIndexDBService = {};
module(function($provide) {
$provide.value('HostelManagementService', HostelManagementService);
$provide.value('HostelManagementIndexDBService', HostelManagementIndexDBService);
});
cntrlrCandidate= $controller('cntrlrCandidate', {
'$scope': scope
});
// };;
}));
it('return will pass the Amount', function() {
scope.Candidate={};
scope.Candidate.Amount=2000;
scope.validateAmount();
expect(scope.IsValidAmount).toEqual(true);
});
});
cntrlrCandidate = function() {
return $controller('cntrlrCandidate', {
'$scope': scope
});
};
You have defined the function cntrlrCandidate but not calling anywhere.
You need to call it first then you will get your controller;
Add this line after the cntrlrCandidate defination;
var ctrlCandidate =cntrlrCandidate ();
OR
better if you define like this instead of defining above function.
cntrlrCandidate = $controller('cntrlrCandidate', {'$scope': scope});
EDIT :
Your controller required following Dependency HostelManagementService and HostelManagementIndexDBService which you not provided.So,you need to mock up it.
Add the following script.
var HostelManagementIndexDBService,HostelManagementService; //declare at top
// add in beforeEach
HostelManagementIndexDBService = {};
HostelManagementIndexDBService = {};
module(function($provide) {
$provide.value('HostelManagementService', HostelManagementService);
$provide.value('HostelManagementIndexDBService', HostelManagementIndexDBService);
});
UPDATE :
describe("ManagemntApp ", function() {
var HostelManagementIndexDBService, HostelManagementService;
beforeEach(module('ManagemntApp'));
var scope;
var cntrlrCandidate,
$location;
beforeEach(function() {
HostelManagementService = {};
HostelManagementIndexDBService = {};
module(function($provide) {
$provide.value('HostelManagementService', HostelManagementService);
$provide.value('HostelManagementIndexDBService', HostelManagementIndexDBService);
});
inject(function($controller, $rootScope) {
scope = $rootScope.$new();
cntrlrCandidate = $controller('cntrlrCandidate', { '$scope': scope });
})
});
it('return will pass the Amount', function() {
scope.Candidate = {};
scope.Candidate.Amount = 2000;
scope.validateAmount();
expect(scope.IsValidAmount).toEqual(true);
});
});

How to restrict to invoke service dependencies while writing Unit test for Controller?

I have the following controller:
(function () {
"use strict";
angular.module('usp.configuration').controller('ConfigurationController', ConfigurationController);
ConfigurationController.$inject = ['$scope', '$rootScope', '$routeParams', 'configurationService'];
function ConfigurationController($scope, $rootScope, $routeParams, configurationService) {
//Get Master Gas List
configurationService.getMasterGasList().then(function (response) {
$scope.masterGasList = response.data.data;
});
$scope.convertToInt = function (str) {
if (!isNumberEmpty(str) && !isNaN(str)) {
return parseInt(str, 10);
}
return "";
}
$scope.convertToString = function (num) {
if (!isNumberEmpty(num) && !isNaN(num)) {
return num + "";
}
return "";
}
}
}());
And below is the test case for the controller:
describe("test suite for Configuration test controller", function() {
var scope = null;
var configurationService;
beforeEach(module("usp.configuration"));
beforeEach(inject(function($rootScope, $controller, _configurationService_) {
// Services
// _'s are automatically unwrapped
configurationService = _configurationService_;
// Controller Setup
scope = $rootScope.$new();
$controller("ConfigurationController", {
$scope: scope,
configurationService : configurationService
});
}));
it("should convert to int", function() {
expect(scope.convertToInt("2")).toEqual(2);
});
it("should return empty string", function() {
expect(scope.convertToInt("asd")).toEqual("");
});
});
I don't want to call that service while I am running the test case.
I am new to unit testing, I don't know how can I do this.
Please help me to do this?
You need to mock the dependencies with $provide
beforeEach(function () {
configurationServiceMock = {
getSomething: function () {
return 'mockReturnValue';
}
};
module(function ($provide) {
$provide.value('configurationService', configurationServiceMock);
});
});
see: Injecting a mock into an AngularJS service
Solution for your needs:
var configurationServiceMock = {
getMasterGasList: function () {
return {
then: function(callback) {}
};
}
};
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
controller = $controller('ConfigurationController', {
'$scope': scope,
'configurationService': configurationServiceMock
});
}));

JS Unit Testing Using karma and Jesmine

I have written a unit test like below
describe('modals', function() {
beforeEach(module('DetailsApp'));
var controller, rootScope, templateCache, compile, http, httpBackend;
var uibModalInstance = {
dismiss: function(message) {
},
close: function(message) {
}
};
var plugins = {
get: function(plugin) {
if(plugin == 'Workorder'){
return {
'workorder_id': 'workorder_id'
};
} else if(plugin == 'CompanyInfo'){
return {
'company_name': 'company_name',
'company_id': 'company_id'
};
}
}
};
beforeEach(module(function($provide) {
$provide.value('$uibModalInstance', uibModalInstance);
$provide.value('plugins', plugins);
}));
beforeEach(inject(function($controller, $templateCache, $compile, $rootScope, $http, $httpBackend) {
controller = $controller;
templateCache = $templateCache;
compile = $compile;
rootScope = $rootScope;
http = $http;
httpBackend = $httpBackend;
}));
describe('When modal functions are called', function() {
it('they should be called correctly', function() {
var $scope = {};
var companyRatingHistory = controller('companyRatingHistory', { $scope: $scope });
spyOn(uibModalInstance, 'dismiss');
spyOn(uibModalInstance, 'close');
$scope.cancel();
expect(uibModalInstance.dismiss).toHaveBeenCalledWith('cancel');
$scope.close('close');
expect(uibModalInstance.close).toHaveBeenCalledWith('close');
});
}); });
and found that my code coverage shows an E in plugins else block like below
else if(plugin == 'CompanyInfo'){
return {
'company_name': 'company_name',
'company_id': 'company_id'
};
}
What i have missed in my test. Advance thanks and get any suggestions from anybody who helps me.

Spyon angular service method returns Unexpected POST

I have a promise in a controller that I'm trying to test and I'm getting Error: Unexpected request: POST /v1/users.
I'm trying to spyOn the AuthService.changePassword which returns a promise and test whether it got called or not. Not sure why it's actually making the POST call...
controller
angular.module('example')
.controller('ChangePasswordCtrl', ['AuthService', '$state',
function(AuthService, $state) {
var vm = this;
vm.submitted = false;
vm.submit = function(valid) {
vm.submitted = true;
if (!valid) return false;
AuthService.changePassword(vm.email)
.then(function(res) {
$state.go('reset.confirmation');
}, function(err) {
vm.hasError = true;
});
};
}
]);
unit test
describe('ChangePasswordCtrl', function() {
var ctrl, scope, AuthService, $q, $state, deferred;
beforeEach(module('example'));
function _inject() {
inject(function($controller, $rootScope, _AuthService_, _$state_, _$q_) {
scope = $rootScope.$new();
$state = _$state_;
$q = _$q_;
AuthService = _AuthService_;
ctrl = $controller('ChangePasswordCtrl', {
$scope: scope
});
});
}
describe('#submit', function() {
beforeEach(function() {
_inject();
deferred = $q.defer();
spyOn(AuthService, 'changePassword').and.returnValue(deferred.promise);
spyOn($state, 'go');
});
describe('when email address is valid', function() {
it('should call the changePassword method on the AuthService', function() {
ctrl.submit(true);
scope.$digest();
expect(ctrl.submitted).toBe(true);
expect(AuthService.changePassword).toHaveBeenCalled();
});
});
});
});
Your spec code works for me (the real implementation of AuthService.changePassword doesn't get called): http://jsfiddle.net/7W2XB/7/
angular.module('example', [])
.factory('AuthService', function() {
return {
changePassword: function() {
throw new Error('Should not be called');
}
};
})
.controller('ChangePasswordCtrl', ['AuthService',
function(AuthService) {
var vm = this;
vm.submitted = false;
vm.submit = function(valid) {
vm.submitted = true;
if (!valid) return false;
AuthService.changePassword(vm.email)
.then(function(res) {
$state.go('reset.confirmation');
}, function(err) {
vm.hasError = true;
});
};
}
]);
describe('ChangePasswordCtrl', function() {
var ctrl, scope, AuthService, $q, deferred;
function _inject() {
module('ui.router');
module('example');
inject(function($controller, $rootScope, _AuthService_, _$state_, _$q_) {
scope = $rootScope.$new();
$state = _$state_;
$q = _$q_;
AuthService = _AuthService_;
ctrl = $controller('ChangePasswordCtrl', {
$scope: scope
});
});
}
describe('#submit', function() {
beforeEach(function() {
_inject();
deferred = $q.defer();
spyOn(AuthService, 'changePassword').and.returnValue(deferred.promise);
});
describe('when email address is valid', function() {
it('should call the changePassword method on the AuthService', function() {
ctrl.submit(true);
scope.$digest();
expect(ctrl.submitted).toBe(true);
expect(AuthService.changePassword).toHaveBeenCalled();
});
});
});
});
Some questions that might help make the JSFiddle more realistic to your situation: What versions of angular and Jasmine are you using? - How are you defining the AuthService (presumably using angular.factory)?

Unit testing controller with injected service

What is the best way to go about unit testing the following controller? I'm having trouble properly injecting AuthService into my controller. I've seen so many different ways to do it and I'm not really sure what the best practice is - i.e. mocks vs spies?
I have a simple service like this:
angular.module('users')
.factory('AuthService', ['$http', '$window',
function($http, $window) {
var authService = {};
authService.login = function(creds) {
return $http.post('/auth', creds)
.then(function(res) {
$window.localStorage.exampleToken = res.data.returned_token;
return res;
});
};
authService.isLoggedIn = function() {
if($window.localStorage.exampleToken) {
return true;
} else {
return false;
}
};
authService.clear = function() {
delete $window.localStorage.exampleToken;
};
return authService;
}]);
My controller:
angular.module('users')
.controller('ExampleCtrl', ['AuthService',
function(AuthService) {
var vm = this;
vm.isLoggedIn = AuthService.isLoggedIn();
}]);
My unfinished test:
describe('ExampleCtrl', function() {
beforeEach(module('users'));
var ctrl;
beforeEach(inject(function($controller) {
ctrl = $controller('ExampleCtrl', {});
}));
describe('when logged in', function() {
beforeEach(function() {
// how do i mock the isLoggedIn function to
// return true
});
it('should return true', function() {
expect(ctrl.isLoggedIn).toBe(true);
});
});
describe('when not logged in', function() {
beforeEach(function() {
// how do i mock the isLoggedIn function to
// return false
});
it('should return false', function() {
expect(ctrl.isLoggedIn).toBe(false);
});
});
});
You can merely use the callFake function of Jasmine:
By chaining the spy with and.callFake, all calls to the spy will delegate to the supplied function.
var AuthService; //so that you can have a reference within all your test file
beforeEach(function() {
inject(function(_AuthService_) {
AuthService = _AuthService_;
});
spyOn(AuthService, 'isLoggedIn').and.callFake(function() {
return true;
});
});

Resources