I've been slowly updating an angular app ready for a 2.0 migration when it's released and I'm running into issues updating my specs. The main problem is I've started to use the controller as syntax and remove scope from my controllers completely but now I'm having problems spying on services that are called in the context of 'this' inside the controller.
I'm copying below the controller code and the spec. I've dummed it down so we can try and solve this problem easily.
Controller:
var LoginCtrl;
LoginCtrl = (function() {
function LoginCtrl(Auth, $state, Session) {
this.Auth = Auth;
this.$state = $state;
this.Session = Session;
this.credentials = {
username: undefined,
password: undefined,
email: undefined
};
}
LoginCtrl.prototype.login = function () {
var _self = this;
this.Auth.login(this.credentials).then(function(response) {
console.log(response)
}, function(error) {
console.log(error)
});
};
return LoginCtrl;
})();
angular.module('projectx').controller('LoginCtrl', ['Auth', '$state', 'Session', LoginCtrl]);
Spec:
describe('Controller: LoginCtrl', function () {
var $controller, Auth;
beforeEach(module('projectx'));
beforeEach(inject(function(_$controller_, _Auth_) {
$controller = _$controller_;
Auth = _Auth_;
}));
it('should pass user credentials to server to log you in', function() {
var scope = {};
var login = $controller('LoginCtrl as login', {$scope: scope});
expect(scope.login).toBe(login);
spyOn(scope.login.Auth, 'login');
scope.login.credentials = {
username: 'test',
password: 'test'
};
scope.login.login();
expect(scope.login.Auth.login).toHaveBeenCalledWith({username: 'test', password: 'test'});
});
});
The error I get is 'undefined' when the controller trys to call this.Auth.login because my spec is spying on (scope.login.Auth, 'login') which is a separate instance I think? Heres the actual error:
PhantomJS 1.9.8 (Mac OS X) Controller: LoginCtrl should pass user credentials to server to log you in FAILED
TypeError: 'undefined' is not an object (evaluating 'this.Auth.login(this.credentials).then')
at /app/controllers/login.js:25
at /spec/controllers/login.js:49
Related
I followed this post (http://gonehybrid.com/how-to-write-automated-tests-for-your-ionic-app-part-2/) to create a simple unit test using Karma & Jasmine for a Ionic controller, but i keep getting undefined errors while the stated objects have been defined. I'm i missing something obvious? By the way, i'm able to run referenced tests from the blog above successfully which makes me think i'm missing something in mine.
Errora are as follows:
TypeError: undefined is not an object (evaluating 'authMock.login') in /Users/projects/app/tests/unit-tests/login.controller.tests.js (line 65)
TypeError: undefined is not an object (evaluating 'deferredLogin.resolve') in /Users/projects/app/tests/unit-tests/login.controller.tests.js (line 71)
TypeError: undefined is not an object (evaluating 'deferredLogin.reject') in /Users/projects/app/tests/unit-tests/login.controller.tests.js (line 79)
Here's the controller:
angular.module('app').controller('LoginCtrl', function($scope, $state, $ionicPopup, $auth) {
$scope.loginData = {};
$scope.user = {
email: '',
password: ''
};
$scope.doLogin = function(data) {
$auth.login(data).then(function(authenticated) {
$state.go('app.tabs.customer', {}, {reload: true});
}, function(err) {
var alertPopup = $ionicPopup.alert({
title: 'Login failed!',
template: 'Please check your credentials!'
});
});
};
});
Here's the test:
describe('LoginCtrl', function() {
var controller,
deferredLogin,
$scope,
authMock,
stateMock,
ionicPopupMock;
// load the module for our app
beforeEach(angular.mock.module('app'));
// disable template caching
beforeEach(angular.mock.module(function($provide, $urlRouterProvider) {
$provide.value('$ionicTemplateCache', function(){} );
$urlRouterProvider.deferIntercept();
}));
// instantiate the controller and mocks for every test
beforeEach(angular.mock.inject(function($controller, $q, $rootScope) {
deferredLogin = $q.defer();
$scope = $rootScope.$new();
// mock dinnerService
authMock = {
login: jasmine.createSpy('login spy')
.and.returnValue(deferredLogin.promise)
};
// mock $state
stateMock = jasmine.createSpyObj('$state spy', ['go']);
// mock $ionicPopup
ionicPopupMock = jasmine.createSpyObj('$ionicPopup spy', ['alert']);
// instantiate LoginController
controller = $controller('LoginCtrl', {
'$scope': $scope,
'$state': stateMock,
'$ionicPopup': ionicPopupMock,
'$auth': authMock
});
}));
describe('#doLogin', function() {
// call doLogin on the controller for every test
beforeEach(inject(function(_$rootScope_) {
$rootScope = _$rootScope_;
var user = {
email: 'test#yahoo.com',
password: 'test'
};
$scope.doLogin(user);
}));
it('should call login on $auth Service', function() {
expect(authMock.login).toHaveBeenCalledWith(user);
});
describe('when the login is executed,', function() {
it('if successful, should change state to app.tabs.customer', function() {
deferredLogin.resolve();
$rootScope.$digest();
expect(stateMock.go).toHaveBeenCalledWith('app.tabs.customer');
});
it('if unsuccessful, should show a popup', function() {
deferredLogin.reject();
$rootScope.$digest();
expect(ionicPopupMock.alert).toHaveBeenCalled();
});
});
})
});
Here's my Karma config:
files: [
'../www/lib/ionic/js/ionic.bundle.js',
'../www/lib/angular-mocks/angular-mocks.js',
'../www/js/*.js',
'../www/js/**/*.js',
'unit-tests/**/*.js'
],
I think that your controller for tests is undefined. Try to replace first it function with this and check if is it defined.
it('controller to be defained', function() {
expect($controller).toBeDefined();
});
If it isn't, try to call controller with:
$controller = _$controller_;
let me present first the scenario.
this is my angular code.
//this is my controller
function viewUserProfile($scope, $http, svcUserInfo) {
svcUserInfo.userInfo(null, function(data) {
$scope.profile = loadProfile(data);
}, function() {
Console.log('Error getting User info.');
});
}
so basically i am just loading a profile object on $scope.profile (e.g first name, last name,etc) using svcUserInfo REST service.
the service is created like this:
anythingHere.factory('svcUserInfo', ['$resource', function($resource) {
return $resource(SVC_USER, null, {
userInfo: {method: 'GET'}
});
}]);
now this one works perfectly so just assume other declarations or initialization before and after those codes.
now i am writing test codes using jasmine-maven-plugin here
what i want to achieve is to mock the service with and populate $scope.profile. and this is what i have so far:
describe('User Profile Controller', function() {
var rootScope, scope,
location = {}, route = {}, http = {}, userProfCtrl,
mockSvcUserInfo, userInfoPromise;
beforeEach(module('myApp'));
beforeEach(module(MOD_USERPROF));
beforeEach(module(function($provide){
$provide.factory('svcUserInfo', ['$q', function($q) {
var profile = {
nameType: 'Personal Name',
firstName: 'Joseph',
lastName: 'Bada',
email: 'email#email.com',
hasReferrer: false,
referrer: null,
country: 'myCountry',
state: 'myState',
city: 'myCIty',
address1: 'this is address1',
address2: 'this is address2',
zipcode: '9999',
phone: '+191234567',
maxProduct: '2'
};
function userInfo(data){
if(userInfoPromise){
return $q.when(profile);
} else {
return $q.reject();
}
}
return{
userInfo: userInfo
};
}]);
}));
beforeEach(inject(function($rootScope, $controller, svcUserInfo){
scope = $rootScope.$new();
rootScope = $rootScope.$new();
mockSvcUserInfo = svcUserInfo;
spyOn(mockSvcUserInfo, 'userInfo').and.callThrough();
userProfCtrl = $controller(CTRL_USERPROF, {
$scope: scope,
svcUserInfo: mockSvcUserInfo,
$rootScope: rootScope
});
}));
it('should call the rest service for User Profile and populate profile', function() {
userInfoPromise = true;
scope.$digest();
rootScope.$digest();
expect(mockSvcUserInfo.userInfo).toHaveBeenCalled();
expect(scope.profile).not.toBe(undefined); // or undefined anything like these
it("profile object: " + JSON.stringify(scope.profile)); // this is for debugging purposes so that i can check if scope.profile has values or not.
});
});
now on the spec i expect that after mockSvcUserInfo.user have been called.
then scope.profile must be populated/initialized since base on my code on the controller is that on successful call of the service, $scope.profile must be populated.
the result of this so far is that scope.profile goes undefined
i based my test codes from here:
http://www.sitepoint.com/mocking-dependencies-angularjs-tests/#mocking-methods-returning-promises
please help me, thanks
EDIT
I have updated the code coming from the first answer
it('should call the rest service for User Profile', inject(function($rootScope,$controller,svcUserInfo) {
scope = $rootScope.$new();
rootScope = $rootScope.$new();
userInfoPromise = true;
mockSvcUserInfo = svcUserInfo;
spyOn(mockSvcUserInfo, 'userInfo').and.callThrough();
userProfCtrl = $controller(CTRL_USERPROF, {
$scope: scope,
svcUserInfo: mockSvcUserInfo,
$rootScope: rootScope
});
scope.$digest();
rootScope.$digest();
expect(mockSvcUserInfo.userInfo).toHaveBeenCalled();
expect(scope.profile).not.toBe([]);
it("hello" + JSON.stringify(scope.profile));
}));
still scope.profile is undefined
I think i know the problem.
You set userInfoPromise to true in your test, but your controller is created in the setup process, and by the time you set the boolean value the service call is done.
Either set userInfoPromise inside beforeeach or move the controller creation itno the test.
I just switched my application from using the Jade template engine to use client side HTML in order to improve performance and decrease server requests. Everything is working fine in the application however I'm having an issue updating my unit tests.
I have the following test:
describe('Registration Controller Tests', function() {
var $controller, $scope, defer, registerSpy, doesUserExistSpy, auth, RegistrationCtrl;
beforeEach(module('enigmaApp'));
beforeEach(inject(function (_$controller_, _$rootScope_, $q) {
$controller = _$controller_;
$scope = _$rootScope_;
defer = $q.defer();
// Create spies
registerSpy = jasmine.createSpy('register').and.returnValue(defer.promise);
doesUserExistSpy = jasmine.createSpy('doesUserExist').and.returnValue(defer.promise);
auth = {
register: registerSpy,
doesUserExist: doesUserExistSpy
};
// Init register controller with mocked services and scope
RegistrationCtrl = $controller('RegistrationCtrl', {
$scope: $scope,
auth: auth
});
// digest to update controller with services and scope
$scope.$digest();
}));
describe('RegistrationCtrl.register()', function () {
beforeEach(function () {
$scope.user = {
email: 'bwayne#wayneenterprise.com',
first_name: 'Bruce',
last_name: 'Wyane',
password: 'password123'
}
});
it('should call auth.register() with $scope.user', function () {
$scope.register();
expect(auth.register).toHaveBeenCalledWith($scope.user);
});
});
Which results in the following error:
Error: Unexpected request: GET modules/home/home.html
No more requests expected
Any ideas what I need to do in order to mock the routes? I've tried a few things but nothings working so far.
Additional code:
RegistrationCtrl
.controller('RegistrationCtrl', function($scope, $state, auth) {
$scope.user = {};
$scope.userExists = false;
$scope.error = '';
$scope.register = function() {
auth.register($scope.user)
.then(function(response){
$state.go('secure.user');
})
.catch(function(err){
$scope.error = err;
});
};
});
assuming your static files are all in /modules:
$httpBackend.whenGET(/modules\/[\w\W]*/).passThrough();
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;
});
};
});
Given i have a service like this.
angular.module('app')
.factory('Session', function Session($rootScope, $cookieStore) {
var user;
if (user = $cookieStore.get('user')) {
$rootScope.currentUser = user;
}
});
and a test
'use strict';
describe('Service: Session', function () {
var Session,
_rootScope,
_cookieStore;
beforeEach(module('app'));
beforeEach(module(function($provide, $injector) {
_rootScope = $injector.get('$rootScope').$new();
_cookieStore = {
get: angular.noop
};
$provide.value('$rootScope', _rootScope);
$provide.value('$cookieStore', _cookieStore);
}));
beforeEach(inject(function(_Session_) {
Session = _Session_;
}));
it('transfers the cookie under user into the currentUser', function() {
spyOn(_cookieStore, 'get').andReturn('user');
inject(function(_Session_) {
Session = _Session_;
});
expect(_rootScope.currentUser).toEqual('user');
});
});
I end up getting
Error: [$injector:unpr] Unknown provider: $rootScope
http://errors.angularjs.org/1.2.6/$injector/unpr?p0=%24rootScope
Can someone explain to me what concept I'm missing? I'm finding unit testing services to be exceedingly difficult.
The trick was to use $injector to explicitly instantiate the service at a specific moment in time. (Thanks for your help #caitp)
'use strict';
describe('Service: Session', function () {
var _cookieStore;
beforeEach(module('rallyApp'));
beforeEach(module(function($provide) {
_cookieStore = {
get: angular.noop
};
$provide.value('$cookieStore', _cookieStore);
}));
it('transfers the cookie under user into the currentUser', function() {
inject(function($rootScope, $injector) {
spyOn(_cookieStore, 'get').andReturn('caitp');
$injector.get('Session');
expect($rootScope.currentUser).toBe('caitp');
});
});
});