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.
Related
I'm trying to test an Angular service with Karma, and was getting some strange results, so just for kicks I made two tests that were identical to see if I would get the same results, but I didn't:
describe('Profile Data Service', function() {
var LoginData, rootScope, $httpBackend;
beforeEach(module('myApp'));
describe('get profile data', function() {
beforeEach(inject(function(_LoginData_, $rootScope, _$httpBackend_) {
LoginData = _LoginData_;
LoginData.username= "jsmith";
LoginData.token = "token";
rootScope = $rootScope;
$httpBackend = _$httpBackend_;
}));
it('should get profile data on instantiation', inject(function(ProfileDataService) {
$httpBackend.expect('POST', 'http://mytestserver.com/api/', {
params: [{
username: "jsmith",
token: "token",
method: "getProfile"
}]
})
.respond({
result: {
address: "111 Main St."
city: "Los Angeles",
state: "CA"
}
});
$httpBackend.flush();
expect(ProfileDataService.profileData.address).toMatch("111 Main St.");
}));
it('should get profile data on instantiation', inject(function(ProfileDataService) {
$httpBackend.expect('POST', 'http://mytestserver.com/api/', {
params: [{
username: "jsmith",
token: "token",
method: "getProfile"
}]
})
.respond({
result: {
address: "111 Main St."
city: "Los Angeles",
state: "CA"
}
});
$httpBackend.flush();
expect(ProfileDataService.profileData.address).toMatch("111 Main St.");
}));
});
});
The first test passes, but the second test states that profileData is undefined. They are identical tests. I am assuming that for each it that the ProfileDataService is being re-initialized, but that may not be the case. If that's not true, how do I create separate tests so that my service is destroyed and re-initialized for each test case?
The logic for the service is fairly straightforward:
(function() {
'use strict';
angular.module('servicesModule')
.service('ProfileDataService', ProfileDataService);
ProfileDataService.$inject = ["$http", "$q", "LoginData", "CacheFactory"];
function ProfileDataService($http, $q, LoginData, CacheFactory) {
var ProfileDataService = this;
(function() {
init();
})();
function init() {
getProfile().then(function(profileData) {
ProileDataService.profileData = profileData;
});
}
function getProfile() {
var deferred = $q.defer();
var data = {
params: [{
username: LoginData.username,
token: LoginData.token,
method: "getProfile"
}]
};
var profileDataCache = CacheFactory.get('profileDataCache');
if (profileDataCache.get('profileData') && !invalidateCache) {
deferred.resolve(profileDataCache.get('profileData'));
}
else {
$http.post('http://mytestserver.com/api/', data)
.success(function(data) {
profileDataCache.put('profileData', response.result);
deferred.resolve(data.result);
});
}
return deferred.promise;
}
}
})();
I should also note that I've tried adding rootScope.$digest() in different places in the test, but that doesn't seem to make any difference. I thought manually triggering the digest cycle would ensure that the http post was caught and the response mocked.
Edit: I left out a huge detail... angular-cache. I forgot to mention that I was using a cacheing plugin. This was the reason for my problem. After creating a plunker and seeing that I was getting consistent results for identical tests, I knew there was something I was failing to realize. The second test was not making a POST request because of my cacheing logic.
My issue was failing to realize that my use of a cache was giving me different results for each test. Specifically, my service logic prevented a second POST to the api if a cache had already been persisted for that response.
I'm trying to work out how to unit test my login controller with Karma/Jasmine/Mocha.
I basically want to test if a 200 comes back from the $auth.login() then the message saved should be equal to "successfully logged in",
otherwise if I receive a 401 then the message that comes back should be "error logging in".
UPDATE
This is where I'm at, at the moment.
login.controller.js
function loginCtrl($auth, $scope, $rootScope, $location) {
var vm = this;
vm.login = function() {
var credentials = { email: vm.email, password: vm.password };
// Use Satellizer's $auth service to login
$auth.login(credentials).then(function() {
vm.message = "Successfully logged in!";
}, function(error) {
vm.message = "Error logging in!";
}).then(function(responses) {
$location.path('home');
});
};
}
login.controller.spec.js
describe('Login Controller', function() {
var q, scope, ctrl, auth;
beforeEach(module('app.login'));
beforeEach(inject(function($q, $rootScope, $controller, $auth) {
q = $q;
scope = $rootScope.$new();
ctrl = $controller('loginCtrl', { $scope: scope, SessionService: sessionService, $auth: auth, $q: q });
auth = $auth;
}));
it('should present a successfull message when logged in', function () {
var defer = q.defer();
sinon.stub(auth, 'login')
.withArgs({ email: 'test#test.com', password: 'test_password' })
.returns(defer.promise);
ctrl.login();
defer.resolve();
scope.$apply();
expect(ctrl.message).to.equal('Successfully logged in!');
});
});
Since this is your controller test, you'd probably need to spyOn your service ($auth) like so (in Jasmine) -
var defer = $q.defer();
spyOn('$auth', login).andReturn(defer.promise);
controller.email = 'test#test.com';
controller.password = 'test_password';
controller.login();
defer.resolve();
scope.$apply();
expect($auth.login).toHaveBeenCalledWith({ email: 'test#test.com', password: 'test_password' });
expect(scope.message).toEqual("successfully logged in");
and for the failure case using defer.reject() and pretty much the same format for assertion.
In my opinion, you'd end up worrying about http related status-codes or responses only at the service level and not at the controller level. There you'd use $httpBackend to mock the responses with their status-codes and the corresponding responses.
EDIT
In mocha, as per my research, you'd end up doing something like -
sinon.stub($auth, 'login')
.withArgs({ email: 'test#test.com', password: 'test_password' })
.returns(defer.promise);
to stub the method. And the verification of the call as -
sinon.assert.calledOnce($auth.login);
Rest of it remains the same. The assertion of the message will also change to assert.equal for mocha.
EDIT
Checkout this fiddle - http://jsfiddle.net/9bLqh5zc/. It uses 'sinon' for the spy and 'chai' for assertion
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'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
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');
});;
});