So I'm new to the world of JavaScript and AngularJS and therefor my code is not as good as it should be yet, but it's improving. Nevertheless I started learning and implementing a simple login page with a REST Backend. After the Login-Form is submitted, a authentication-token is returned and set as a default http-header property like this
$http.defaults.headers.common['X-AUTH-TOKEN'] = data.authToken;
This works fine whenever I test it manually, but that's not the way to go so I'd like to implement a unit-test which checks if the X-AUTH-TOKEN header is set.
Is there a way to check that with $httpBackend? e.g I have the following test:
describe('LoginController', function () {
var scope, ctrl, $httpBackend;
// Load our app module definition before each test.
beforeEach(module('myApp'));
// 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 (_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
scope = $rootScope.$new();
ctrl = $controller('LoginController', {$scope: scope}, {$http: $httpBackend}, {$location: null});
}));
it('should create an authToken and set it', function () {
$httpBackend.expectPOST('http://localhost:9000/login', '200').respond(200, '{"authToken":"52d29fd63004c92b972f6b99;65e922bc-5e33-4bdb-9d52-46fc352189fe"}');
scope.login('200');
$httpBackend.flush();
expect(scope.data.authToken).toBe('52d29fd63004c92b972f6b99;65e922bc-5e33-4bdb-9d52-46fc352189fe');
expect(scope.loginValidationOverallError).toBe(false);
expect(scope.status).toBe(200);
});
My Controller looks like this:
.controller('LoginController', ['$scope', '$http', '$location',
function ($scope, $http, $location) {
// Login Stuff
$scope.data = {};
$scope.status = {};
$scope.loginValidationOverallError = false;
$scope.login = function (user) {
$http.post('http://localhost:9000/login', user).success(function (data, status) {
$scope.data = data;
$scope.status = status;
$scope.loginValidationOverallError = false;
console.log($scope.status, $scope.data);
$http.defaults.headers.common['X-AUTH-TOKEN'] = data.authToken;
$location.path('/user');
}).error(function (data, status) {
console.log(status + ' error');
$scope.loginValidationOverallError = true;
});
};
...
I checked the documentation at http://docs.angularjs.org/api/ngMock.$httpBackend but am not sure if the last test is actually applicable to my code (and how that code actually tests something)
it('should send auth header', function() {
var controller = createController();
$httpBackend.flush();
$httpBackend.expectPOST('/add-msg.py', undefined, function(headers) {
// check if the header was send, if it wasn't the expectation won't
// match the request and the test will fail
return headers['Authorization'] == 'xxx';
}).respond(201, '');
$rootScope.saveMessage('whatever');
$httpBackend.flush();
});
I was facing the same issue and I finally solved it. It was very tricky
Souce code for AuthenticationService.login() function
$http.post(...)
.success(function(data) {
...
$http.defaults.headers.common['Authorization'] = data.oauth_token;
});
Test code
beforeEach(inject(function(_$httpBackend_,AuthenticationService) {
$httpBackend = _$httpBackend_;
$authenticationService = AuthenticationService;
}));
it('should login successfully with correct parameter', inject(function($http) {
// Given
...
...
var fakeResponse = {
access_token: 'myToken'
}
$httpBackend.expectPOST('oauth/token',urlEncodedParams, function(headers) {
return headers['Content-Type'] === 'application/x-www-form-urlencoded';
}).respond(200, fakeResponse);
// When
$authenticationService.login(username,password);
// Then
$httpBackend.flush();
expect($http.defaults.headers.common['Authorization']).toBe('myToken');
The trick here is that the default header is set on the real $http service, not the mocked $httpBackend. That's why you should inject the real $http service
I've tried testing the $httpBackend but got an "undefined" error because $httpBackend does not have 'defaults' property
Related
I need to test my signin process using karma unit testing, but I am stuck with an error while executing the test.
My controller code for login:
$scope.signin = function (valid) {
if (valid) {
$http.post('/auth/signin', $scope.credentials).success(function (response) {
console.log(response)
// If successful we assign the response to the global user model
$scope.authentication.user = response.user;
localStorage.setItem('token',response.token.logintoken);
// And redirect to the index page
$scope.$dismiss();
}).error(function (response) {
$scope.error = response.message;
});
} else {
$scope.userSigninrequredErr = true;
}
};
Karma test code:
(function () {
// Authentication controller Spec
describe('AuthenticationController', function () {
// Initialize global variables
var AuthenticationController,
scope,
$httpBackend,
$stateParams,
$cookieStore,
notify,
$location,
modal;
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_, _$cookieStore_, _notify_) {
// Set a new global scope
scope = $rootScope.$new();
// Point global variables to injected services
$stateParams = _$stateParams_;
$httpBackend = _$httpBackend_;
$cookieStore = _$cookieStore_;
notify = _notify_;
$location = _$location_;
modal = jasmine.createSpyObj('modal', ['show', 'hide']);
// Initialize the Authentication controller
AuthenticationController = $controller('AuthenticationController', {
$scope: scope
});
}));
it('$scope.signin() should login with a correct user and password', function () {
// Test expected GET request {user: 'Fred', token: {logintoken: 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9'}}
scope.credentials.user = 'jenson';
scope.credentials.token = {logintoken: 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9'};
$httpBackend.when('POST', '/auth/signin').respond(200, scope.credentials);
scope.signin(true);
$httpBackend.when('GET', /\.html$/).respond('');
$httpBackend.flush();
// Test scope value
expect(scope.credentials.user).toEqual('jenson');
expect($location.url()).toEqual('/dashboard');
});
}
I'm getting the correct response after executing karma test but the following error is showing in the devtools console.
TypeError: 'undefined' is not a function (evaluating '$scope.$dismiss()')
Any help is appreciated. Thanks.
So the problem is that in "real" code, the modal service is invoking the controller and creating a new scope for you, populating the $dismiss() method on it, but in your unit test, you are invoking the controller manually (using the $controller service), meaning there is no $scope.$dismiss() function, hence the error.
Instead, you need to either do what you do in the real code, and use the modal service to instantiate your controller OR at the very least, mimic what the modal service does and add some stub functions on your new scope, including $dismiss().
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 the following test for a service object and the promise doesn't return and neither does the http request get called from inside the service, but it works in browser testing.
'use strict';
describe('Service: AuthService', function () {
// load the controller's module
beforeEach(module('adminPanelAngularApp'));
var AuthService, AuthService, $rootScope;
// Initialize the controller and a mock scope
beforeEach(inject(function (_AuthService_, _$rootScope_) {
AuthService = _AuthService_;
$rootScope = _$rootScope_;
}));
it('it auths', function () {
AuthService.login(SOMECREDENTIALS).then(function(){
console.log('this doesnt output in log');
});
expect(3).toBe(3);
});
});
this is my service
angular.module('adminPanelAngularApp').factory('AuthService', ['$http', '$cookieStore', '$rootScope', '$location', '$q', function ($http, $cookieStore, $rootScope, $location, $q) {
var authService = {};
....
authService.get_current_user = function(){
return $rootScope.current_user;
}
authService.login = function (credentials) {
var url = REDACTED;
return $http.post(server+url).then(function (res) {
if (!res.data){
return false;
}
if (res.data.error){
$rootScope.login_error = res.data.error;
}
var user = {
email: res.data.email,
session: res.data.session,
uid: res.data.uid
}
$cookieStore.put('loginData', user);
$rootScope.current_user = user;
return user;
});
};
...
what am I doing wrong with the tests?
I know my code is pretty bad too, but if I can test this then i'm halfway there.
If you don't want to mock $http, I suggest you to use $httpBackend.
With $httpBackend you can mock the calls you make with $http.
Imagine this service:
app.factory('Auth', function($http) {
return {
login: function() {
return $http.post('/login');
}
};
});
The goal is to test that you make your $http.post and it returns successfully, so the idea is like:
describe('Service: Auth', function() {
var Auth, $httpBackend;
beforeEach(function() {
module('app');
inject(function(_Auth_, _$httpBackend_) {
Auth = _Auth_;
$httpBackend = _$httpBackend_;
$httpBackend.whenPOST('/login').respond(200);
});
});
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should do a proper login', function() {
var foo;
Auth.login().then(function() {
foo = "success";
});
$httpBackend.flush();
expect(foo).toBe("success");
});
});
So, for starters, we inject what we need (Auth and $httpBackend)
And then, we call the whenPOST of $httpBackend. Basically it does something like:
When someone does a POST to /login, respond it with 200
Then on the test, we call login which is going to do the $http.post. To process this $http.post, since it is async, we can simulate the real call doing a $httpBackend.flush() which is going to "process" the call.
After that, we can verify that the .then was executed.
What about the afterEach? We don't really need it for this example, but when you want to assert yes or yes that a call was made, you can change the whenPOST to expectPOST, to make a test fail if that POST is never made. The afterEach is basically checking the status of the $httpBackend to see if any of those expectation weren't matched.
On the other hand, you don't need to create a promise by hand. $http returns a promise for you, so you can return the $http call directly, and on the $http then you can:
return user;
That will simplify the implementation a little bit.
Demo here
So, i am trying to follow this tutorial while trying to create some unit tests along the way.
Well i am stuck at trying to test the $http request to the ergast developer api. As u can see in the tutorial the $http request is made to the service ergastAPIservice
angular.module('F1FeederApp.services', []).
factory('ergastAPIservice', function($http) {
var ergastAPI = {};
ergastAPI.getDrivers = function() {
return $http({
method: 'JSONP',
url: 'http://ergast.com/api/f1/2013/driverStandings.json?callback=JSON_CALLBACK'
});
}
return ergastAPI;
});
Which is then used in the controller:
angular.module('F1FeederApp.controllers', []).
controller('driversController', function($scope, ergastAPIservice) {
$scope.nameFilter = null;
$scope.driversList = [];
ergastAPIservice.getDrivers().success(function (response) {
$scope.driversList = response.MRData.StandingsTable.StandingsLists[0].DriverStandings;
});
});
How would i go about testing this (the service). I have started up something like this:
describe('F1FeederApp services', function() {
describe('ergastAPIservice', function(){
var scope, ctrl, $httpBackend;
beforeEach(module('F1FeederApp'));
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('driversController', {$scope: scope});
$httpBackend = _$httpBackend_;
$httpBackend.whenJSONP("
http://ergast.com/api/f1/2013/driverStandings.json?callback=JSON_CALLBACK").
respond(WHAT SHOULD GO HERE);
}));
it("should get ???", function () {
$httpBackend.flush();
expect(WHAT SHOULD GO HERE).toEqual('foo');
});
});});
Like i said stuck,getting this to work, i thought that what ever you put in the respond should be what u expect in your test, that it doesn't have anything to do with the actual mocked $http request...or am i getting this all wrong
Any tips and/or pointing me in the right direction would be very appreciated,
Cheers!
I would like to unit test the following AngularJs service:
.factory('httpResponseInterceptor', ['$q', '$location', '$window', 'CONTEXT_PATH', function($q, $location, $window, contextPath){
return {
response : function (response) {
//Will only be called for HTTP up to 300
return response;
},
responseError: function (rejection) {
if(rejection.status === 405 || rejection.status === 401) {
$window.location.href = contextPath + '/signin';
}
return $q.reject(rejection);
}
};
}]);
I have tried with the following suite:
describe('Controllers', function () {
var $scope, ctrl;
beforeEach(module('curriculumModule'));
beforeEach(module('curriculumControllerModule'));
beforeEach(module('curriculumServiceModule'));
beforeEach(module(function($provide) {
$provide.constant('CONTEXT_PATH', 'bignibou'); // override contextPath here
}));
describe('CreateCurriculumCtrl', function () {
var mockBackend, location, _window;
beforeEach(inject(function ($rootScope, $controller, $httpBackend, $location, $window) {
mockBackend = $httpBackend;
location = $location;
_window = $window;
$scope = $rootScope.$new();
ctrl = $controller('CreateCurriculumCtrl', {
$scope: $scope
});
}));
it('should redirect to /signin if 401 or 405', function () {
mockBackend.whenGET('bignibou/utils/findLanguagesByLanguageStartingWith.json?language=fran').respond([{"description":"Français","id":46,"version":0}]);
mockBackend.whenPOST('bignibou/curriculum/new').respond(function(method, url, data, headers){
return [401];
});
$scope.saveCurriculum();
mockBackend.flush();
expect(_window.location.href).toEqual("/bignibou/signin");
});
});
});
However, it fails with the following error message:
PhantomJS 1.9.2 (Linux) Controllers CreateCurriculumCtrl should redirect to /signin if 401 or 405 FAILED
Expected 'http://localhost:9876/context.html' to equal '/bignibou/signin'.
PhantomJS 1.9.2 (Linux) ERROR
Some of your tests did a full page reload!
I am not sure what is going wrong and why. Can anyone please help?
I just want to ensure the $window.location.href is equal to '/bignibou/signin'.
edit 1:
I managed to get it to work as follows (thanks to "dskh"):
beforeEach(module('config', function($provide){
$provide.value('$window', {location:{href:'dummy'}});
}));
You can inject stub dependencies when you load in your module:
angular.mock.module('curriculumModule', function($provide){
$provide.value('$window', {location:{href:'dummy'}});
});
To get this to work for me I had to make a minor adjustment. It would error out and say:
TypeError: 'undefined' is not an object (evaluating '$window.navigator.userAgent')
So I added the navigator.userAgent object to get it to work for me.
$provide.value('$window', {
location:{
href:'dummy'
},
navigator:{
userAgent:{}
}
});
I faced the same problem, and went a step further in my solution. I didn't just want a mock, I wanted to replace $window.location.href with a Jasmine spy for the better ability to track changes made to it. So, I learned from apsiller's example for spying on getters/setters and after creating my mock, I was able to spy on the property I wanted.
First, here's a suite that shows how I mocked $window, with a test to demonstrate that the spy works as expected:
describe("The Thing", function() {
var $window;
beforeEach(function() {
module("app", function ($provide) {
$provide.value("$window", {
//this creates a copy that we can edit later
location: angular.extend({}, window.location)
});
});
inject(function (_$window_) {
$window = _$window_;
});
});
it("should track calls to $window.location.href", function() {
var hrefSpy = spyOnProperty($window.location, 'href', 'set');
console.log($window.location.href);
$window.location.href = "https://www.google.com/";
console.log($window.location.href);
expect(hrefSpy).toHaveBeenCalled();
expect(hrefSpy).toHaveBeenCalledWith("https://www.google.com/");
});
});
As you can see above, the spy is generated by calling the below function: (it works for both get and set)
function spyOnProperty(obj, propertyName, accessType) {
var desc = Object.getOwnPropertyDescriptor(obj, propertyName);
if (desc.hasOwnProperty("value")) {
//property is a value, not a getter/setter - convert it
var value = desc.value;
desc = {
get: function() { return value; },
set: function(input) { value = input; }
}
}
var spy = jasmine.createSpy(propertyName, desc[accessType]).and.callThrough();
desc[accessType] = spy;
Object.defineProperty(obj, propertyName, desc);
return spy;
}
Lastly, here's a fiddle demonstrating this in action. I've tested this against Angular 1.4, and Jasmine 2.3 and 2.4.