mocking angular service for testing controllers - angularjs

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.

Related

Issue with closing login module for jasmine testing

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;
});
};
});

Confused by angular unit test

I'm testing out on of my controllers. I keep getting
Error: Expected POST /auth/signup with different data
EXPECTED: {"username":"justin","firstName":"Justin","lastName":"Young","email":"xxxx#xxx.com","company":"5579d602ba9f26a414be5d57","url":"http://www.me.com","referrer":"me#me.com"}
It's completing the post to auth/signup as expected, but the data is empty? I'm passing in sampleUserResponse into the expectations so I don't get why it's not passing that data back as the response. What am I doing wrong?
My test:
'use strict';
(function() {
// Authentication controller Spec
describe('AdminItemController', function() {
// Initialize global variables
var controller,
scope,
$httpBackend,
$stateParams,
$location;
beforeEach(function() {
jasmine.addMatchers({
toEqualData: function(util, customEqualityTesters) {
return {
compare: function(actual, expected) {
return {
pass: angular.equals(actual, expected)
};
}
};
}
});
});
// Load the main application module
beforeEach(module(ApplicationConfiguration.applicationModuleName));
// The injector ignores leading and trailing underscores here (i.e. _$httpBackend_).
// This allows us to inject a service but then attach it to a variable
// with the same name as the service.
beforeEach(inject(function($controller, $rootScope, _$location_, _$stateParams_, _$httpBackend_) {
// Set a new global scope
scope = $rootScope.$new();
// Point global variables to injected services
$stateParams = _$stateParams_;
$httpBackend = _$httpBackend_;
$location = _$location_;
// Initialize the Authentication controller
controller = $controller('AdminItemController', {
$scope: scope,
$stateParams:$stateParams
});
$httpBackend.when('GET', 'modules/core/views/home.client.view.html').respond({});
}));
it('$scope.create() with valid form data should send a POST request with the form input values and then locate to new object URL', inject(function(Users) {
// Create a sample article object
var sampleUserPostData = new Users({
username: 'justin',
firstName:'Justin',
lastName:'Young',
email:'xxxx#xxx.com',
company:'5579d602ba9f26a414be5d57',
url:'http://www.me.com',
referrer:'me#me.com'
});
// Create a sample User response
var sampleUserResponse = new Users({
_id:'4579d602ba9f26a414be5d59',
username: 'justin',
firstName:'Justin',
lastName:'Young',
email:'xxxx#xxx.com',
company:'5579d602ba9f26a414be5d57',
url:'http://www.me.com',
referrer:'me#me.com'
});
// Fixture mock form input values
//scope.title = 'An User about MEAN';
//scope.content = 'MEAN rocks!';
// Set POST response
$httpBackend.expectPOST('/auth/signup', sampleUserPostData).respond(sampleUserResponse);
// Run controller functionality
scope.addPost();
$httpBackend.flush();
// Test form inputs are reset
//expect(scope.title).toEqual('');
//expect(scope.content).toEqual('');
// Test URL redirection after the User was created
//expect($location.path()).toBe('/admin/users/' + sampleUserResponse._id);
}));
});
}());
My Simplified Controller:
.controller('AdminItemController', ['$scope', '$http', '$location','apiResource','$stateParams', '$state','$log',
function($scope, $http, $location, apiResource, $stateParams, $state, $log) {
$scope.addPost = function() {
apiResource.save({api_resource:'auth', api_action: 'signup'},$scope.item).$promise.then(function(response){
$scope.$parent.users.push($scope.item);
});
};
}
])
The problem is in your controller, the params you send with the POST is $scope.item but in your test, you DO NOT set your $scope.item to be anything. Therefore, a POST with undefined params will be sent (because $scope.item is undefined). Moreover, in your test, you expect the params sent to equal to sampleUserPostData. Apparently it will fail because undefined !== sampleUserPostData. What you can do is just to set the scope.item = sampleUserPostData; before expectPOST and it will be fine.
Working fiddle.

Testing Angular controllers using controllerAs

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

angularjs testing with jasmine - $httpBackend not giving me the right behavior

This is my Controller:
rcCtrls.controller('LoginController', [
'$scope',
'AuthService',
function($scope, AuthService)
{
console.log('LoginController');
$scope.credantials = {
email: '',
password: '',
remember: false
}
$scope.login = function(credantials)
{
console.log('before login');
AuthService.login(credantials.email, credantials.password, credantials.remember || false).success(function(response) {
console.log('login successful');
console.log(response);
location.reload();
}).error(function() {
console.log('failed to login');
});
};
}]);
This is my AuthService:
rcServices.factory('AuthService', [
'$http',
function($http)
{
return {
login: function(email, password, remember) {
console.log('Auth Service Login');
return $http.post('/auth/login', {email: email, password: password, remember: remember});
},
logout: function() {
return $http.post('/auth/logout');
}
};
}]);
This is my spec:
describe('Controller: LoginController', function() {
// We initiate the app moudle mock
beforeEach(module('app')); // alias for angular.mock.module('app')
beforeEach(inject(function($controller, $rootScope, AuthService, $httpBackend){
this.$httpBackend = $httpBackend;
this.scope = $rootScope.$new();
$controller('LoginController', {
$scope: this.scope,
AuthService: AuthService
});
}));
describe("checking credantials at the begining", function() {
it("should be initiated", function() {
expect(this.scope.credantials).toEqual({ email: '', password: '', remember: false });
});
});
describe("successfully logged in", function() {
it("should redirect to home", function() {
this.scope.credantials = { email: 'test#mail.com', password: '123', remember: false };
this.$httpBackend.expectPOST('/auth/login', this.scope.credantials).respond(200, {test: 'test'});
this.scope.login(this.scope.credantials);
console.log(this);
this.$httpBackend.flush();
expect(this.scope).toBeDefined();
});
});
});
It seems as if the $httpBackend doesn't do what it is suppose to do.
I indeed get all the console logs up until the moment where my Service uses $http post request, and there it stops.
FYI, my controller is working properly on the app itself! The post is happening and the response is returning properly.
However the test just doesn't...
I'm getting and error:
Error: No pending request to flush !
Commenting it doesn't help.
EDIT:
The issue was my angular-mock.js version.... how sad.
In your service you call:
location.reload();
This could be messing with jasmine and causing unpredictable behavior. What does this call do, do you need it, and what happens to your tests if you just comment that out?
If you really need to make that call, you should use AngularJS built in $location service and mock it so that you keep the behavior but not break jasmine in the process.
===========
Update:
Additionally, it's a little odd that you're using this.httpBackend, when normally you'd totally overwrite the reference - not storing it as an attribute of this but a variable global to your tests. I am suspicious that somewhere else in your app you have called flush already and the reference isn't being reset properly. Do you have any other tests calling flush, anywhere?
http://plnkr.co/edit/jBewavvO1n8NBa6WGFja
I made working tests in this plunkr, which pass with your code as it is. With the exception of the location.reload() line which I commented out, which did not break the tests, although this test runner is a bit different as its in a plunkr and that won't always be the case. You really should not be calling that in tests.

value does not update after running a function in karma/jasmine

Im new to jasmine/karma, trying to write a test for my angular application and i have a problem that i can't solve, hopefully someone here can help me.
My problem is that my login() function updates a value inside my controller but jasmine fails to see the updated value when it's inside .then() and the test fails, but when i update the value outside of the .then() it passes successfully.
here is my controller:
var Authctrl = this;
Authctrl.myVariable = "oldValue";
Authctrl.login = function () {
AuthService.login(Authctrl.credentials).then(function(authData){
Authctrl.credentials = { email: '',password: ''};
/*case 1 */ Authctrl.myVariable = "newValue"; //test gives error
},function(error){
console.log(error);
Authctrl.errors.login = 'Wrong username or password. Please try again';
});
/*case 2 */ Authctrl.myVariable = "newValue"; //test passes successfully
};
and my test code:
it('should be newValue',function(){
Authctrl.credentials = {
email: 'myEmail#yahoo.com',
password: '12345'
};
Authctrl.login();
expect(Authctrl.myVariable).toBe('newValue');
});
and my service:
authService.login = function (credentials) {
return $q(function(resolve, reject){
ref.authWithPassword(credentials , function(error, authData) {
if (error === null) {
// user authenticated with Firebase
console.log('SERVICE IS RUNNING, success'); //this does not log when testing with karma
resolve(authData);
} else {
console.log('SERVICE IS RUNNING, error'); //this does not log when testing with karma
reject(error);
}
},{
remember: "default"
});//ref.authWithPassword end
console.log('SERVICE IS RUNNING'); //this logs when testing with karma
});//$q end
};//authService.login end
so I finally figured this out and decided to post the answer here in case others run into the same problem. as #MatthewGreen mentioned, i had to create a mock service. and use the $provide to define mock AuthService methods and their return values. i followed many tutorials online and i kept getting errors, then i learnt that few things have changed in new jasmine and one of them is the spyOn command. This tutorial helped me a lot.
'use strict';
describe('Controller: AuthCtrl', function () {
var $rootScope,$scope,$controller,AuthService,AuthCtrl;
//fake firebase user data
var mockAuthData = {
provider: 'password',
password:{
email: 'myEmail#yahoo.com',
isTemporaryPassword: false
},
auth:{
provider:'password',
uid:'simplelogin:1'
},
uid:'simplelogin:1'
};
beforeEach(function() {
module('myApp');
// Provide will help us create fake implementations for our dependencies
module(function($provide) {
// Fake AuthService Implementation returning a promise
$provide.value('AuthService', {
login:function(){
return{
then:function(callback){return callback(mockAuthData);}
};
}
});
return null;
});
});
// load the controller's module
beforeEach(inject(function(_$rootScope_, _$controller_, _$q_, _AuthService_) {
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
$controller = _$controller_;
AuthService = _AuthService_;
AuthCtrl = $controller('AuthCtrl',
{'$rootScope' : $rootScope, '$scope': $scope, 'AuthService': AuthService});
$rootScope.$apply();
}));
it("myVariable should be newValue", function() {
spyOn(AuthService, 'login').and.callThrough();
AuthCtrl.login();
expect(AuthService.login).toHaveBeenCalled();
expect(AuthCtrl.myVariable).toBe('newValue');
});
it("should retrieve the email address", function() {
spyOn(AuthService, 'login').and.callThrough();
AuthCtrl.login();
expect(AuthService.login).toHaveBeenCalled();
expect(AuthCtrl.userEmail).toBe('myEmail#yahoo.com');
});;
});

Resources