My understanding is that when you load your module in Angular unit tests, the run block gets called.
I'd think that if you're testing a component, you wouldn't want to simultaneously be testing the run block, because unit tests are supposed to just test one unit. Is that true?
If so, is there a way to prevent the run block from running? My research leads me to think that the answer is "no", and that the run block always runs when the module is loaded, but perhaps there's a way to override this. If not, how would I test the run block?
Run block:
function run(Auth, $cookies, $rootScope) {
$rootScope.user = {};
Auth.getCurrentUser();
}
Auth.getCurrentUser:
getCurrentUser: function() {
// user is logged in
if (Object.keys($rootScope.user).length > 0) {
return $q.when($rootScope.user);
}
// user is logged in, but page has been refreshed and $rootScope.user is lost
if ($cookies.get('userId')) {
return $http.get('/current-user')
.then(function(response) {
angular.copy(response.data, $rootScope.user);
return $rootScope.user;
})
;
}
// user isn't logged in
else {
return $q.when({});
}
}
auth.factory.spec.js
describe('Auth Factory', function() {
var Auth, $httpBackend, $rootScope, $cookies, $q;
var user = {
username: 'a',
password: 'password',
};
var response = {
_id: 1,
local: {
username: 'a',
role: 'user'
}
};
function isPromise(el) {
return !!el.$$state;
}
beforeEach(module('mean-starter', 'ngCookies', 'templates'));
beforeEach(inject(function(_Auth_, _$httpBackend_, _$rootScope_, _$cookies_, _$q_) {
Auth = _Auth_;
$httpBackend = _$httpBackend_;
$rootScope = _$rootScope_;
$cookies = _$cookies_;
$q = _$q_;
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('#signup', function() {
$rootScope.user = {};
$httpBackend.expectPOST('/users', user).respond(response);
spyOn(angular, 'copy').and.callThrough();
spyOn($cookies, 'put').and.callThrough();
var retVal = Auth.signup(user);
$httpBackend.flush();
expect(angular.copy).toHaveBeenCalledWith(response, $rootScope.user);
expect($cookies.put).toHaveBeenCalledWith('userId', 1);
expect(isPromise(retVal)).toBe(true);
});
it('#login', function() {
$rootScope.user = {};
$httpBackend.expectPOST('/login', user).respond(response);
spyOn(angular, 'copy').and.callThrough();
spyOn($cookies, 'put').and.callThrough();
var retVal = Auth.login(user);
$httpBackend.flush();
expect(angular.copy).toHaveBeenCalledWith(response, $rootScope.user);
expect($cookies.put).toHaveBeenCalledWith('userId', 1);
expect(isPromise(retVal)).toBe(true);
});
it('#logout', function() {
$httpBackend.expectGET('/logout').respond();
spyOn(angular, 'copy').and.callThrough();
spyOn($cookies, 'remove');
Auth.logout();
$httpBackend.flush();
expect(angular.copy).toHaveBeenCalledWith({}, $rootScope.user);
expect($cookies.remove).toHaveBeenCalledWith('userId');
});
describe('#getCurrentUser', function() {
it('User is logged in', function() {
$rootScope.user = response;
spyOn($q, 'when').and.callThrough();
var retVal = Auth.getCurrentUser();
expect($q.when).toHaveBeenCalledWith($rootScope.user);
expect(isPromise(retVal)).toBe(true);
});
it('User is logged in but page has been refreshed', function() {
$cookies.put('userId', 1);
$httpBackend.expectGET('/current-user').respond(response);
spyOn(angular, 'copy').and.callThrough();
var retVal = Auth.getCurrentUser();
$httpBackend.flush();
expect(angular.copy).toHaveBeenCalledWith(response, $rootScope.user);
expect(isPromise(retVal)).toBe(true);
});
it("User isn't logged in", function() {
$rootScope.user = {};
$cookies.remove('userId');
spyOn($q, 'when').and.callThrough();
var retVal = Auth.getCurrentUser();
expect($q.when).toHaveBeenCalledWith({});
expect(isPromise(retVal)).toBe(true);
});
});
});
Attempt 1:
beforeEach(module('mean-starter', 'ngCookies', 'templates'));
beforeEach(inject(function(_Auth_, _$httpBackend_, _$rootScope_, _$cookies_, _$q_) {
Auth = _Auth_;
$httpBackend = _$httpBackend_;
$rootScope = _$rootScope_;
$cookies = _$cookies_;
$q = _$q_;
}));
beforeEach(function() {
spyOn(Auth, 'getCurrentUser');
});
afterEach(function() {
expect(Auth.getCurrentUser).toHaveBeenCalled();
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
This doesn't work. The run block is run when the module is loaded, so Auth.getCurrentUser() is called before the spy is set up.
Expected spy getCurrentUser to have been called.
Attempt 2:
beforeEach(inject(function(_Auth_, _$httpBackend_, _$rootScope_, _$cookies_, _$q_) {
Auth = _Auth_;
$httpBackend = _$httpBackend_;
$rootScope = _$rootScope_;
$cookies = _$cookies_;
$q = _$q_;
}));
beforeEach(function() {
spyOn(Auth, 'getCurrentUser');
});
beforeEach(module('mean-starter', 'ngCookies', 'templates'));
afterEach(function() {
expect(Auth.getCurrentUser).toHaveBeenCalled();
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
This doesn't work because Auth isn't available to be injected before my app module is loaded.
Error: [$injector:unpr] Unknown provider: AuthProvider <- Auth
Attempt 3:
As you can see, there's a chicken-egg problem here. I need to inject Auth and set up the spy before the module is loaded, but I can't because Auth isn't available to be injected before the module is loaded.
This blog posts mentions the chicken-egg problem and provides an interesting potential solution. The author proposes that I should create my Auth service manually using $provide before I load my module. Since I'm creating the service, not injecting it, I could do it before the module is loaded, and I could set up the spy. Then when the module is loaded, it'd use this created mock service.
Here is his example code:
describe('example', function () {
var loggingService;
beforeEach(function () {
module('example', function ($provide) {
$provide.value('loggingService', {
start: jasmine.createSpy()
});
});
inject(function (_loggingService_) {
loggingService = _loggingService_;
});
});
it('should start logging service', function() {
expect(loggingService.start).toHaveBeenCalled();
});
});
The problem with this, is that I need my Auth service! I only would want to use the mock one for the run block; I need my real Auth service elsewhere so I could test it.
I guess that I could create the actual Auth service using $provide, but that feels wrong.
Final question - for whatever code I end up using to deal with this run block problem, is there a way for me to extract it out so I don't have to re-write it for each of my spec files? The only way I could think to do it would be to use some sort of global function.
auth.factory.js
angular
.module('mean-starter')
.factory('Auth', Auth)
;
function Auth($http, $state, $window, $cookies, $q, $rootScope) {
return {
signup: function(user) {
return $http
.post('/users', user)
.then(function(response) {
angular.copy(response.data, $rootScope.user);
$cookies.put('userId', response.data._id);
$state.go('home');
})
;
},
login: function(user) {
return $http
.post('/login', user)
.then(function(response) {
angular.copy(response.data, $rootScope.user);
$cookies.put('userId', response.data._id);
$state.go('home');
})
;
},
logout: function() {
$http
.get('/logout')
.then(function() {
angular.copy({}, $rootScope.user);
$cookies.remove('userId');
$state.go('home');
})
.catch(function() {
console.log('Problem logging out.');
})
;
},
getCurrentUser: function() {
// user is logged in
if (Object.keys($rootScope.user).length > 0) {
return $q.when($rootScope.user);
}
// user is logged in, but page has been refreshed and $rootScope.user is lost
if ($cookies.get('userId')) {
return $http.get('/current-user')
.then(function(response) {
angular.copy(response.data, $rootScope.user);
return $rootScope.user;
})
;
}
// user isn't logged in
else {
return $q.when({});
}
}
};
}
Edit - failed attempt + successful attempt:
beforeEach(module('auth'));
beforeEach(inject(function(_Auth_) {
Auth = _Auth_;
spyOn(Auth, 'requestCurrentUser');
}));
beforeEach(module('mean-starter', 'ngCookies', 'templates'));
beforeEach(inject(function(_Auth_, _$httpBackend_, _$rootScope_, _$cookies_, _$q_) {
// Auth = _Auth_;
$httpBackend = _$httpBackend_;
$rootScope = _$rootScope_;
$cookies = _$cookies_;
$q = _$q_;
}));
// beforeEach(function() {
// spyOn(Auth, 'getCurrentUser');
// });
afterEach(function() {
expect(Auth.getCurrentUser).toHaveBeenCalled();
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
I'm not sure why this wouldn't work (independent of the problem with using inject twice).
I was trying to get around having to use $provide as that initially felt hacky/weird to me. After thinking about it some more though, I now feel that $provide is fine, and that following your suggestion to use mock-auth is fantastic!!! Both worked for me.
In auth.factory.spec.js I just loaded the auth module (I'm calling it auth, not mean-auth) without loading mean-starter. This doesn't have the run block problem because that module doesn't have the run block code, but it allows me to test my Auth factory. Elsewhere, this works:
beforeEach(module('mean-starter', 'templates', function($provide) {
$provide.value('Auth', {
requestCurrentUser: jasmine.createSpy()
});
}));
As does the fantastic mock-auth solution:
auth.factory.mock.js
angular
.module('mock-auth', [])
.factory('Auth', Auth)
;
function Auth() {
return {
requestCurrentUser: jasmine.createSpy()
};
}
user.service.spec.js
beforeEach(module('mean-starter', 'mock-auth', 'templates'));
My understanding is that when you load your module in Angular unit
tests, the run block gets called.
Correct.
I'd think that if you're testing a component, you wouldn't want to
simultaneously be testing the run block, because unit tests are
supposed to just test one unit. Is that true?
Also correct, in that right now you are effectively testing the integration of Auth and your run block, and there is no isolation of one from the other.
If so, is there a way to prevent the run block from running? My
research leads me to think that the answer is "no", and that the run
block always runs when the module is loaded, but perhaps there's a way
to override this. If not, how would I test the run block?
As implemented, no you cannot prevent the run block from running. However, it remains possible with some minor refactoring as your question is ultimately one of modularisation. Without being able to see your module declaration, I would imagine it looks something like this:
angular.module('mean-starter', ['ngCookies'])
.factory('Auth', function($cookies) {
...
});
.run(function(Auth, $rootScope) {
...
});
This pattern can be broken into modules to support testability (and module reusability):
angular.module('mean-auth', ['ngCookies'])
.factory('Auth', function() {
...
});
angular.module('mean-starter', ['mean-auth'])
.run(function(Auth, $rootScope) {
...
});
This now allows you to test your Auth factory in isolation by loading the mean-auth module only into its test.
While this solves the problem of your run block interfering with your unit tests for Auth, you still face the problem of mocking Auth.getCurrentUser so as to test your run block in isolation. The blog post you referenced is correct in that you should be looking to leverage the configuration stage of the module to stub/spy on dependencies used during the run stage. Therefore, in your test:
module('mean-starter', function ($provide) {
$provide.value('Auth', {
getCurrentUser: jasmine.createSpy()
});
});
As to your final question, you can create reusable mocks by declaring them as modules. For example, if you wanted to create a reusable mock factory for Auth you define it in a separate file loaded prior to your unit tests:
angular.module('mock-auth', [])
.factory('Auth', function() {
return {
getCurrentUser: jasmine.createSpy()
};
});
and then load it in your tests subsequent to any module in which you require it, as angular will overwrite any service with the same name:
module('mean-starter', 'mock-auth');
Related
I want to unit test my controller. I started with basic test assertions of expect API. But I am facing challenge in mocking scope methods inside a conditional check. I am getting an undefined error since it is not available under scope, only the global logout() method is available.
I tried mocking the localStorageService using spyOn as true to satisfy the condition, but that's still of no help. Any solution will be of great help to get me kickstarted.
Controller:
angular.module('app').controller('sampleCtrl',
function($scope, $state, $http, $rootScope, localStorageService) {
if (!(localStorageService.get('isAuthenticated'))) {
$state.go('home');
}
if (localStorageService.get('isAuthenticated') === true) {
//http post calls made here to perform certain operation on page load
$scope.someMethod = function(){
//do something
}
}
$scope.logOut = function() {
localStorageService.set('property', '');
localStorageService.set('isAuthenticated', false);
$state.go('home');
};
});
Karma:
'use strict';
describe('Controller: sampleCtrl', function() {
/** to load the controller's module */
beforeEach(module('app'));
var sampleCtrl,scope,httpBackend,deferred,rootScope;
beforeEach(inject(function ($controller,_$rootScope_,$httpBackend,$q) {
var store = {};
scope= _$rootScope_.$new(); // creates a new child scope of $rootScope for each test case
rootScope = _$rootScope_;
localStorageService = _localStorageService_;
httpBackend = $httpBackend;
httpBackend.whenGET(/\.html$/).respond('');
spyOn(localStorageService, 'set').and.callFake(function (key,val) {
store[key]=val;
});
spyOn(localStorageService, 'get').and.callFake(function(key) {
return store[key];
});
sampleCtrl = $controller('sampleCtrl',{
_$rootScope_:rootScope,
$scope:scope,
$httpBackend:httpBackend,
_localStorageService_:localStorageService
// add mocks here
});
localStorageService.set('isAuthenticated',true);
}));
/**ensures $httpBackend doesn’t have any outstanding expectations or requests after each test*/
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequest();
});
it('sampleCtrl to be defined:',function(){
httpBackend.flush();
expect(sampleCtrl).toBeDefined();
});
// failing test case - scope.someMethod not available in scope
it('is to ensure only authenticated user can access the state methods',function(){
localStorageService.get('isAuthenticated');
httpBackend.flush();
expect(scope.someMethod).toBeDefined();
});
});
I've managed to get it work.
The problem was that localStorageService did not have isAuthenticated set to true on starting the controller. Place setting it to true before calling the controller.
This is my LoginController, as you can see I have injected the LoginService, I can't seem to figure out why I am getting the error mentioned above (Note: I've made the project modular by breaking my project in separates folder, using gulp and browserify to bundle everything into one file)
'use strict';
function LoginController($scope, $ionicModal, $timeout, $location,
$ionicLoading, $ionicPopup, LoginService) {
// With the new view caching in Ionic, Controllers are only called
// when they are recreated or on app start, instead of every page change.
// To listen for when this page is active (for example, to refresh data),
// listen for the $ionicView.enter event:
//$scope.$on('$ionicView.enter', function(e) {
//});
// Form data for the login modal
$scope.loginData = {};
// Create the login modal that we will use later
$ionicModal.fromTemplateUrl('js/modules/login/login.html', {
scope: $scope
}).then(function(modal) {
$scope.modal = modal;
});
// Triggered in the login modal to close it
$scope.closeLogin = function() {
$scope.modal.hide();
};
// Open the login modal
$scope.login = function() {
$scope.modal.show();
};
$scope.show = function() {
$ionicLoading.show({
template:'<p>Loading...</p><ion-spinner></ion-spinner>'
});
};
$scope.hide = function(){
$ionicLoading.hide();
};
// Perform the login action when the user submits the login form
$scope.doLogin = function() {
console.log('Doing login', $scope.loginData);
// Start showing the progress
$scope.show($ionicLoading);
// Do the call to a service using $http or directly do the call here
LoginService.login($scope.loginData).success(function(data) {
// Do something on success for example if you are doing a login
console.log('Login successful', data);
}).error(function(data) {
// Do something on error
console.log('Login failed', data);
}).finally(function($ionicLoading) {
// On both cases hide the loading
console.log('Hide');
$scope.hide($ionicLoading);
});
};
}
module.exports = ['$scope', '$ionicModal', '$timeout','$location',
'$ionicLoading','LoginService','$ionicPopup',
LoginController];
This is my LoginService file, this is very weird to me because I've injected the appropriate file but yet I keep getting the error mentioned above. Any help or guidance would deeply be appreciated.
'use strict';
function LoginService($http, $q, API_ENDPOINT) {
var BASE_URL = API_ENDPOINT.url;
var LOCAL_TOKEN_KEY = 'yourTokenKey';
var isAuthenticated = false;
var authToken;
function storeUserCredentials(token) {
window.localStorage.setItem(LOCAL_TOKEN_KEY, token);
useCredentials(token);
}
function useCredentials(token) {
isAuthenticated = true;
authToken = token;
// Set the token as header for your requests!x-access-token'
$http.defaults.headers.common.Authorization = authToken;
}
var login = function(user) {
return $q(function(resolve, reject) {
$http.post(BASE_URL + '/authenticate', user).then(function(result){
if (result.data.success) {
storeUserCredentials(result.data.token);
resolve(result.data.msg);
}else{
reject(result.data.msg);
}
});
});
};
return {
login: login,
isAuthenticated: function() {return isAuthenticated;},
};
}
module.exports = ['$http', '$q', 'API_ENDPOINT', LoginService];
This is my login.js file in the same directory as the one posted above
'use strict';
module.exports = angular.module('login', [])
.factory('LoginService', require('./login-service'))
.controller('LoginController', require('./login-controller'));
You should first do require and then only define the service. Since you are directly passing require in the .factory('LoginServie, require()` the service name is getting registered but its body is empty.
I've not worked much with require but here is you can try:
require('./login-service');
module.exports = angular.module('login', [])
// Make this code synchornous that this should only run when above require loaded the script
.factory('LoginService', LoginService)
.controller('LoginController', require('./login-controller'));
Or (probably)
require('./login-service', function() {
module.exports = angular.module('login', [])
.factory('LoginService', LoginService)
.controller('LoginController', require('./login-controller'));
})
When using a factory you are getting the actual class, and you need to instantiate it. When using a service you get an instance of the service. Take a look at the following example: AngularJS : Factory and Service?
The module definition
var module = angular.module('test', []);
module.provider('client', function() {
this.$get = function($http) {
return {
foo: function() {
return $http.get('foo');
}
}
}
});
module.factory('service', ['client', function(client) {
return {
bar: function() {
return client.foo();
}
}
}]);
Basically, client is a wrapper for http calls, and service is a wrapper around the client basic features.
I'm unit testing both the provider and the service with karma+jasmine. The provider tests run as expected, but i have a problem with the service tests:
describe('service test', function(){
var service = null;
beforeEach(function(){
module('test')
inject(function(_service_, $httpBackend, $injector) {
service = _service_;
$httpBackend = $injector.get('$httpBackend');
});
});
it('should invoke client.foo via service.bar', function() {
$httpBackend.expect("GET", "foo");
service.bar();
expect($httpBackend.flush).not.toThrow();
});
});
I get Expected function not to throw, but it threw Error: No pending request to flush !.. When testing the provider with the same way, this test passes. Why?
When you are testing your service, you need to mock the client and inject that mock instead of the real client. Your mock can be in the same file if you only expect to use it for testing this service or in a separate file if you'll use it again elsewhere. Doing it this way does not require the use of $httpBackend (because you are not actually making an http call) but does require using a scope to resolve the promise.
The mock client:
angular.module('mocks.clientMock', [])
.factory('client', ['$q', function($q) {
var mock = {
foo: function() {
var defOjb = $q.defer();
defOjb.resolve({'your data':'1a'});
return defOjb.promise;
}
};
return mock;
}]);
Using the mock:
describe('service test', function(){
var service, mock, scope;
beforeEach(function(){
module('test', 'mocks.clientMock');
inject(function(_service_, $rootScope) {
service = _service_;
scope = $rootScope.$new();
});
});
it('should invoke client.foo via service.bar', function() {
spyOn(client, 'foo').and.callThrough();
service.bar();
scope.$digest();
expect(client.foo).toHaveBeenCalled();
});
});
I am using jasmine for testing my angular controllers.
I am catching errors and success in the .then(successCallback, errorCallback)
Although it is working fine on the bases of live functionality but am confused how to write a spy for returning an error as it is always caught in the successCallback()
Following is the controller :-
angular.module('myApp')
.controller('LoginCtrl', function ($scope, $location, loginService, SessionService) {
$scope.errorMessage = '';
$scope.login = function () {
var credentials = {
email: this.email,
password: this.password
};
SessionService.resetSession();
var request = loginService.login(credentials);
request.then(function(promise){ //successfull callback
if (promise['status'] === 200){
//console.log('login');
$location.path('/afterloginpath');
}
},
function(errors){ //fail call back
// console.log(errors);
$location.path('/login');
});
};
});
My test case :-
'use strict';
describe('Controller: LoginCtrl', function () {
// load the controller's module
beforeEach(module('myApp'));
var LoginCtrl, scope, location, login, loginReturn, session;
var credentials = {'email': 'email#email.com', 'password': 'admin123'};
// Initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope, $location, _loginService_, _SessionService_) {
scope = $rootScope.$new();
LoginCtrl = $controller('LoginCtrl', {
$scope: scope
});
location = $location;
login = _loginService_;
session = _SessionService_;
scope.errorMessage = '';
spyOn(login, "login").andCallFake(
function(){
return {
then: function(response){
response(loginReturn);
}
}
}
);
spyOn(session, "setName").andCallFake(function(){
return true;
});
}));
it('should go to login when login fail', function () {
loginReturn = function(){
return{
successfullyCallback: {throwError:true},
failCallback: {status: 400, 'data' : {'errors' : [{"type":"invalid_data","target":"email,password"}]}}
}
};
var wrong_creds = {email: 'wrong#email.com', password: 'wrong_password'};
scope.email = wrong_creds.email;
scope.password = wrong_creds.password;
scope.login();
expect(location.path()).toBe("/login");
expect(scope.errorMessage).toBe('username or password combination is invalid');
});
});
I find it easier to use actual promises for mocked services as it removes a lot of nested functions and is a lot easier to read.
Relevant snippets ($q needs to be injected in beforeEach):
deferred = $q.defer();
spyOn(login, 'login').andReturn(deferred.promise);
...
deferred.reject({ ... });
After resolving or rejecting the promise you need to call scope.$digest() so angular can process it.
I've looked up and down and tried all kinds of things to make the E2E passThrough() actually work. Only examples are for templates. I don't think it actually can work but if someone has ever seen a working example I would love to see it. Basically I want real XHR calls in my Karma tests instead of mocks.
This is where I am at - i've tried a bunch of variations - right now I get an Unknown provider: cartServiceProvider error - sometimes I get that injected but then it's something else.
myAppDev = angular.module('myApp', ['ngMockE2E']);
myAppDev.run(function($httpBackend) {
$httpBackend.whenPOST(/^api\/cart\/save/).passThrough();
});
beforeEach(angular.module('myApp'));
beforeEach(inject(function(_$httpBackend_, _$rootScope_, _$http_e) {
$scope = _$rootScope_;
$http = _$http_;
$httpBackend = _$httpBackend_;
}));
describe('#createOrder()', function() {
it('should return an order ID', function(done) {
inject(function(cartService) {
$scope.$apply(function() {
cartService.createOrder(function(res) {
done();
});
});
})
});
});
Sample service:
angular.module('myApp').factory('cartService', function($http) {
Cart.prototype.createOrder = function(callback) {
var self = this;
var data = {}
$http.post('/api/cart/save', data ).success(function(res) {
self.order = res.id;
callback(res);
});
};