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.
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');
I am using Ionic framework for custom applications. In the process, I am trying to write Unit test for the factory datastoreServices which has a dependency on DomainService and $http. I am kind a confused on the implementation of Jasmine Unit tests.
My factories are as follows.
app.factory("datastoreServices", ["$http", function($http) {
return {
getData: function(data, DomainService) {
return $http.post(DomainService.host + 'factor', data);
}
};
}]);
app.factory('DomainService', function() { //here
if (ionic.Platform.isAndroid()) {
return {
host: 'http://10.0.2.2:7001/'
}
}
return {
host: 'http://localhost:7001/'
}
})
And my unit test skeleton is as follows. It has two dependencies so, couldn't figure out how to proceed. This is what I got so far for in unit test file.
describe(
'datastoreServices',
function() {
beforeEach(module('Myapp'));
describe('getData'),
function() {
it("Should return correct values", inject(function(datastoreServices, DomainService, $httpBackend) {
expect(datastoreServices.getData(httpBackend.. /***something here!**/ )
.toEqual("2.2");
}))
}
I have very little knowledge on mocking and stuffs. Can someone help me test that factory datastoreServices. The following things are to be tested:
Is Http post making correct calls?
Is the function returning correct promise?
Here is the similar scenario of app in plnkr.
Idk, if I am asking too much. Thanks in advance.
The key principles are:
$http is mocked during testing, meaning that your server is not being actually called during your test execution
you must use $httpBackend in order to assert http calls and mock server response https://docs.angularjs.org/api/ngMock/service/$httpBackend
you can easily inject or mock any dependencies needed for your test
Here's an example based on your OP code:
describe('datastoreServices', function() {
beforeEach(module('MyApp'));
// get a reference to the $httpBackend mock and to the service to test, and create a mock for DomainService
var $httpBackend, datastoreServices, DomainService;
beforeEach(inject(function(_$httpBackend_, _datastoreServices_) {
$httpBackend = _$httpBackend_;
datastoreServices = _datastoreServices_;
DomainService = function() {
return {
host: 'http://localhost:7001/'
};
};
}));
// after each test, this ensure that every expected http calls have been realized and only them
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('calls http backend to get data', function() {
var data = {foo: 'bar'};
// write $http expectation and specify a mocked server response for the request
// see https://docs.angularjs.org/api/ngMock/service/$httpBackend
$httpBackend.expectPOST('http://localhost:7001/factor', data).respond(201, {bar: 'foo'});
var returnedData;
datastoreServices.getData(data, DomainService).success(function(result) {
// check that returned result contains
returnedData = result;
expect(returnedData).toEqual({bar: 'foo'});
});
// simulate server response
$httpBackend.flush();
// check that success handler has been called
expect(returnedData).toBeDefined();
});
});
I'm unit testing a provider in Jasmine, which relies on another provider. There's no configuration associated with this provider. When mocking a provider, I've read you're supposed to use something like
beforeEach(module(function ($provide) {
mockInjectedProvider = { };
$provide.value('injected', mockInjectedProvider );
}));
which works fine for me when injecting a custom provider into a service. When injecting them into a provider it doesn't work though. The code doesn't fail, but what gets executed when testing is the actual provider, not the mocked one. Abstracted example below.
var mockInjectedProvider;
beforeEach(function () {
module('myModule');
});
beforeEach(module(function ($provide) {
mockInjectedProvider = {
myFunc: function() {
return "testvalue"
}
}
};
$provide.value('injected', mockInjectedProvider );
}));
beforeEach(inject(function (_base_) {
baseProvider = _base_;
}));
it("injectedProvider should be mocked", function () {
var resultFromMockedProvider = baseProvider.executeMyFuncFromInjected();
expect(resultFromMockedProvider).toEqual("testvalue");
}); // Here instead of using my mock it executes the actual dependency
In the $provide.value statement I've tried including both injected and injectedProvider, as well as using $provide.provider and mocking a $get function on it but nothing seems to work. I just can't get it to mock away the actual provider. Abstracted base provider looks like this.
(function (ng, module) {
module.provider("base",
["injectedProvider", function (injectedProvider) {
this.executeMyFuncFromInjected= function() {
return injectedProvider.myFunc(); // let's say this returns "realvalue"
}
this.$get = function () {
return this;
};
}]
);
})(window.angular, window.angular.module("myModule"));
Everything in my code is working except the Jasmine mocking.
In this case is better to just mock the return value instead of the provider.
var mockInjectedProvider;
beforeEach(function () {
module('myModule');
});
beforeEach(inject(function (_injected_) {
spyOn(_injected_, "myFunc").and.returnValue("testvalue");
}));
beforeEach(inject(function (_base_) {
baseProvider = _base_;
}));
it("injectedProvider should be mocked", function () {
var resultFromMockedProvider = baseProvider.executeMyFuncFromInjected();
expect(resultFromMockedProvider).toEqual("testvalue");
}); // Here instead of using my mock it executes the actual dependency
I'm using Jasmine to test my angular application and want to spy on an anonymous function.
Using angular-notify service https://github.com/cgross/angular-notify, I want to know whether notify function have been called or not.
Here is my controller:
angular.module('module').controller('MyCtrl', function($scope, MyService, notify) {
$scope.isValid = function(obj) {
if (!MyService.isNameValid(obj.name)) {
notify({ message:'Name not valid', classes: ['alert'] });
return false;
}
}
});
And here is my test:
'use strict';
describe('Test MyCtrl', function () {
var scope, $location, createController, controller, notify;
beforeEach(module('module'));
beforeEach(inject(function ($rootScope, $controller, _$location_, _notify_) {
$location = _$location_;
scope = $rootScope.$new();
notify = _notify_;
notify = jasmine.createSpy('spy').andReturn('test');
createController = function() {
return $controller('MyCtrl', {
'$scope': scope
});
};
}));
it('should call notify', function() {
spyOn(notify);
controller = createController();
scope.isValid('name');
expect(notify).toHaveBeenCalled();
});
});
An obviously return :
Error: No method name supplied on 'spyOn(notify)'
Because it should be something like spyOn(notify, 'method'), but as it's an anonymous function, it doesn't have any method.
Thanks for your help.
Daniel Smink's answer is correct, but note that the syntax has changed for Jasmine 2.0.
notify = jasmine.createSpy().and.callFake(function() {
return false;
});
I also found it useful to just directly return a response if you only need a simple implementation
notify = jasmine.createSpy().and.returnValue(false);
You could chain your spy with andCallFake see:
http://jasmine.github.io/1.3/introduction.html#section-Spies:_andCallFake
//create a spy and define it to change notify
notify = jasmine.createSpy().andCallFake(function() {
return false;
});
it('should be a function', function() {
expect(typeof notify).toBe('function');
});
controller = createController();
scope.isValid('name');
expect(notify).toHaveBeenCalled();