Unit test controller with service in it - angularjs

I'm trying to unit test a controller with a service injected into it. No matter what I seem to try, I get an error. Any assistance to help me get this going would be much appreciated. I'm using Angular/Karma/Jasmine to get run my tests.
There seem to be a lot of posts with similar stories but this feels like it may not be a duplicate - apologies if it is.
My controller looks like this:
(function() {
angular
.module('blah')
.controller('AdminController', AdminController);
/* #ngInject */
function AdminController($scope, toastr, adminService) {
activate();
/**
* Controller initialisation.
*/
function activate() {
getAllUsers();
}
/**
* Gets all users.
*/
function getAllUsers() {
adminService.getAllUsers()
.then(function(response) {
$scope.users = response.data;
})
.catch(function(error) {
toastr.error('Unable to load users', 'Error');
console.log(error);
});
}
}
})();
And my service looks like this:
(function() {
angular
.module('blah')
.factory('adminService', adminService);
/* #ngInject */
function adminService($http, environmentConfig) {
var service = {
getAllUsers: getAllUsers
};
return service;
/**
* Gets all user objects.
*/
function getAllUsers() {
return $http.get(environmentConfig.apiBaseUrl + '/user');
}
}
})();
and my unit tests look like this:
describe('AdminController', function() {
var ctrl,
adminService,
$scope;
var listOfTestUsers = [
{ name: 'Dave', id: 1 },
{ name: 'Bob', id: 2 },
{ name: 'Bill', id:3 }
];
beforeEach(function() {
module('blah');
});
beforeEach(inject(function($rootScope, $controller) {
adminService = {
getAllUsers: function() {}
};
spyOn(adminService, 'getAllUsers').and.returnValue(listOfTestUsers);
$scope = $rootScope.$new();
ctrl = $controller('AdminController', {
$scope: $scope,
adminService: adminService
});
}));
describe('The getAllUsers function should exist', function() {
it('should work', function() {
expect(ctrl).toBeDefined();
});
});
});
I get this error when running my Jasmine tests with Karma:
TypeError: adminService.getAllUsers(...).then is not a function

Here are a few things that I found wrong with the code.
.catch was used earlier. .then is called with two callbacks, a success callback and an error callback. So that's what I've done in your call to adminService.getAllUsers.
For the TypeError: adminService.getAllUsers(...).then is not a function that you were getting. You didn't mock getAllUsers properly. I've done that in the testCases file. It returns a function named then which was not available earlier.
Controller
(function() {
angular
.module('blah', [])
.controller('AdminController', AdminController);
/* #ngInject */
function AdminController($scope, toastr, adminService) {
$scope.greeting = "Hello World!";
/**
* Gets all users.
*/
$scope.getAllUsers = function() {
adminService.getAllUsers()
.then(function(response) {
$scope.users = response.data;
}, function(error) {
toastr.error('Unable to load users', 'Error');
console.log(error);
});
}
activate();
/**
* Controller initialisation.
*/
function activate() {
$scope.getAllUsers();
}
}
})();
environmentConfig Constant. Replace this with yours.
(function() {
angular.module('blah').constant('environmentConfig', {
apiBaseUrl: 'https://www.something.com'
})
})();
toastr Service. Replace this with yours
(function() {
angular
.module('blah')
.factory('toastr', toastr);
/* #ngInject */
function toastr() {
var service = {
error: error
};
return service;
/**
* Gets all user objects.
*/
function error(a, b) {
console.log("Here's the error : ", a);
}
}
})();
adminService
(function() {
angular
.module('blah')
.factory('adminService', adminService);
/* #ngInject */
function adminService($http, environmentConfig) {
/**
* Gets all user objects.
*/
function getAllUsers() {
return $http.get(environmentConfig.apiBaseUrl + '/user');
}
var service = {
getAllUsers: getAllUsers
};
return service;
}
})();
Test Cases
describe('controller: AdminController', function() {
var scope, $scope, toastr, adminService, AdminController, flag, $q;
flag = 'success';
var listOfTestUsers = [{
name: 'Dave',
id: 1
}, {
name: 'Bob',
id: 2
}, {
name: 'Bill',
id: 3
}];
beforeEach(module('blah'));
beforeEach(inject(function($controller, $rootScope, _toastr_, _adminService_, _$q_) {
scope = $rootScope.$new();
toastr = _toastr_;
adminService = _adminService_;
$q = _$q_;
spyOn(adminService, 'getAllUsers').and.callFake(function() {
return flag === 'success' ? $q.when(listOfTestUsers) : $q.reject("Error");
});
AdminController = $controller('AdminController', {
$scope: scope,
toastr: _toastr_,
adminService: _adminService_
});
}));
describe('The getAllUsers function should exist', function() {
it('should work', function() {
expect(AdminController).toBeDefined();
});
});
});
Hope this helps.

Your controller code is causing the error. You should be callling $scope.getAllUsers(); in your activate function, not "getAllUsers()".

Your actual error is the fact that your mock service does not have the function 'getAllUsers' here is a paste bin with the adjustments http://pastebin.com/LwG0CzUW
If you prefer you could adjust your test to call the actual service as the following pastebin.
http://pastebin.com/RSP4RfF9

Related

TypeError: service.getForums() is not a function - jasmine unit testing for AngularJS

Hello I am new to AngularJS. I am trying to test angular service ForumService. For that I have to mock the response that I get while calling the service method. I did something but I don't know if its right or not. I am getting an error
"ForumService should use Function FAILED"
"TypeError: service.getForums is not a function"
This is the service that I am testing
(function() {
'use strict';
function ForumService($q, $http, config, Forum) {
var service = {};
/**
* Sends a GET request to backend API for all forums.
*
* #return {Promise}
*/
service.getForums = function(onSuccessCallback, onErrorCallback) {
$http.get(config.apiBaseUrl + '/api/forums')
.then(
function handleSuccess(response) {
onSuccessCallback(response.data.data);
},
function handleError(response) {
onErrorCallback(response.data.error);
}
);
};
/**
* Sends a GET request to backend API for all forums.
*
* #return {Promise}
*/
service.getForumsPromise = function() {
var q = $q.defer();
$http.get(config.apiBaseUrl + '/api/forums')
.then(
function success(response) {
q.resolve(buildForumArray(response.data.data));
},
function error(response) {
q.reject(response.data.error);
}
);
return q.promise;
};
function buildForumArray(data) {
var forumArray = [];
data.forEach(function(forumData) {
forumArray.push(new Forum(forumData));
});
return forumArray;
}
return service;
}
ForumService.$inject = [
'$q',
'$http',
'config',
'Forum'
];
angular
.module('app.services')
.factory('ForumService', ForumService);
})();
The following is the code where I am testing the first method service.getForums()
'use strict';
describe('ForumService', function() {
var service, $q, config, httpBackend;
beforeEach(module('app.services'));
beforeEach(module('app.models'));
beforeEach(module(function($provide) {
$provide.service('config', function() {
this.apiBaseUrl = "localhost";
});
$provide.service('ForumService', function() {
this.constructor = jasmine.createSpy('ForumService')
});
$provide.service('Forum', function() {
this.constructor = jasmine.createSpy('Forum')
});
}));
//2.
beforeEach(function() {
inject(function($injector) {
service = $injector.get('ForumService');
httpBackend = $injector.get('$httpBackend');
$q = $injector.get('$q');
});
});
// 5. make sure no expectations were missed in your tests.
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequest();
});
it('should use Function', function() {
var returnData = [
{
id: 1,
name: "Programming Questions",
description: "Please post all Questions you have in regards to programming here"
}, {
id: 2,
name: "OOP",
description: "Object Oriented Programming"
}
];
console.info('foo');
httpBackend.when('GET', 'localhost/api/forums').respond(200, returnData);
service.getForums().then(function(response) {
console.info(response); // to see the response
expect(response.data.id).toBe(1);
expect(response.data.name).toBe("Programming Questions");
expect(response.data.description).toBe("Please post all Questions you have in regards to programming here");
});
httpBackend.flush();
});
});
And this is my model class
(function() {
'use strict';
angular
.module('app.models')
.factory('Forum', Forum);
Forum.$inject = [];
function Forum() {
/**
* Forum prototype (constructor function).
*
* #param data
* #constructor
*/
function Forum(data) {
var self = this;
if (angular.isDefined(data)) {
self.id = data.id;
self.name = data.name;
self.description = data.description;
} else {
self.id = 0;
self.name = '';
self.description = '';
}
}
return Forum;
}
})();
There were are lot of issues with the test cases and the code you've written. Fixed a few of them here
Do read the inline comments for the explainations
This should be the definition of you getForums method:
service.getForums = function(onSuccessCallback, onErrorCallback) {
$http.get(config.apiBaseUrl + '/api/forums').then(function handleSuccess(response) {
// You'll get the data inside response.data and not response.data.data
// onSuccessCallback(response.data.data);
onSuccessCallback(response.data);
}, function handleError(response) {
onErrorCallback(response.data.error);
});
};
If you really wanted to return promise from getForumsPromise method, you could have simply done this:
service.getForumsPromise = function() {
return $http.get(config.apiBaseUrl + '/api/forums');
};
$http.get returns a promise anyways.
And this is how you should be writing the test case:
'use strict';
describe('ForumService', function() {
var returnData = [{
id: 1,
name: "Programming Questions",
description: "Please post all Questions you have in regards to programming here"
}, {
id: 2,
name: "OOP",
description: "Object Oriented Programming"
}];
//Below line of code is not required.
// var service, $q, config, httpBackend;
beforeEach(module('app.services'));
beforeEach(module('app.models'));
beforeEach(module(function($provide) {
$provide.service('config', function() {
this.apiBaseUrl = "localhost";
});
// Below line of code is not required.
// $provide.service('ForumService', function() {
// this.constructor = jasmine.createSpy('ForumService')
// });
// $provide.service('Forum', function() {
// this.constructor = jasmine.createSpy('Forum')
// });
}));
//2.
// Instead of injecting the services like this
// beforeEach(function() {
// inject(function($injector) {
// service = $injector.get('ForumService');
// httpBackend = $injector.get('$httpBackend');
// $q = $injector.get('$q');
// });
// });
// Inject them like this
beforeEach(inject(function(_ForumService_, _$httpBackend_) {
ForumService = _ForumService_;
$httpBackend = _$httpBackend_;
$httpBackend.when('GET', 'localhost/api/forums').respond(200, returnData);
}))
// 5. make sure no expectations were missed in your tests.
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should use Function', function() {
// This data should be outside a specific it block so that it could be reused.
// Moved it outside.
// var returnData = [{
// id: 1,
// name: "Programming Questions",
// description: "Please post all Questions you have in regards to programming here"
// }, {
// id: 2,
// name: "OOP",
// description: "Object Oriented Programming"
// }];
console.info('foo');
// This should be inside beforeEach block so that it could be reused.
// httpBackend.when('GET', 'localhost/api/forums').respond(200, returnData);
// You call httpBackend's flush right after the call to your service's method and then expect.
// Also your expectations are wrong. So you might get errors.
// Fixing those.
// service.getForums().then(function(response) {
// console.info(response); // to see the response
// expect(response.data.id).toBe(1);
// expect(response.data.name).toBe("Programming Questions");
// expect(response.data.description).toBe("Please post all Questions you have in regards to programming here");
// });
// httpBackend.flush();
// Like this.
var successCallback = function(data) {
expect(data.length).toEqual(2);
expect(data[0].id).toBe(1);
expect(data[0].name).toBe("Programming Questions");
expect(data[0].description).toBe("Please post all Questions you have in regards to programming here");
}
var errorCallback = function(error) {
}
ForumService.getForums(successCallback, errorCallback);
$httpBackend.flush();
});
});
Hope this helps

Mocking $mdSideNav in unit test

I have a simple enough function that closes an $mdSidenav instance in my application
function closeSideNav() {
$mdSidenav('left').close();
}
I'm now needing to unit test this, but am having trouble writing an expectation for the close() call on $mdSidenav.
I thought about using $provide in my test spec
module(function($provide) {
$provide.value('$mdSidenav', function(id) {
return {
close: jasmine.createSpy('$mdSidenav.close')
}
})
});
beforeEach(inject(function(_$controller_, _$mdSidenav_) {
$controller = _$controller_;
$mdSidenav = _$mdSidenav_;
}));
beforeEach(function() {
vm = $controller('NavbarController', {
$mdSidenav: $mdSidenav
});
});
describe('vm.closeSideNav', function() {
beforeEach(function() {
spyOn($mdSidenav, 'close');
vm.closeSideNav()
});
it('should call $mdSidenav.close()', function() {
expect($mdSidenav.close).toHaveBeenCalled();
});
});
This throws a couple of errors:
Error: close() method does not exist
Error: Expected a spy, but got undefined.
Has anyone managed to mock out $mdSidenav and offer me some guidance please?
Thanks
UPDATE
Based on the suggested answer, I have now updated my test spec to
'use strict';
describe('NavbarController', function() {
var $controller,
vm,
$mdSidenav,
sideNavCloseMock;
beforeEach(function() {
module('app.layout');
sideNavCloseMock = jasmine.createSpy();
module(function($provide) {
$provide.value('$mdSidenav', function() {
return function(sideNavId) {
return {close: sideNavCloseMock}
}
})
});
});
beforeEach(inject(function(_$controller_, _$mdSidenav_) {
$controller = _$controller_;
$mdSidenav = _$mdSidenav_;
}));
beforeEach(function() {
vm = $controller('NavbarController', {
$mdSidenav: $mdSidenav
});
});
describe('vm.closeSideNav', function() {
beforeEach(function() {
vm.closeSideNav()
});
it('should call $mdSidenav.close()', function() {
expect(sideNavCloseMock).toHaveBeenCalled();
});
});
});
And for a sanity check, my actual controller looks as follows:
(function () {
'use strict';
angular
.module('app.layout')
.controller('NavbarController', Controller);
Controller.$inject = ['$mdSidenav'];
function Controller($mdSidenav) {
var vm = this;
vm.closeSideNav = closeSideNav;
//This only affects the sideNav when its not locked into position, so only on small\medium screens
function closeSideNav() {
$mdSidenav('left').close();
}
}
})();
Unfortunately this still isn't working for me, and I end up with a different error
TypeError: undefined is not a constructor (evaluating '$mdSidenav('left').close())
close method doesn't belong to $mdSidenav. $mdSidenav is a function that returns a side nav object. That's why it complains 'close() method does not exist'.
What you can do is mock the $mdSidenav to return an object hat has mocked close method, like this: -
var sideNavCloseMock;
beforeEach(module(function($provide){
sideNavCloseMock = jasmine.createSpy();
$provide.factory('$mdSidenav', function() {
return function(sideNavId){
return {close: sideNavCloseMock};
};
});
}));
then do
it('should call $mdSidenav.close()', function() {
expect(sideNavCloseMock).toHaveBeenCalled();
});

'No more request expected' error when using $httpBackend

I'm trying to test a simple call to my API, and I'm going round in circles trying to work out why it's failing.
I've simplified things a bit.
This would be the error for the test below:
Error: Unexpected request: GET /api/search?blah=something
No more request expected
Here is the test:
it('does what it should', function() {
httpBackend.expectGET('/api/search?blah=something').respond(aTestResponse);
scope.search();
httpBackend.flush();
// expectations here...
});
The search function in the controller:
function search() {
myDataService.getSearchResults().query(mySearchParams, function(response) {
// do stuff
}
}
and the data service function:
function getSearchResults() {
return $resource('/api/search', {
param1: '#param1',
param2: '#param2',
...etc
});
}
Any suggestions would be really appreciated.
Edit - here is an edited, but more complete version of my spec file:
'use strict';
describe('Controller: BlahCtrl', function() {
beforeEach(module('blahApp'));
beforeEach(module(function($urlRouterProvider) {
$urlRouterProvider.deferIntercept();
}));
var BlahCtrl;
var scope;
var rootScope;
var httpBackend;
beforeEach(inject(function($controller, $rootScope, $httpBackend) {
httpBackend = $httpBackend;
scope = $rootScope.$new();
rootScope = $rootScope;
BlahCtrl = $controller('BlahCtrl as vm', {
$scope: scope
});
this.testResults = [
{
testProperty1: 'test-value-1-1',
testProperty2: 'test-value-1-2',
testProperty3: 'test-value-1-3'
},
{
testProperty1: 'test-value-2-1',
testProperty2: 'test-value-2-2',
testProperty3: 'test-value-2-3'
}
];
}));
beforeEach(function() {
this.addMatchers({
toEqualData: function(expected) {
return angular.equals(this.actual, expected);
}
});
});
it('stores the search results', function() {
httpBackend.expectGET('/api/search?blah=something').respond(this.testResults);
scope.vm.doSearch();
httpBackend.flush();
// expectations here...
});
});

Unit-Testing a service in Controller with Jasmine in AngularJS

In my Controller I've defined the following service:
CrudService.getAllGroups().$promise.then(
function (response) { $scope.groups = response; },
function (error) { //error code.. }
);
Well, I want to test this service whether it gets a response or not. In test script at first I've defined a function to check whether the service is defined at all.
Test code:
describe('Ctrl: TestCtrl', function () {
beforeEach(module('testApp'));
var scope,
CrudService,
ctrl,
backend;
beforeEach(inject(function ($controller, $rootScope, _CrudService_, $httpBackend) {
scope = $rootScope.$new();
ctrl = $controller('TestCtrl', {
$scope: scope
});
CrudService = _CrudService_;
backend = $httpBackend;
}));
it('should defined the service getGroups', function () {
expect(CrudService.getGroups).toBeDefined();
});
//this is wrong!
it('should returns a successful response', function () {
backend.expectGET('http://localhost:63831/api/group').respond(200, 'success');
backend.flush();
});
});
I don't know how to get a response in the test. I'm new in unit testing and need some help.
For a better comprehension here is the service code:
//CrudService file:
...
return {
getAllGroups: function () {
return ResService.group.query();
}
}
...
//ResService file:
return {
group: $resource(baseUrl + '/api/group/:Id', {
Id: '#Id'
}, {})
}
Do anyone has an idea?
It's incorrect in the sense that it's not a unit test. If you are testing controller here, then you should mock CrudService and test that $scope.groups has been assigned correctly.
beforeEach(function () {
module(function ($provide) {
$provide.factory('CrudService', function () {
return {
getAllGroups: function () {
return {
$promise: null // return an actual promise here
}
}
}
});
});
});
it('should set groups', function () {
expect($scope.groups).toEqual('success')
});
And you need a separate spec to test if CrudService calling backend correctly.

Angular Unit Test Jasmine Spy error

The following controller is getting a TypeError: 'undefined' is not a function (evaluating sessionService.getCurrentPlace()). I have a Mock Service with that method being spied on. The other method on the mock service works fine. I've tried .AndReturns({..}) on the spy as well as .AndCallThrough() but no luck. Any idea what I'm missing, or am I going about this wrong? Much Thanks!
CONTROLLER:
'use strict';
angular.module('wallyApp')
.controller('addGatewayCtrl', function ($scope, $location, $filter, sessionService) {
/*
private members
*/
//set scope from session data
$scope.processSession = function (places) {
$scope.currentPlaceId = sessionService.getCurrentPlace();
if (!$scope.currentPlaceId) {
$scope.currentPlaceId = places[0].id;
}
$scope.place = $filter("getById")(places, $scope.currentPlaceId);
$scope.ready = true;
};
/*
setup our scope
*/
$scope.currentPlaceId = null;
$scope.place = {};
$scope.videoSrc = "/videos/gateway-poster.gif";
$scope.loaded = true;
/*
setup controller behaivors
*/
//set video or gif to show or hide video
$scope.setVideo = function () {
$scope.videoSrc = "/videos/gateway.gif";
};
$scope.setPoster = function () {
$scope.videoSrc = "/videos/gateway-poster.gif";
};
//initialize scope
$scope.setVideo();
//submit form
$scope.continue = function () {
$location.path("/setup/pair-gateway");
return false;
};
//cancel
$scope.back = function () {
$location.path("/setup/plan-locations");
return false;
};
//wifi
$scope.gotoWifi = function () {
$location.path("/setup/wifi");
return false;
};
/*
setup our services, etc
*/
//get our places from the cache
sessionService.get("places").then(function (places) {
if (!places || places.length < 1) {
sessionService.refreshPlaces(); //Note we don't care about the promise as our broadcast watch will pick up when ready
} else {
$scope.processSession(places);
}
}).catch(function (error) {
//TODO:SSW Call Alert Service??
});
//Watch broadcast for changes
$scope.$on("draco.placesRefreshed", function (event, data) {
sessionService.get("places").then(function (places) {
$scope.processSession(places);
});
});
});
UNIT TEST:
'use strict';
describe('addGatewayCtrl', function () {
var $q,
$rootScope,
$location,
$scope,
$filter,
mockSessionService,
completePath = "/setup/pair-gateway",
backPath = "/setup/plan-locations",
wifiPath = "/setup/wifi",
sessionDeferred,
sessionInitDeferred,
mockPlaces = [{ id: "0001" }];
beforeEach(module('wallyApp'));
beforeEach(inject(function (_$q_, _$rootScope_, _$location_, _$filter_) {
$q = _$q_;
$location = _$location_;
$rootScope = _$rootScope_;
$filter = _$filter_;
}));
beforeEach(inject(function ($controller) {
$scope = $rootScope.$new();
mockSessionService = {
get: function (contact) {
sessionDeferred = $q.defer();
return sessionDeferred.promise;
},
getCurrentPlace: function () {
return mockPlaces[0].id;
},
refreshPlaces: function () {
sessionInitDeferred = $q.defer();
return sessionInitDeferred.promise;
}
};
spyOn(mockSessionService, 'get').andCallThrough();
spyOn(mockSessionService, 'getCurrentPlace').andReturn(mockPlaces[0].id);
spyOn(mockSessionService, 'refreshPlaces').andCallThrough();
$controller('addGatewayCtrl', {
'$scope': $scope,
'$location': $location,
'$filter':$filter,
'sessionService': mockSessionService
});
}));
describe('call session service to get place data ', function () {
//resolve our mock place and session services
beforeEach(function () {
//resolve mocks
sessionDeferred.resolve(mockPlaces);
$rootScope.$apply();
});
//run tests
it('should have called sessionService get places', function () {
expect(mockSessionService.get).toHaveBeenCalledWith("places");
});
it('should have called sessionService get currentPlaceId', function () {
expect(mockSessionService.getCurrentPlace).toHaveBeenCalled();
});
it('should have set scope', function () {
expect($scope.place).toEqual(mockPlaces[0]);
});
});
});
So I figured it out. With nested deferred's you have to call $scope.$apply() in between. The following fixed it up (along with a few minor changes to the mock data responses, but those were trivial):
//resolve promises
activityMessagesDeferred.resolve(mockActivityMessages);
$rootScope.$apply();
$rootScope.$broadcast("draco.sessionRefreshed");
activityCountDeferred.resolve(mockActivityCount);
$rootScope.$apply();
placesDeferred.resolve(mockPlaces);
activityListDeferred.resolve(mockActivities);
$rootScope.$apply();

Resources