I'm starting to do unit testing for my Angular app, and I had some questions on how to actually structure the tests folder. I basically used the yeoman angular generator, so it comes with Jasmine and Karma pre-configured.
Here's a scenario of what I'm trying to test...
I have a "PageHeaderDirective" which displays a user's name and email (like a welcome message) as well as a logout link. The code for the page header directive is inconsequential, but I do need to hit the "/user" endpoint from the backend to get the user's details. Here is the code for UserService which is injected into PageHeaderDirective:
/**
* #ngdoc function
* #name Common.service.UserService
* #description
* Service to retrieve a {#link User} from the backend.
*/
(function () {
'use strict';
angular.module('Common').service('UserService', UserService);
UserService.$inject = ['User', 'Restangular'];
/**
* User service function.
* #param User The {#link User} provider.
* #param Restangular The restangular provider.
*/
function UserService(User, Restangular) {
var userPromise;
return {
getUser: getUser
};
/**
* Retrieves a {#link User} instance from the /user endpoint.
* #returns A promise to be resolved with a {#link User} instance.
*/
function getUser() {
if(!userPromise) {
userPromise = Restangular.one('user').get().then(function(data) {
return User.factory(data);
});
}
return userPromise;
}
}
})();
Here is a really simple test for PageHeaderDirective:
describe('Pageheader Tests', function() {
'use strict';
var scope;
var element;
beforeEach(module('templates'));
beforeEach(module('Common'));
beforeEach(inject(function(_$rootScope_, $compile) {
scope = _$rootScope_.$new();
scope.message = 'Test message';
element = '<ft-page-header message="message" page="home"></ft-page-header>';
element = $compile(element)(scope);
scope.$digest();
}));
it('should render a page header with the logo and username', function() {
expect(element.find('.logo-text').length).toBe(1);
var isolateScope = element.isolateScope();
expect(isolateScope.name).toBe('test');
});
});
Now, as you can probably tell, I'm getting an unknown provider error "Unknown provider: RestangularProvider <- Restangular <- UserService <- pageHeaderDirective" because I haven't injected it into the tests.
I've read that you can do something like beforeEach(function(){ module(function($provide) { $provide.service('UserService', function() { ... }})}); in each test file, but I don't really want to do that any time a directive/controller uses the UserService. How do I break that portion out of each test file and put it into its own "UserService.mock.js" file? If it's possible, how would I inject the "UserService.mock.js" into my tests?
Secondly, I'm also injecting Restangular into PageHeaderDirective to logout the user (Restangular.one('logout').get().then...). How do I mock this (I don't ever want to call the API endpoints)?
Lastly, if there are other providers that I am injecting ($document, $localStorage, $window), do I need to inject all of these into the tests as well? If so, how?
Thanks!
In case anyone wants to do what I have done (separate your mocks into different files so you don't need to copy-paste things a lot), here is what I have found out.
// /test/mock/UserService.mock.js
(function() {
"use strict";
angular.module('mocks.Common').service('UserService', mock);
mock.$inject = ['$q', 'User'];
function mock($q, User) {
return {
getUser : getUser
};
function getUser() {
return $q.when(User.factory({
firstName: 'test',
email: 'test#gmail.com',
id: 1
}));
}
}
})();
So first, you need to make sure your module (in this case I made "mocks.Common") is created. In a separate file I put this line: angular.module('mocks.Common', []); This creates my "mocks.Common" module. Then, I created a mock called "UserService" and used $q to return a promise with some dummy data. The User.factory portion is just a factory function within my real App from the Common module.
Once you have the above mocked "UserService", make sure to inject the modules in the correct order during your test's setup. Like so:
module('app');
module('templates');
module('mocks.Common');
Now, when my test runs, PageHeaderDirective will use the mocked "UserService" instead of the real one!
As for my second question: I haven't actually done it yet, but I believe I'll be able to use $httpBackend to test any Restangular functionality.
Thirdly, I figured out that if you just run module('appName') in all of your tests, you should automatically get any required dependencies. For example, here is my module definition for my entire app:
angular.module('app', [
'Common',
'ngAnimate',
'ngCookies',
'ngResource',
'ngRoute',
'ngSanitize',
'ngTouch',
'ngDialog',
'ngStorage',
'lodash',
'smart-table',
'rhombus',
'helpers',
'restangular',
'moment',
'cgBusy',
'duScroll'
])
So when I do module('app') I get all of these dependencies automatically in my tests (note the "Common" dependency in my app config).
Related
I have problem to inject $rootScope in config angularJS, this is my code, but still error, maybe anyone help me how to inject $rootScope in config angularJS. .
thanks.
(function() {
'use strict';
angular
.module('uliappApp')
.directive('angular-loading-bar', ['cfpLoadingBarProvider'])
.config(cfpLoadingBarProvider);
cfpLoadingBarProvider.$inject = ['cfpLoadingBarProvider', '$rootScope'];
function cfpLoadingBarProvider(cfpLoadingBarProvider, $rootScope) {
cfpLoadingBarProvider.includeBackdrop = true;
console.log(rootScope.concessionLoadingScreen);
cfpLoadingBarProvider.spinnerTemplate = '<div class="loading-bar-container">'
+ '<div id="loading-bar-spinner"><div class="spinner-icon"></div></div></div>';
}
})();
You don't need rootScope in configuration phase, it can be simply achieved by using .run().
angular
.module('uliappApp')
.run(['$rootScope', function($rootScope){
$rootScope.concessionLoadingScreen = true;
}])
During the config phase, only providers can be injected.
Basically angularjs first invoke the config method and then invoke the run method. During config only providers are available. A provider can then be used to create service instance. So, you can use .run to inject $rootScope.
For example, the following is not allowed:
myMod.config(function(greeting) {
// WON'T WORK -- greeting is an *instance* of a service.
// Only providers for services can be injected in config blocks.
});
What you do have access to are any providers for services you've made:
myMod.config(function(greetingProvider) {
// ok fine!
});
All the best.
You can not use $rootScope during the configuration phase of an angular application.
Only constant and provider can be injected to the configuration phase.
You can use run phase, or create a provider (that is actually a service) to hold the configuration you want.
// Option 1 - during run
angular
.module('yourApp')
.run(['$rootScope', function($rootScope) {
}])
// Option 2 - provider
angular
.module('yourApp')
.provider('yourSettings', function() {
var $this = this;
this.yourSettings = 'yourValue';
this.$get = function() {
return $this;
}
})
angular
.module('yourApp')
.config(['yourSettingsProvider', function(yourSettingsProvider) {
// You can use yourSettingsProvider.yourSettings
}])
I have the following Angular module. How can i call for example APIHost from one of my controllers?
angular.module('configuration', [])
.constant('APIHost','http://api.com')
.constant('HostUrl','http://example.com')
.constant('SolutionName', 'MySite');
Constant is nothing but one kind of provider recipe.
You need to inject constant dependency inside your controller factory function, that's it.
app.controller('testCtrl', function($scope, APIHost){
console.log(APIHost)
})
Make sure your configuration module has been added to main module as dependency
to get use of constant's provider like below
var app = angular.module('app', ['configuration', 'otherdependency']);
app.controller( ... ) //here you can have configuration constant available
Like this, just like any service or factory.
I have also include structure for industry standard (kind of) from john papa's coding guidelines.
(function() {
'use strict';
angular
.module('configuration')
.controller('ctrlXYZ', ctrlXYZ);
//Just inject as you would inject a service or factory
ctrlXYZ.$inject = ['APIHost'];
/* #ngInject */
function ctrlXYZ(APIHost) {
var vm = this;
activate();
function activate() {
//Go crazy with APIHost
console.log(APIHost);
}
}
})();
Hope the helps!
I want to dynamically inject a factory into my Angular controller based on the route parameter. This is my route configuration:
$routeProvider
.when("/tables/:table",
{
controller: "tableController",
templateUrl: "/app/views/table.html",
resolve: {
"factory": function r($route) {
return $injector.get($route.current.params.table + "Factory"); // Error!
}
}
})
For instance, when the route is tables/employee, I want an employeeFactory to be injected into tableController, and so on.
Unfortunately, this configuration does not work — I am getting an Unknown provider: employeeFactory error in the r function.
On the other hand, I can instead pass an $injector service directly to the tableController and successfully resolve employeeFactory there:
(function (angular) {
var tableController = function ($routeParams, $injector) {
// employeeFactory resolves successfully here!
var factory = $injector.get($routeParams.table + "Factory");
};
angular.module("appModule").controller("tableController", tableController);
})(angular);
However, I do not like this approach because it follows the service locator anti-pattern. I would really like factory to be injected using routing configuration and not this ugly workaround.
So, why Angular is throwing an error when using $injector.get() with resolve, but successfully resolves the factory inside of the tableController? I am using Angular 1.4.4.
You apparently use $injector that was injected into config block, and it differs from $injector that is injected anywhere else. The former acts on service providers, the latter acts on service instances.
It should be
"factory": function r($route, $injector) {
return $injector.get($route.current.params.table + "Factory");
}
I have an angular service responsible for loading a config.json file. I would like to call it in my run phase so I set that json in my $rootContext and hence, it is available in the future for everyone.
Basically, this is what I've got:
angular.module('app.core', []).run(function(CoreRun) {
CoreRun.run();
});
Where my CoreRun service is:
angular.module('app.core').factory('CoreRun', CoreRun);
CoreRun.$inject = ['$rootScope', 'config'];
function CoreRun($rootScope, config) {
function run() {
config.load().then(function(response) {
$rootScope.config = response.data;
});
}
return {
run: run
};
}
This works fine and the problem comes up when I try to test it. I want to spy on my config service so it returns a fake promise. However, I cannot make it since during the config phase for my test, services are not available and I cannot inject $q.
As far as I can see the only chance I have to mock my config service is there, in the config phase, since it is called by run block.
The only way I have found so far is generating the promise using jQuery which I really don't like.
beforeEach(module('app.core'));
var configSample;
beforeEach(module(function ($provide) {
config = jasmine.createSpyObj('config', [ 'load' ]);
config.load.and.callFake(function() {
configSample = { baseUrl: 'someurl' };
return jQuery.Deferred().resolve({data: configSample}).promise();
});
provide.value('config', config);
}));
it('Should load configuration using the correspond service', function() {
// assert
expect(config.load).toHaveBeenCalled();
expect($rootScope.config).toBe(configSample);
});
Is there a way to make a more correct workaround?
EDIT: Probably worth remarking that this is an issue just when unit testing my run block.
Seems that it is not possible to inject $q the right way, because function in your run() block fires immediately. run() block is considered a config phase in Angular, so inject() in tests only runs after config blocks, therefore even if you inject() $q in test, it will be undefined, because run() executes first.
After some time I was able to get $q in the module(function ($provide) {}) block with one very dirty workaround. The idea is to create an extra angular module and include it in test before your application module. This extra module should also have a run() block, which is gonna publish $q to a global namespace. Injector will first call extra module's run() and then app module's run().
angular.module('global $q', []).run(function ($q) {
window.$q = $q;
});
describe('test', function () {
beforeEach(function () {
module('global $q');
module('app.core');
module(function ($provide) {
console.log(window.$q); // exists
});
inject();
});
});
This extra module can be included as a separate file for the test suite before spec files. If you put the module in the same file where the tests are, then you don't event need to use a global window variable, but just a variable within a file.
Here is a working plunker (see a "script.js" file)
First solution (does not solve the issue):
You actually can use $q in this case, but you have to inject it to a test file. Here, you won't really inject it to a unit under test, but directly to a test file to be able to use it inside the test. So it does not actually depend on the type of a unit under test:
// variable that holds injected $q service
var $q;
beforeEach(module(function ($provide) {
config = jasmine.createSpyObj('config', [ 'load' ]);
config.load.and.callFake(function() {
var configSample = { baseUrl: 'someurl' };
// create new deferred obj
var deferred = $q.defer();
// resolve promise
deferred.resolve({ data: configSample });
// return promise
return deferred.promise;
});
provide.value('config', config);
}));
// inject $q service and save it to global (for spec) variable
// to be able to access it from mocks
beforeEach(inject(function (_$q_) {
$q = _$q_;
}));
Resources:
Read more about inject() - angular.mock.inject
$q
And one more note: config phase and run phase are two different things. Config block allows to use providers only, but in the run block you can inject pretty much everything (except providers). More info here - Module Loading & Dependencies
Here is a working example of how I have set up an interceptor which attaches an authentication token to each request (this is more or less the example from https://docs.angularjs.org/api/ng/service/$http)
angular.module("app", [])
.config(function ($httpProvider) {
$httpProvider.interceptors.push("authInterceptor");
})
.factory("authInterceptor", function ($q) {
return {
// interceptor configuration here
}
})
I have a lot of other stuff in my config and run blocks which call and initiate services from different angular modules, so I want to tidy things up a bit. However I understand there are some very specific rules to dependency injection in config blocks, which I don't quite understand, and these are preventing me from defining my authInterceptor factory in a separate module. As other logic in the config and run blocks calls other modules in the app, declaring that interceptor right there looks out of place.
This is what I want to do:
angular.module("services.authInterceptor", [])
.factory("authInterceptor", function ($q) {
return {
// interceptor configuration here
}
});
angular.module("app", [
"services.authInterceptor"
]).config(function ($httpProvider, authInterceptor) {
$httpProvider.interceptors.push("authInterceptor");
});
// Error: Unknown provider authInterceptor.
I tried injecting it to the run block instead, but I guess you're not allowed to inject $httpProvider there:
angular.module("app", [
"services.authInterceptor"
]).run(function ($httpProvider, authInterceptor) {
$httpProvider.interceptors.push("authInterceptor");
});
// Error: Unknown provider: $httpProviderProvider <- $httpProvider
Where should I inject the module so that $httpProvider is also injectable, and where should I add the interceptor to existing ones? My main goal is keeping the interceptor and other similar services in their own self-containing modules.
EDIT
I get a different error which seems to be getting me closer when I declare a provider instead of factory (for some reason I always thought these were interchangeable):
angular.module("services.authInterceptor")
.provider("authInterceptor", function ($q) {
return {}
})
// Error: Unknown provider: $q
So it now successfully injects authInterceptor to my config block, but fails when trying to find $q.
During the configuration phase only providers and constants can be injected. This is to prevent instantiation of services before they have been fully configured.
This is why you register interceptors by name (pushing the name as a string into the $httpProvider.interceptors array). They will be resolved later during runtime.
This is exactly what you did in your working example, and what you need to do in your second, even when the interceptor is in another module:
angular.module("services.authInterceptor", [])
.factory("authInterceptor", function ($q) {
return {
// interceptor configuration here
}
});
angular.module("app", ["services.authInterceptor"])
.config(function ($httpProvider) {
$httpProvider.interceptors.push('authInterceptor');
});
Demo: http://plnkr.co/edit/A8SgJ87GOBk6mpXsoFuZ?p=preview