I have the following function in my controller:
$scope.submitNote = function(){
myService.addNote($scope.note).then(function(data){
if(data.success === true){
$scope.note = null;
}
else{
// API call failed
}
}, function(){
// Promise call failed
});
};
I set up my testing environment with:
// Mock out fake service
beforeEach(function(){
myService = {
addNote: function(){
deferred = q.defer();
deferred.resolve({
success: true
});
return deferred.promise;
}
};
spyOn(myService, 'addNote').and.callThrough();
});
// Assign controller scope
beforeEach(inject(function($controller, $rootScope, $q){
q = $q;
scope = $rootScope.$new();
$controller('myController', {
$scope: scope,
myService: myService
});
}));
Then test out my submitNote() function with:
describe('submitNote Test', function(){
it('should set scope.note to null after successful service call', function(){
scope.submitNote();
expect(myService.addNote).toHaveBeenCalled();
expect(scope.note).toBe(null);
});
});
The first expect passes, but the second expect does not. It looks like the then() callback from my submitNote() function isn't being called in the test.
How do I make sure the promise callback in the original function is called?
To give you cleaner tests that you have more control over the ngMock module extends various core services so they can be inspected and controlled in a synchronous manner.
Promise callbacks are executed during the digest loop, which in your testing environment you need to start manually.
For example:
describe('submitNote Test', function () {
it('should set scope.note to null after successful service call', function () {
scope.submitNote();
scope.$digest();
expect(myService.addNote).toHaveBeenCalled();
expect(scope.note).toBe(null);
});
});
Related
I have to unit test my controller. First I have to create mock for my services.
Here is my service:
angular.module("demo-app")
.factory("empService",function($http){
var empService={};
empService.getAllEmployees=function(){
return $http.get("http://localhost:3000/api/employees");
}
empService.postEmployee=function(emp){
return $http.post("http://localhost:3000/api/employees",emp);
}
empService.getEmployee=function(id){
return $http.get("http://localhost:3000/api/employees/"+id)
}
empService.putEmployee=function(emp){
return $http.put("http://localhost:3000/api/employees/"+emp._id,emp)
}
empService.deleteEmployee=function(id){
return $http.delete("http://localhost:3000/api/employees/"+id);
}
empService.findEmployee=function(emp){
return $http.post("http://localhost:3000/api/employees/search",emp);
}
return empService;
})
Here is findData() method in my controller, which I am going to test:
$scope.findData=function(){
$scope.loadingEmployee=true;
var emp={};
listProp=Object.getOwnPropertyNames($scope.searchEmployee);
for(index in listProp){
if($scope.searchEmployee[listProp[index]]!=""){
emp[listProp[index]]=$scope.searchEmployee[listProp[index]];
}
}
console.log(emp);
empService.findEmployee(emp).then(function(data){
$scope.allEmployees=data.data;
console.log(data.data);
$scope.loadingEmployee=false;
});
}
How can I mock my empService.findEmployee(emp) method, so that I can test the findData() method.
My spec.js test file with mocking my service method. Here it is:
beforeEach(function(){
var emp={"name":"sanjit"};
fakeService={
getAllEmployees:function(emp){
def=q.defer();
def.resolve({data:[{"name":"sanjit"},{'name':'ssss'}]});
return def.promise;
},
findEmployee:function(emp){
var def=q.defer();
def.resolve({data:[{"name":"sanjit"}]});
console.log("working");
return def.promise;
}
};
spyOn(fakeService,'findEmployee').and.callThrough();
fakeService.findEmployee(emp);
});
beforeEach(angular.mock.inject(function($rootScope,$controller,$injector,$q){
httpBackend=$injector.get('$httpBackend');
scope=$rootScope.$new();
q=$q;
ctrl=$controller('adminEmployeeCtrl',{$scope:scope,empService:fakeService});
}));
it('findData test',function(){
scope.$apply();
scope.findData();
expect(scope.loadingEmployee).toEqual(false);
})
But I got another error:
Error: Unexpected request: GET dashboard/views/dashboard-new.html
No more request expected
But I didn't call it. Please help me
You may not have manually called GET dashboard/views/dashboard-new.html but $scope.$apply() might be triggering it somehow and you can't do anything but handle it.
You can do something like this to handle it: (after injecting it using _$httpBackend_ and assigning to $httpBackend in beforeEach)
$httpBackend.when('GET', 'dashboard/views/dashboard-new.html').respond(200);
scope.$digest();
$httpBackend.flush();
One of the most important rules when testing controllers in angularjs is you do not need to create reall http requests, just mock the functions in that service that are used by your controller. So you need to spyOn them and call fake function to return the proper value. Let's spy on one of them
/**
* #description Tests for adminEmployeeCtrl controller
*/
(function () {
"use strict";
describe('Controller: adminEmployeeCtrl ', function () {
/* jshint -W109 */
var $q, $scope, $controller;
var empService;
var errorResponse = 'Not found';
var employeesResponse = [
{id:1,name:'mohammed' },
{id:2,name:'ramadan' }
];
beforeEach(module(
'loadRequiredModules'
));
beforeEach(inject(function (_$q_,
_$controller_,
_$rootScope_,
_empService_) {
$q = _$q_;
$controller = _$controller_;
$scope = _$rootScope_.$new();
empService = _empService_;
}));
function successSpies(){
spyOn(empService, 'findEmployee').and.callFake(function () {
var deferred = $q.defer();
deferred.resolve(employeesResponse);
return deferred.promise;
// shortcut can be one line
// return $q.resolve(employeesResponse);
});
}
function rejectedSpies(){
spyOn(empService, 'findEmployee').and.callFake(function () {
var deferred = $q.defer();
deferred.reject(errorResponse);
return deferred.promise;
// shortcut can be one line
// return $q.reject(errorResponse);
});
}
function initController(){
$controller('adminEmployeeCtrl', {
$scope: $scope,
empService: empService
});
}
describe('Success controller initialization', function(){
beforeEach(function(){
successSpies();
initController();
});
it('should findData by calling findEmployee',function(){
$scope.findData();
// calling $apply to resolve deferred promises we made in the spies
$scope.$apply();
expect($scope.loadingEmployee).toEqual(false);
expect($scope.allEmployees).toEqual(employeesResponse);
});
});
describe('handle controller initialization errors', function(){
beforeEach(function(){
rejectedSpies();
initController();
});
it('should handle error when calling findEmployee', function(){
$scope.findData();
$scope.$apply();
// your error expectations
});
});
});
}());
I want to test the following method in my controller class:
// getIds() {
// this.api.getIds()
// .then((response)=> {
// this.ids = response.data;
// this.doSomethingElse();
// });
// }
I'm not sure how to handle the promise using jasmine and karma. The project is written in ES6. api.getIds() returns a $http.get().
beforeEach(function() {
inject(function($controller, $rootScope, _api_) {
vm = $controller('MainController', {
api: _api_,
$scope:$rootScope.$new()
});
});
});
beforeEach(function () {
vm.getIds();
});
it('should set the ids', function () {
expect(vm.ids).toBeDefined(); //error
});
How do I wait for the promise to complete before running the expect() ?
First of all, you should use the done callback provided by the jasmine; see async support in Jasmine.
Then, you should mock your getIds on the api so that it returns a resolved promise with an expected value. The asserts should be done after the then promise is called - se bellow the full example.
beforeEach(function () {
var $q, vm, api, $controller, $rootScope;
inject(function (_$controller_, _$rootScope_, _$q_) {
$q = _$q_;
$controller = _$controller_;
$rootScope = _$rootScope_;
api = jasmine.createSpyObj('api', ['getIds']);
api.getIds.and.returnValue($q.when([]));
vm = $controller('MainController', {
api: api,
$scope: $rootScope.$new()
});
});
});
it('should set the ids', function (done) {
vm
.getIds()
.then(function (ids) {
expect(ids).toBeDefined();
// add more asserts
done();
});
});
As a side note, if the this.doSomethingElse(); is a promise too, you have to return it in the first then so that you can test the final result.
I have an application which uses angulars $modal to popup a login modal any time a user tried to enter a secure route without a valid authentication token. This works great but is causing an issue with my testing.
The modal was created as a factory
.factory('loginModal', function ($modal) {
return function() {
var instance = $modal.open({
templateUrl: 'partials/login',
controller: 'AuthCtrl',
controllerAs: 'AuthCtrl'
})
return instance.result;
};
});
In my controller I have a login action, upon successfully logging in the modal is closed using $scope.$close.
$scope.login = function() {
auth.login($scope.user)
.then(function(response) {
$scope.$close(response);
$state.go('secure.user');
}, function(response) {
$scope.hasErrMsg = true;
$scope.errMsg = 'Incorrect password.';
$scope.$dismiss;
});
};
Lastly my unit test which is checking to make sure that auth.login is called with the correct properties when my controllers login function is called.
describe('Auth Controller Tests', function () {
var $scope, $controller, $q, $httpBackend, auth, controller, deferred, loginReqHandler, userReqHandler, indexReqHandler, registerPostReqHandler, doesUserExistPostReqHandler, loginPostReqHandler, loginModal;
beforeEach(module('enigmaApp'));
beforeEach(inject(function ($injector) {
$scope = $injector.get('$rootScope');
$controller = $injector.get('$controller');
$q = $injector.get('$q');
$httpBackend = $injector.get('$httpBackend');
auth = $injector.get('auth');
controller = $controller('AuthCtrl', { $scope: $scope });
deferred = $q.defer();
spyOn(auth, 'isLoggedIn');
loginReqHandler = $httpBackend.when('GET', 'partials/login').respond(deferred.promise);
userReqHandler = $httpBackend.when('GET', 'partials/user').respond(deferred.promise);
indexReqHandler = $httpBackend.when('GET', 'partials/index').respond(deferred.promise);
registerPostReqHandler = $httpBackend.when('POST', '/register').respond(deferred.promise);
doesUserExistPostReqHandler = $httpBackend.when('POST', '/doesUserExist').respond(deferred.promise);
loginPostReqHandler = $httpBackend.when('POST', '/login').respond(deferred.promise);
loginModal = $injector.get('loginModal');
}));
afterEach(function () {
$httpBackend.flush();
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
describe('AuthCtrl.login()', function () {
it('should call auth.login() with $scope.user', function () {
$scope.user = {
email: 'bwayne#wayneenterprise.com',
password: 'password123'
};
spyOn(auth, 'login').and.returnValue(deferred.promise);
$scope.login();
deferred.resolve();
$scope.$digest();
expect(auth.login).toHaveBeenCalledWith($scope.user);
});
});
});
Now when I run the test I get the following error:
TypeError: $scope.$close is not a function
I suspect this error is because in code expects $scope to be set to the scope for the modal when it calls $scope.$close and in my test $scope is set to the controllers scope. Although I'm not sure how to reference the $modal's scope.
Update: I just discovered if I add $scope.$close = function () {}; inside the it() block then the test runs properly. Is this the correct approach?
I think you are probably trying to test too much. If you need to be making all those $http calls and such just to test a controller then you are almost certainly doing something wrong.
This is how I would go about testing your controller. See comments for further explanation. I realise this might not fit you use-case exactly but hopefully you will find it helpful to see a different approach.
DEMO
appSpec.js
describe('Auth Controller Tests', function () {
var $scope, $controller, $state, auth, controller,
loginDeferred, $closeSpy, goSpy, loginSpy;
beforeEach(module('enigmaApp'));
beforeEach(inject(function($q, _$controller_, _$rootScope_){
$controller = _$controller_;
$scope = _$rootScope_.$new();
loginDeferred = $q.defer();
// create spies
$closeSpy = jasmine.createSpy('$close');
goSpy = jasmine.createSpy('go');
loginSpy = jasmine
.createSpy('login')
.and
.returnValue(loginDeferred.promise);
// create mock services with spies
$scope.$close = $closeSpy;
auth = {
login : loginSpy
};
$state = {
go: goSpy
}
// initiate controller and inject mocks
controller = $controller('AuthCtrl', {
$scope: $scope,
auth: auth,
$state: $state
});
// manual $digest to update our controller
// with our mocked services and scope
$scope.$digest();
}));
describe('AuthCtrl.login()', function () {
it('should call auth.login() with $scope.user', function () {
// define mock user object on our $scope
$scope.user = {
email: 'bwayne#wayneenterprise.com',
password: 'password123'
};
// call login() which in turn calls our
// loginSpy
$scope.login();
// just assert that our loginSpy was called with
// the mockUser
// we don't care about anything else so no need
// to worry about promises etc.
expect(auth.login).toHaveBeenCalledWith($scope.user);
});
it('should call $state.go on succesful login', function(){
// call login which will
// call our authLogin spy that returns
// the loginDeferred promise
$scope.login();
// manually resolve the loginDeferred promise and
// call $digest to trigger the then() callback
loginDeferred.resolve({});
$scope.$digest();
// assert $state.go is called when
// our then callback it triggered.
expect($state.go).toHaveBeenCalledWith('secure.user');
});
it('should set the errMsg to true if the login fails', function(){
expect($scope.hasErrMsg).toBeUndefined();
$scope.login();
// this time reject our promise
// so we can evaluate the catch callback
loginDeferred.reject({});
$scope.$digest();
expect($scope.hasErrMsg).toBe(true);
});
});
});
app.js
var app = angular.module('enigmaApp', ['ui.router', 'ui.bootstrap']);
app.controller('AuthCtrl', function($scope, auth, $state){
// Warning: OPINIONATED CODE
// I refactored your auth login function
// to use the then and catch methods which I
// think are much cleaner
$scope.login = function() {
auth
.login($scope.user)
.then(function(response) {
$scope.$close(response);
$state.go('secure.user');
})
.catch(function(response) {
$scope.hasErrMsg = true;
$scope.errMsg = 'Incorrect password.';
$scope.$dismiss;
});
};
});
I have a controller that takes a dependency on a service, and as part of it's initialisation calls a function on the service. Here's a contrived example:
describe('tests', function() {
var _scope, service, serviceValue = 'value';
beforeEach(module('app'));
beforeEach(inject(['$rootScope','$controller', function($rootScope, $controller) {
_scope = $rootScope.$new();
service = {
get: function(key) {
return serviceValue;
}
};
$controller('myController', {
'$scope': _scope,
'service': service
});
}]));
describe('initialisation', function() {
describe('key exists', function() {
it('should find the key', function() {
expect(_scope.message).toBe('found the key');
});
});
describe('key does not exist', function() {
beforeEach(function() {
serviceValue = undefined;
});
it('should not find the key', function() {
expect(_scope.message).toBe('did not find the key');
});
});
});
});
angular.module('app').controller('myController', ['$scope','service',
function($scope, service) {
if(service.get('key') === 'value') {
$scope.message = 'found the key';
} else {
$scope.message = 'did not find the key';
}
});
The tests for when the key does not exist fail because the controller initialisation has run in the first beforeEach, before the next beforeEach runs to change the service return value.
I can get around this by recreating the whole controller in the beforeEach of the 'key does not exist' tests, but this seems wrong to me, as it initialises the controller twice for the test. Is there a way to get the controller initialisation to run for every test, but after all other beforeEach functions have run.
Is this the right way to be initialising controllers? Am I missing some feature of jasmine?
Creating the controller for each test is the recommended way, especially when you have initialization logic.
I would however use Jasmine's spyOn to set up what the service returns and tracking calls to it, instead of modifying internal values of a mocked or real service.
Inject the real service and save it in a variable, and define a function that creates the controller:
describe('tests', function() {
var $scope, createController, service;
beforeEach(function() {
module('app');
inject(function($rootScope, $controller, _service_) {
$scope = $rootScope.$new();
service = _service_;
createController = function() {
$controller('myController', {
'$scope': $scope,
'service': service
});
};
});
});
For each test use spyOn to intercept calls to the service and decide what it should return, then create the controller:
describe('initialisation', function() {
it('should find the key', function() {
spyOn(service, 'get').and.returnValue('value');
createController();
expect($scope.message).toBe('found the key');
});
it('should not find the key', function() {
spyOn(service, 'get').and.returnValue(undefined);
createController();
expect($scope.message).toBe('did not find the key');
});
});
Demo: http://plnkr.co/edit/BMniTis1RbOR0h5O4kZi?p=preview
As spyOn sets up tracking you can now for example also make sure the service only gets called once on controller initilization:
spyOn(service, 'get').and.returnValue('value');
expect(service.get.calls.count()).toEqual(0);
createController();
expect(service.get.calls.count()).toEqual(1);
Note: The examples above use Jasmine 2.0. Syntaxes will have to be slightly modified for older versions.
I have a controller that saves a resource. I can't tell how to "access" the part of the code that executes after the promise resolves. What do I need to change about my test or controller in order to get it to work? Here's the code.
Controller:
'use strict';
/**
* #ngdoc function
* #name lunchHubApp.controller:AnnouncementsCtrl
* #description
* # AnnouncementsCtrl
* Controller of the lunchHubApp
*/
angular.module('lunchHubApp')
.controller('AnnouncementsCtrl', ['$scope', 'Announcement', function ($scope, Announcement) {
$scope.announcements = [];
$scope.save = function() {
// This next line is the part I'm finding hard to test.
new Announcement($scope.announcement).create().then(function(announcement) {
$scope.foo = 'bar'
});
};
}]);
Test:
'use strict';
describe('AnnouncementsCtrl', function() {
beforeEach(function() {
module('lunchHubApp', 'ng-token-auth')
});
it('sets scope.announcements to an empty array', inject(function($controller, $rootScope) {
var scope = $rootScope.$new(),
ctrl = $controller('AnnouncementsCtrl', { $scope: scope });
expect(scope.announcements).toEqual([]);
}));
describe('save', function() {
it('works', inject(function($controller, $rootScope, _$httpBackend_) {
var $httpBackend = _$httpBackend_;
var scope = $rootScope.$new(),
ctrl = $controller('AnnouncementsCtrl', { $scope: scope });
expect(scope.announcements.length).toBe(0);
var announcement = {
restaurantName: 'Bangkok Taste',
userId: 1
};
scope.announcement = announcement;
$httpBackend.expect('POST', '/api/announcements').respond(200, announcement);
scope.save();
scope.$digest();
expect(scope.foo).toEqual('bar');
}));
});
});
Update: here's the way I ended up modifying my controller test. The following passes and has been refactored from the original.
'use strict';
describe('AnnouncementsCtrl', function() {
var $httpBackend,
announcement,
scope,
ctrl;
beforeEach(function() {
module('lunchHubApp');
inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
scope = $injector.get('$rootScope').$new();
ctrl = $injector.get('$controller')('AnnouncementsCtrl', { $scope: scope });
announcement = { restaurantName: 'Bangkok Taste' };
scope.announcement = { restaurantName: 'Jason\'s Pizza' };
$httpBackend.expect('GET', '/api/announcements').respond([announcement]);
});
});
it('sets scope.announcements to an empty array', function() {
expect(scope.announcements).toEqual([]);
});
it('grabs a list of announcements', function() {
expect(scope.announcements.length).toBe(0);
$httpBackend.flush();
expect(scope.announcements.length).toBe(1);
});
describe('save', function() {
beforeEach(function() {
$httpBackend.expect('POST', '/api/announcements').respond(200, { restaurantName: 'Foo' });
scope.save();
$httpBackend.flush();
});
it('adds an announcement', function() {
expect(scope.announcements.length).toBe(2);
});
it('clears the restaurant name', function() {
expect(scope.announcement.restaurantName).toEqual('');
});
});
});
I think what you're doing is good. Since the Angular resources are factories using the $http service in a restful way, you should use the expect of the $httpBackend just as you did.
One thing that you miss however is that you need to make sure your promise is resolved. But write async tests can be tricky in some cases. To do so, you have to use the flush() method of $httpBackend to force your test to be synchronous.
After the flush, you can make your expect normally. Also you might have to move your expectPOST before your $rootScope.$new() statement.
You can go with a change like this, I don't think the $digest() is necessary:
$httpBackend.expect('POST', '/api/announcements').respond(200, announcement);
scope.save();
$httpBackend.flush();
expect(scope.foo).toEqual('bar');
The tests you've started writing seem to be testing not just AnnouncementsCtrl, but the Announcements service/factory as well. The signs of this in this case are
You're not mocking the Announcements service/factory / not stubbing any of its methods.
There is no code in the AnnouncementsCtrl regarding making http requests, and yet you're using $httpBackend.expect(... in the tests for them.
The success/failure of the tests that claim to test AnnouncementsCtrl will succeed or fail depending on code in the Announcements service/factory.
This goes against what unit tests are usually used for: testing each component in isolation. Keeping the focus of this answer on testing the success callback passed to the then method of the promise returned by create, my suggestion is to mock the Announcements service/factory, so its create method returns a promise that you can control in the test. This mock would be of the form:
var MockAnnouncement = null;
var deferred = null;
beforeEach(module(function($provide) {
MockAnnouncement = function MockAnnouncement() {
this.create = function() {
return deferred.promise;
};
};
$provide.value('Announcement', MockAnnouncement);
}));
You would then have to make sure that you create deferred object before each test:
beforeEach(inject(function($rootScope, $controller, $q) {
$scope = $rootScope.$new();
deferred = $q.defer(); // Used in MockAnnouncement
ctrl = $controller('AnnouncementsCtrl', {
$scope: $scope
});
}));
This deferred object is then resolved in the test:
it('calls create and on success sets $scope.foo="bar"', function() {
$scope.save();
deferred.resolve();
$scope.$apply();
expect($scope.foo).toBe('bar');
});
A slightly extended version of this, testing a few other behaviours of the controller as well, can be seen at http://plnkr.co/edit/v1bCfmSPmmjBoq3pfDsk