I am having a lot of trouble to write the unit test case for a provider that contains some injections.
The particular provider is:
(function () {
angular
.module('core.router', [])
.provider('routerHelper', routerHelperProvider);
routerHelperProvider.$inject = ['$stateProvider', '$urlRouterProvider'];
/**
* This method Initializes the Router Helper provider to be used by all modules
*
* #param $stateProvider
* #param $urlRouterProvider
*/
function routerHelperProvider($stateProvider, $urlRouterProvider) {
this.$get = routerHelperService;
routerHelperService.$inject = ['$state'];
/**
* This method sets the methods to be used by the helper
*
* #param $state
* #returns {Object}
*/
function routerHelperService($state) {
var hasOtherwise = false;
return {
configureStates: configureStates,
getStates: getStates
};
/**
* This method configures all the states to be used by the application
*
* #param {!String[]} states
* #param {!String} otherwisePath
*/
function configureStates(states, otherwisePath) {
states.forEach(function (state) {
//console.log("adding state", state.state, "with config", state.config);
$stateProvider.state(state.state, state.config);
});
if (otherwisePath && !hasOtherwise) {
hasOtherwise = true;
$urlRouterProvider.otherwise(otherwisePath);
}
}
/**
* This method returns the states to be used by the application
*
* #return {Object}
*/
function getStates() {
return $state.get();
}
}
} })();
The basic unit test is:
'use strict';
describe('core.router test', function () {
// All Service injections
var $urlRouterProvider, $stateProvider;
// Mocks
var m_url = function () {
};
var m_state = function () {
};
// Others
var routerHelper, urlRouter, state, base;
// Before statements
beforeEach(module('core.router', function ($provide, _routerHelperProvider_) {
$provide.value('$urlRouterProvider', m_url);
$provide.value('$stateProvider', m_state);
base = _routerHelperProvider_;
}));
// Starting the Factory
beforeEach(inject(function (_routerHelper_, _$urlRouter_, _$state_) {
routerHelper = _routerHelper_;
urlRouter = _$urlRouter_;
state = _$state_;
}));
describe('when testing it', function () {
it('should return true', function () {
//var abc = routerHelper.getStates();
expect(1).toEqual(1);
});
});
});
I keep getting errors like:
Error: [$injector:unpr] Unknown Provider: $stateProvider
Error: [$injector:unpr] Unknown Provider: $urlRouterProvider
Error: [$injector:unpr] Unknown Provider: routerHelperProvider
I tried several different module instantiations and several different injections, but I can't seem to make it work. When I take out the injections ($stateProvider, $urlRouterProvider and $state), the unit test is straightforward.
So, this would be the solution, bringing some complexity because the provider is using both the $state and $stateProvider:
'use strict';
describe('core.router test', function () {
// All Provider injections
var $urlRouterProvider, $stateProvider;
// Mocks
var m_urlProvider = mockDataCore.urlRouterProvider();
var m_stateProvider = mockDataCore.stateProvider();
var m_state = mockDataCore.state();
// Others
var routerHelper, base;
// Define the mock providers
beforeEach(function(){
module(function($provide){
$provide.provider('$urlRouter', m_urlProvider);
$provide.provider('$state', m_stateProvider);
});
});
// Start the module with the internal mock
beforeEach(function () {
module('core.router', function ($provide) {
$provide.value('$state', m_state);
});
});
// Load the provider with module to be able to call its configuration methods
beforeEach(function () {
module(['routerHelperProvider', function (_$urlRouterProvider_, _$stateProvider_, _routerHelperProvider_) {
$urlRouterProvider = _$urlRouterProvider_;
$stateProvider = _$stateProvider_;
base = _routerHelperProvider_;
}]);
});
// Inject and start the provider
beforeEach(function () {
inject(['routerHelper', function (_routerHelper_, $state) {
routerHelper = _routerHelper_;
}]);
});
// test cases
describe('when adding one state and no "otherwise"', function () {
it('otherwise should not be called and state should be saved to state list', function () {
spyOn(m_urlProvider, "otherwise");
spyOn(m_stateProvider, "state");
var simpleState = [{
state : "home",
config : {
url: "/home"
}}];
routerHelper.configureStates(simpleState);
expect(m_urlProvider.otherwise).not.toHaveBeenCalled();
expect(m_stateProvider.state).toHaveBeenCalledWith("home", {url: "/home"});
});
});
describe('when getting the states', function () {
it('should return the states', function () {
spyOn(m_state, "get");
var states = routerHelper.getStates();
expect(m_state.get).toHaveBeenCalled();
});
});
});
The mock methods are:
var mockDataCore = (function () {
return {
urlRouterProvider: urlRouterProvider,
stateProvider: stateProvider,
state: state
};
function urlRouterProvider() {
return {
otherwise: function () { /* void */
},
$get: function () { /* void */
}
};
}
function stateProvider() {
return {
state: function () { /* void */
},
$get: function () { /* void */
}
};
}
function state() {
return {
get: function () {
return {};
},
go: function () { /* void */
}
};
}})();
Of course it doesn't cover all the tests for this provider, but the rest of them are pretty straight forward..
Related
I am trying to test a function which is a private function, and it is been called in some other function in my controller. When I try to test this validateParameterGroup function, it gives an error saying that validateParameterGroup is not defined.
controller
angular.module('PpmApp')
.controller('parameterGroupListController', ['$scope', '$injector', 'parameterGroups', parameterGroupListController]);
function parameterGroupListController($scope, $injector, parameterGroups) {
$scope.createParameterGroup = function (parameterGroup) {
var validationErrors = validateParameterGroup(parameterGroup);
}
function validateParameterGroup(parameterGroup) {
// ...
}
};
Test Case
describe('validateParameterGroup', function () {
beforeEach(function () {
var parameterGroup = {};
});
it('should validate a parameter group', function () {
expect(validateParameterGroup(parameterGroup)).toEqual(false);
});
});
============ Edit ==================
If it is not possible to test a private function, Can I test $scope.createParameterGroup? I tried doing this but I am getting following error.
TypeError: $scope.createParameterGroup(...) is not a function
Test
describe('createParameterGroup', function() {
var validationErrors, parameterGroup;
beforeEach(function() {
validationErrors = {};
validationErrors.isError;
parameterGroup = {
GroupName: "ABC",
Description: "ABC",
fromMonth: 1,
fromYear: 18,
toMonth: 12,
toYear: 18
}
});
it('should create a parameter group', function() {
expect($scope.createParameterGroup(parameterGroup)(validationErrors.isError)).toEqual(false);
});
});
Yes, validateParameterGroup becomes fully private and not accessible from outside. You can extend $scope object to include this function to become public, similar to createParameterGroup
angular.module('PpmApp')
.controller('parameterGroupListController', ['$scope', '$injector', 'parameterGroups', parameterGroupListController]);
function parameterGroupListController($scope, $injector, parameterGroups) {
$scope.createParameterGroup = function (parameterGroup) {
var validationErrors = validateParameterGroup(parameterGroup);
}
$scope.validateParameterGroup = function(parameterGroup) {
// ...
}
};
I'm trying to unit test a controller with a service injected into it. No matter what I seem to try, I get an error. Any assistance to help me get this going would be much appreciated. I'm using Angular/Karma/Jasmine to get run my tests.
There seem to be a lot of posts with similar stories but this feels like it may not be a duplicate - apologies if it is.
My controller looks like this:
(function() {
angular
.module('blah')
.controller('AdminController', AdminController);
/* #ngInject */
function AdminController($scope, toastr, adminService) {
activate();
/**
* Controller initialisation.
*/
function activate() {
getAllUsers();
}
/**
* Gets all users.
*/
function getAllUsers() {
adminService.getAllUsers()
.then(function(response) {
$scope.users = response.data;
})
.catch(function(error) {
toastr.error('Unable to load users', 'Error');
console.log(error);
});
}
}
})();
And my service looks like this:
(function() {
angular
.module('blah')
.factory('adminService', adminService);
/* #ngInject */
function adminService($http, environmentConfig) {
var service = {
getAllUsers: getAllUsers
};
return service;
/**
* Gets all user objects.
*/
function getAllUsers() {
return $http.get(environmentConfig.apiBaseUrl + '/user');
}
}
})();
and my unit tests look like this:
describe('AdminController', function() {
var ctrl,
adminService,
$scope;
var listOfTestUsers = [
{ name: 'Dave', id: 1 },
{ name: 'Bob', id: 2 },
{ name: 'Bill', id:3 }
];
beforeEach(function() {
module('blah');
});
beforeEach(inject(function($rootScope, $controller) {
adminService = {
getAllUsers: function() {}
};
spyOn(adminService, 'getAllUsers').and.returnValue(listOfTestUsers);
$scope = $rootScope.$new();
ctrl = $controller('AdminController', {
$scope: $scope,
adminService: adminService
});
}));
describe('The getAllUsers function should exist', function() {
it('should work', function() {
expect(ctrl).toBeDefined();
});
});
});
I get this error when running my Jasmine tests with Karma:
TypeError: adminService.getAllUsers(...).then is not a function
Here are a few things that I found wrong with the code.
.catch was used earlier. .then is called with two callbacks, a success callback and an error callback. So that's what I've done in your call to adminService.getAllUsers.
For the TypeError: adminService.getAllUsers(...).then is not a function that you were getting. You didn't mock getAllUsers properly. I've done that in the testCases file. It returns a function named then which was not available earlier.
Controller
(function() {
angular
.module('blah', [])
.controller('AdminController', AdminController);
/* #ngInject */
function AdminController($scope, toastr, adminService) {
$scope.greeting = "Hello World!";
/**
* Gets all users.
*/
$scope.getAllUsers = function() {
adminService.getAllUsers()
.then(function(response) {
$scope.users = response.data;
}, function(error) {
toastr.error('Unable to load users', 'Error');
console.log(error);
});
}
activate();
/**
* Controller initialisation.
*/
function activate() {
$scope.getAllUsers();
}
}
})();
environmentConfig Constant. Replace this with yours.
(function() {
angular.module('blah').constant('environmentConfig', {
apiBaseUrl: 'https://www.something.com'
})
})();
toastr Service. Replace this with yours
(function() {
angular
.module('blah')
.factory('toastr', toastr);
/* #ngInject */
function toastr() {
var service = {
error: error
};
return service;
/**
* Gets all user objects.
*/
function error(a, b) {
console.log("Here's the error : ", a);
}
}
})();
adminService
(function() {
angular
.module('blah')
.factory('adminService', adminService);
/* #ngInject */
function adminService($http, environmentConfig) {
/**
* Gets all user objects.
*/
function getAllUsers() {
return $http.get(environmentConfig.apiBaseUrl + '/user');
}
var service = {
getAllUsers: getAllUsers
};
return service;
}
})();
Test Cases
describe('controller: AdminController', function() {
var scope, $scope, toastr, adminService, AdminController, flag, $q;
flag = 'success';
var listOfTestUsers = [{
name: 'Dave',
id: 1
}, {
name: 'Bob',
id: 2
}, {
name: 'Bill',
id: 3
}];
beforeEach(module('blah'));
beforeEach(inject(function($controller, $rootScope, _toastr_, _adminService_, _$q_) {
scope = $rootScope.$new();
toastr = _toastr_;
adminService = _adminService_;
$q = _$q_;
spyOn(adminService, 'getAllUsers').and.callFake(function() {
return flag === 'success' ? $q.when(listOfTestUsers) : $q.reject("Error");
});
AdminController = $controller('AdminController', {
$scope: scope,
toastr: _toastr_,
adminService: _adminService_
});
}));
describe('The getAllUsers function should exist', function() {
it('should work', function() {
expect(AdminController).toBeDefined();
});
});
});
Hope this helps.
Your controller code is causing the error. You should be callling $scope.getAllUsers(); in your activate function, not "getAllUsers()".
Your actual error is the fact that your mock service does not have the function 'getAllUsers' here is a paste bin with the adjustments http://pastebin.com/LwG0CzUW
If you prefer you could adjust your test to call the actual service as the following pastebin.
http://pastebin.com/RSP4RfF9
I want test my angular app with mocha,sinon and chai.
Especially I interest in submit function. How to create mock or stub for LoginResoure to test this function.
Thanks!
(function () {
'use strict';
class LoginController {
constructor($state,LoginResource) {
this.resource = LoginResource;
this.$state = $state;
this.credentials = {};
}
submit() {
let promise = this.resource.login(this.credentials);
promise.then(()=>{
changeState()
}
}
changeState() {
this.$state.go('home');
}
}
angular.module('app.login').controller('LoginController', LoginController);
})();
(function () {
'use strict';
class LoginResource {
constructor($resource, API_LOGIN) {
this.$resource = $resource(API_LOGIN,{'#id':id})
}
login(data) {
return this.$resource.save(data).$promise;
}
}
angular.module('app.login').service('LoginResource', LoginResource);
})();
EDIT:
Previously I do it with jasmine in next way:
let deferred = $q.defer();
deferred.resolve('Remote call result');
mockPeopleResource = {
createPerson: jasmine.createSpy('createPerson').and.returnValue(deferred.promise)
};
or if I want mock #resource
mockThen = jasmine.createSpy();
mockGetPeoplePromise = {then: mockThen};
mockUpdate = jasmine.createSpy().and.returnValue({$promise: mockPromise});
mockSave = jasmine.createSpy().and.returnValue({$promise: mockPromise});
mockGetPeopleQuery = jasmine.createSpy().and.returnValue({$promise: mockGetPeoplePromise});
mockResource = jasmine.createSpy().and.returnValue({
get: mockGet,
update: mockUpdate,
save: mockSave,
query: mockGetPeopleQuery
});
If you want to mock a service, you can create a test module when you set the mocked value:
beforeEach(function() {
angular.module('test', []).factory('LoginResource', function($q) {
return {
/* You can mock an easy login function that succeed when
data >= 0 and fails when data < 0 */
login: function(data) {
return $q(function(resolve, reject) {
if (data >= 0) return resolve();
reject();
});
}
};
});
module('app.login', 'test');
});
I want to develop a generic translator component with configurable url and paramsFn. Here paramsFn can either be a plain function or a function with service dependencies. paramsFn is expected to return a promise.
(function () {
"use strict";
angular.module("translator-app", [])
.provider(translatorProvider);
function translatorProvider() {
var
url,
paramsFn;
//Provider Config Functions
function setUrl (pUrl) {
url = pUrl
};
function setParamsFn (pParamsFn) {
paramsFn = pParamsFn;
};
function factory ($http, $q) {
//Service Function Pseudo
function translate(key) {
if (translateions are cached) {
//return promis of cached translations
} else {
/*
make http call with configured url and
paramsFnto fetch translations.
Cache translations.
Return promise with translations.
*/
}
} //translate
//Service Object
return {
translate: translate
};
} // factory
factory .$inject = [
"$http"
"$q"
];
//Exposed functionality
this.setUrl = setUrl;
this.setParamsFn = setParamsFn;
this.$get = factory;
}
}();
An application can use translator after configuring it. User app provide will be able to provide paramFn with service dependencies. paramFn will be invoked later when translator.translate(...) method is called.
(function () {
"use strict";
angular.module('the-app', ["translator-app"])
.config(translatorConfigurator)
.controller(AppController)
function translatorConfigurator (translatorProvider) {
function theParamsFn (someService) {
//use someService to generate and return params object
}
theParamsFn.$inject = [
"someService"
];
translatorProvider.setUrl("/url/to/translator");
translatorProvider.setParamsFn(theParamsFn);
}
function AppController (translator) {
translator.translate("the-key").then(function (translated) {
//do somethid with 'translated'.
});
}
translatorConfigurator.$injec = [
"translatorProvider"
];
AppController.$inject = [
"translator"
];
}());
How can I achieve this?
Short Story:
According to Angular $injector documentation
// inferred (only works if code not minified/obfuscated)
$injector.invoke(function(serviceA){});
// annotated
function explicit(serviceA) {};
explicit.$inject = ['serviceA'];
$injector.invoke(explicit);
// inline
$injector.invoke(['serviceA', function(serviceA){}]);
Novel
Once upon a time there was a poor translatorProvider. Angular, a great super hero, helped translatorProvider to be feature rich by its $injector weapon. translatorProvider built its getParameters function inside factory function and used it in translate.
(function () {
"use strict";
angular.module("translator-app", [])
.provider(translatorProvider);
function translatorProvider() {
var
url,
paramsFn;
//Provider Config Functions
function setUrl (pUrl) {
url = pUrl
};
function setParamsFn (pParamsFn) {
paramsFn = pParamsFn;
};
function factory ($injector, $http, $q) {
function getParameters() {
var
promise,
fn;
if (paramsFn) {
fn = $injector.invoke(paramsFn);
promise = $q.resolve(fn());
} else {
promise = $q.resolve()
}
return promise;
}
//Service Function Pseudo
function translate(key) {
if (translateions are cached) {
//return promis of cached translations
} else {
getParameters()
.then(function (params) {
return $http({
url: url,
params: params
});
})
.then(function (response) {
var extracted = ...; //extract field from response.data
//put extracted into cache
return $q.resolve(extractedField)
});
}
} //translate
//Service Object
return {
translate: translate
};
} // factory
factory .$inject = [
"$injector",
"$http"
"$q"
];
//Exposed functionality
this.setUrl = setUrl;
this.setParamsFn = setParamsFn;
this.$get = factory;
}
}();
Now translator can be configured as below.
(function () {
"use strict";
angular.module('the-app', ["translator-app"])
.config(translatorConfigurator)
.controller(AppController)
function translatorConfigurator (translatorProvider) {
function theParamsFn (someService) {
return function () {
//returns some parameters object
}
}
theParamsFn.$inject = [
"someService"
];
translatorProvider.setUrl("/url/to/translator");
translatorProvider.setParamsFn(theParamsFn);
}
function AppController (translator) {
translator.translate("the-key").then(function (translated) {
//do somethid with 'translated'.
});
}
translatorConfigurator.$inject = [
"translatorProvider"
];
AppController.$inject = [
"translator"
];
}());
After these changes translatorprovider becomes more powerful and help many other modules and they lived happily ever after.
I am getting this error when I run my karma unit script and I haven't been able to figure out why
Error: [$injector:unpr] Unknown provider: FBURLProvider <- FBURL
Here is my directive code
'use strict';
angular.module('userMenu', ['firebase'])
.directive('userMenu', function (FBURL, angularFire) {
return {
restrict: 'A',
scope: true ,
link: function postLink(scope, element, attrs) {
/**
* Returns the logged in user information
* #param {string} FBURL
* #param {object} scope
* #returns {promise}
*/
scope.getUserDataFromFirebase = function(FBURL, scope) {
var ref = new Firebase(FBURL + '/users/' + scope.auth.id);
return angularFire(ref, scope, 'user', {})
}
}
};
});
Here is my spec code
'use strict';
describe('Directive: userMenu', function () {
// load the directive's module
beforeEach(module('userMenu', 'firebase'));
var element,
elementScope,
scope;
beforeEach(inject(function ($rootScope, $compile, _FBURL_, _angularFire_) {
scope = $rootScope.$new();
element = angular.element('<div user-menu></div>');
element = $compile(element)(scope);
elementScope = element.scope();
}));
it('should get user data', inject(function ($compile) {
console.log(scope);
}));
});
To be honest I'm not that familiar with unit testing so I'm probably missing something really obvious but any help would be appreciated.
If everything is working in your app, but you're getting an error in your tests, then you need to add firebase to the Karma's files. Find your karma.conf.js (in yeoman generated ionic-angular add this to the Karma suite of Gruntfile.js), and have it resemble the following:
karma: {
options: {
...
files: [
...
'https://cdn.firebase.com/v0/firebase.js',
'app/bower_components/angular-fire/angularFire.js',
...
],
...
}
...
}
Then in your spec, include firebase:
beforeEach(module('Simplift', 'firebase'));
And every time you need to use the firebase service:
describe/it('some test desc ...', inject(function (..., $firebase) {
// now we can use $firebase!!
fireSync = $firebase(new Firebase('https://app_name.firebaseio.com'));
...
}));
Took me forever to figure this out, and hoping it will alleviate stress for someone. This works for me for now, but probably not the cleanest way to do it (please contribute suggestions!), since you're not actually stubbing out the firebase data, but you could add a 'test' url to your firebase DB.
The Firbase team had pointed me in the direction of some testing code in the Fireseed project that has subsequently been removed. Here is my unit test that includes the stubs that were in the Fireseed project
'use strict';
describe('Directive: userMenu', function () {
// load the directive's module
beforeEach(module('userMenu', 'firebase', function($provide) {
$provide.value('Firebase', firebaseStub());
$provide.value('FBURL', 'FAKE_FB_URL');
$provide.value('angularFireAuth', angularAuthStub());
}));
/**
* This is from https://github.com/firebase/angularFire-seed/blob/master/test/unit/servicesSpec.js
*/
function stub() {
var out = {};
angular.forEach(arguments, function(m) {
out[m] = jasmine.createSpy();
});
return out;
}
/**
* This is from https://github.com/firebase/angularFire-seed/blob/master/test/unit/servicesSpec.js
*/
function stub() {
var out = {};
angular.forEach(arguments, function(m) {
out[m] = jasmine.createSpy();
});
return out;
}
/**
* This is from https://github.com/firebase/angularFire-seed/blob/master/test/unit/servicesSpec.js
*/
function reject($q, error) {
var def = $q.defer();
def.reject(error);
return def.promise;
}
/**
* This is from https://github.com/firebase/angularFire-seed/blob/master/test/unit/servicesSpec.js
*/
function resolve($q, val) {
var def = $q.defer();
def.resolve(val);
return def.promise;
}
/**
* This is from https://github.com/firebase/angularFire-seed/blob/master/test/unit/servicesSpec.js
*/
function firebaseStub() {
// firebase is invoked using new Firebase, but we need a static ref
// to the functions before it is instantiated, so we cheat here by
// attaching the functions as Firebase.fns, and ignore new (we don't use `this` or `prototype`)
var fns = stub('set');
customSpy(fns, 'child', function() { return fns; });
var Firebase = function() {
angular.extend(this, fns);
return fns;
};
Firebase.fns = fns;
return Firebase;
}
/**
* This is from https://github.com/firebase/angularFire-seed/blob/master/test/unit/servicesSpec.js
*/
function angularAuthStub() {
var auth = stub('login', 'logout', 'createAccount', 'changePassword');
auth._authClient = stub('changePassword', 'createUser');
return auth;
}
/**
* This is from https://github.com/firebase/angularFire-seed/blob/master/test/unit/servicesSpec.js
*/
function customSpy(obj, m, fn) {
obj[m] = fn;
spyOn(obj, m).andCallThrough();
}
var element,
elementScope,
scope;
beforeEach(inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
element = angular.element('<div user-menu></div>');
element = $compile(element)(scope);
elementScope = element.scope();
}));
it('should default to a login message', inject(function ($compile) {
scope.$digest();
var text = element.text();
expect(text).toBe('Please login');
}));
it('default message should contain a link to login page', inject(function ($compile) {
scope.$digest();
var href = element.find('a').attr('href');
expect(href).toBe('#/login');
}));
});