How to unit test an AngularJS service / factory - javascript

I'm already some time in the development using AngularJS, and what I do write works, but know I've come to a point where I would like to run unit tests on my AngularJS code.
I have created a very simple service that will inject a stylesheet onto the page,
see the example code below:
var OfficeSuiteStylesheetInjectorModule = angular.module('OfficeSuiteStylesheetInjectorModule', []);
OfficeSuiteStylesheetInjectorModule.factory('stylesheetInjectorService', ['$q', function($q) {
// Returns the service itself.
return {
inject: function(id, uri) {
var deferred = $q.defer();
// Embed the stylesheet into the page, but only when it's non-existing.
if (!angular.element('#' + id).length) {
var link = StylesheetFactory.Create(id, uri);
{
link.onload = deferred.resolve;
angular.element('head').append(link);
}
return deferred.promise;
}
}
}
}]);
It's not a big service, it's just dependend on $q for promises so that I can run additional logic when the stylesheet has been embedded in the page.
Now, I'm using Jasmine (I'm quite new to this) for testing my JavaScript code and I would like to test this module.
I have a skeleton:
// Tests for the angular 'StylesheetInjectorService'.
describe('StylesheetInjectorService', function() {
var stylesheetInjectorService = {};
// This code is executed before every test.
beforeEach(function() {
// Tells jamine that the module we're working on is the 'OfficeSuiteStylesheetInjectorModule'.
angular.module('OfficeSuiteStylesheetInjectorModule');
});
// Ensures that it injects a stylesheet element into the page.
it('Should inject a stylesheet element into the page.', function() {
// How to test here that the stylesheet is injected?
});
});
});
How can I inject te service in the page and ensures that the stylesheet is loaded?
Edit: Loading service now works:
beforeEach(module('OfficeSuiteStylesheetInjectorModule'));
// This code is executed before every test.
beforeEach(function() {
// Inject the required dependencies into the page.
inject(function($injector) {
stylesheetInjectorService = $injector.get('stylesheetInjectorService');
});
});
The same question is still open however. How to test if a stylesheet was embedded in the page?
Any help is highly appreciated.
Kind regards

To write a spec for the attachment of a stylesheet to angular.element('head') I would change the logic a bit to attach it to $document.head.
If you dont want to do that, I would recommend that you change your service into a directive seeing as how injecting a script element, is manipulating the DOM. That way you would kill two birds with one stone, as you would need to inject $compile to test your directive (which would enable you to $compile a custom head element to boot). But this is slightly "over the top" for now.
Implementation:
if (!angular.element('#' + id).length) {
var link = StylesheetFactory.Create(id, uri);
link.onload = deferred.resolve;
$document.head.append(link);
return deferred.promise;
}
}
beforeEach:
/**
* Sorry, this was previously $location which was just
* such a silly mistake.
*/
var $timeout;
beforeEach(function () {
inject(function ($injector) {
$timeout = $injector.get('$timeout');
});
});
it:
it('attaches the stylesheet to $document.head', function () {
styleSheetInjectorService.inject('someId', '/path/to/some/stylesheet');
$timeout.flush(); // flush promises
expect(angular.element($document.head).lastChild[0].nodeName).to.eq('LINK');
});
Something along those lines should get you up and running. Bare in mind that the spec I wrote uses the chai#expect style assertions, and the mocha test framework. Edit the syntax to fit Jasmine if you mean to copy-paste.

I had this doubt a while ago.
To inject your controllers and services into your tests you need to use a tool called Angular Mocks. Here's some official reference about it.
I sugest you use it together with the Karma enviroment.
Here's a great Getting Started tutorial:
https://www.airpair.com/angularjs/posts/unit-testing-angularjs-applications
This other one is for the Ionic Framework, but can still aplly to your case
Hope it can help.

Related

Minified $provider injection with jasmine and angular

We have a project (angular) and some unittests for it (jasmine+sinon), which when minified creates some issues. For the actual code, we've solved these problems by injecting using the staticly typed string array, e.g. ['locationService', 'etcService'].
Unfortunately for the unittests, the minification has some more problems to solve. As an example:
module(function($provide){
$provide.service('etc..',...);
}
Code above immediately becomes unusuable since the provider variable gets renamed to something like 'a'. I've tried to tweak it a bit wrapping the function with something like below:
function injectTest($provide){
// do the same stuff
}
injectTest.$inject = ['$provide'];
which was a recommended solution in some other online posts. The problem is with modules this really doesn't work. I've tried both:
module(angular.injector().invoke(injectTest)); // which results in 'Unknown provider: $provideProvider <- $provide
and
module(injectTest); // which results in 'Unknown provider: nProvider <- n'
Is there any way to inject the $provider into a module without breaking on minification?
Inline injection :
var myFN = ['$provide', function($provide){
// do stuff
}]
Now if you want to bind a function to a 3rd party library where you need service let's say in my sample your function need the service CRUDService and receive a params objects from the 3rd party :
var myFN = ['CRUDService', function(CRUDService){
// do some init stuff
// you can either make it a singleton by sotrng the function and return the reference or either return new function on each call
return function(params){
// do stuff
};
}] ;
// now to bind it to your 3rd party
objectFor3rdParty = {fn:$injector.invoke(myFN)};
I use only inline injection instead of $inject, matter of taste i guess.

In Protractor, is it possible to run an Angular modules function?

I'm very new to Javascript and Protractor. Still trying to get my head around simple syntax so forgive me if I'm way off base here.
So our angular app, has a module with a factory that generates toast messages. I'd like to disable all toast messages during my E2E testing. We have a function within the factory to disable toasts. Here's some simplified code.
//the module
var module = angular.module('toast',[]);
//the factory
module.factory('tf',[function tf(){
//factory code
//the function within the module's factory
moduleFactory.enable = function(enable){
isEnabled = enable;
};
}]);
My question is, can I access that function in protractor to turn that to false?
I've been searching around and it seems that mocking is how to do it. Something similar to how you disable angular animations.
// Disable animations so e2e tests run more quickly
var disableNgAnimate = function() {
angular.module('disableNgAnimate', []).run(['$animate', function($animate) {
$animate.enabled(false);
}]);
};
browser.addMockModule('disableNgAnimate', disableNgAnimate);
However, I'm struggling with the syntax on accessing the factory's function within the module...Any help would be appreciated.
I believe I've found the solution for anyone else that may have a similar issue.
Using the executeScript function of protractor.
browser.executeScript(function()
{
return angular.element(document).injector().get('toastFactory').enableToasts(false);
});

Cant unit test application with manual bootstrap in AngularJS

my code needs to do manual bootstrap cause requires loading files before laoding application.
var bootstrapModule = angular.module('bootstrapModule', []);
// the bootstrapper service loads the config and bootstraps the specified app
bootstrapModule.factory('bootstrapper', function ($http, $log, $q, $timeout) {
return {
bootstrap: function (appName) {
var deferred = $q.defer();
$http.get('config/urls/development.json')
.success(function (data) {
.....
deferred.resolve();
})
.error(function (data) {
...
});
return deferred.promise;
}
};
});
// create a div which is used as the root of the bootstrap app
var appContainer = document.createElement('div');
// in run() function you can now use the bootstrapper service and shutdown the bootstrapping app after initialization of your actual app
bootstrapModule.run(function (bootstrapper) {
bootstrapper.bootstrap('angular3App').then(function () {
// removing the container will destroy the bootstrap app
if (appContainer) appContainer.remove();
});
});
// make sure the DOM is fully loaded before bootstrapping
angular.element(document).ready(function () {
angular.bootstrap(appContainer, ['bootstrapModule']);
});
I want my unit tests to replicate this behaviour so it can have the info in the loaded file.
I am trying to bootstrap my app in beforeEach but it is not working.
beforeEach(function(done) {
var bootstrapModule = module('bootstrapModule')
// create a div which is used as the root of the bootstrap app
var appContainer = document.createElement('div');
// in run() function you can now use the bootstrapper service and shutdown the bootstrapping app after initialization of your actual app
bootstrapModule.run(function (bootstrapper) {
bootstrapper.bootstrap('angular3App').then(function () {
// removing the container will destroy the bootstrap app
appContainer.remove();
});
});
// make sure the DOM is fully loaded before bootstrapping
angular.element(document).ready(function () {
angular.bootstrap(appContainer, ['bootstrapModule']);
});
});
It is not bootstrapping accordingly. The information in my scope that is dependent from load of the bootstrapped loaded file is not available.
Anyone has any ideas?
This is an old question, but I just dealt with this issue and it seems that at least one other person has recently had a similar issue. I was banging my head against the wall to get my unit tests to run with a setup my like #saulovenancio. The factory was a way I found to import the config.json file needed for the project and it worked - it just didn't play nice with our tests.
As far as I could tell, the tests were trying to run before the promise in the bootstrapMOodule.factory was resolved. Luckily, while loading the config JSON was a requirement - loading it via service was not a project requirement - so I scrapped the service that was fetching the JSON.
I pretty much followed the approach outlined here:
How to configure your AngularJS application using environment variables
In the index.html document I added a script tag for an env.js file before the file that loads my angular app.
<html>
...
<script src="js/jquery.js"></script>
<!-- Gets the config.json -->
<script src="env.js"></script>
<!-- The App -->
<script src="js/main.js"></script>
</html>
The env.js file contains the code that loads the config.json onto the window. Mine looks something like this:
(function (window) {
window.__env = window.__env || {};
$.get( 'config.json', function( data ){
window.__env = data;
})
.error( function handleErrorResponse( xhr, status, error ){
console.error( "An AJAX error occured: " + status + "\nError: " + error ) ;
});
}(this));
From here I just loaded the window.__env into my main.js file and used it for my constants.
var env = {};
if(window){
Object.assign(env, window.__env);
}
...
angular.module('app').constant('AppSettings', env);
Once I made those changes everything worked like a charm ... big thanks to Jurgen Van de Moere. Hopefully, this will be helpful to someone.

Why do I get the error: Unknown provider: personHubProvider <- personHub

I am trying to work things out with angularjs, dependency injections, factories and signalR hubs, but I get the error Unknown provider: personHubProvider <- personHub and I cannot figure out what is causing it.
My code is like this:
First I declare the module:
var ucp = angular.module('UCP', []);
which is used in the html tag by ng-app="UCP".
Then I do some configuration on the ucp module for the hub settings and creating a signalRHub factory to get the hubs without typing much of the same code:
ucp.config(function($routeProvider) {
//Declaring routes, removed this part of code cause I think it hasn't to do
//with the problem I got
$.support.cors = true;
$.connection.hub.url = config.signalR.connectionURL;
$.connection.hub.logging = config.signalR.logging;
$.connection.hub.start();
}).factory('signalRHub', [function() {
return {
person: $.connection.Persons.server
};
}]);
Then, I create another factory, that takes care of getting the data from the server:
ucp.factory('personHub', ['signalRHub', function(signalRHub) {
return {
get: function(onsuccess, onerror) {
signalRHub.person.get()
.done(function(persons) {
onsuccess(persons);
})
.fail(function(error) {
onerror(error);
});
}
}
}]);
and this factory I inject in my controller so I can execute the call to get the data from the server and put it in the scope which provides to show the data in the browser:
ucp.controller('personController', ['$scope', 'personHub', function($scope, personHub){
var self = this;
$scope.init = function() {
personHub.get(self.ongetsuccess, self.ongeterror);
}
self.ongetsuccess = function(persons) {
$scope.persons = persons;
};
self.ongeterror = function(error) {
};
}]);
When I open the webpage I get the error I mentioned before: Error: Unknown provider: personHubProvider <- personHub.
I think something goes wrong with creating the personHub factory service, which on his turn causes an Dependency Injection error for the controller. My question is, what is causing the error, am I doing something wrong with creating the personHub factory and if so, what am I doing wrong?
As I had stated in the comments I know this error occurs because the injected $provider isn't defined therefore it's reporting that there is nothing to provide said $provider. A $provider might be a factory, service, controller, or value (there's probably others I'm forgetting) and must be defined before it is referenced for an injection.
More on providers and injection here: https://github.com/angular/angular.js/wiki/Understanding-Dependency-Injection
The way I've been dealing with organizing code is by encapsulating most of the parts necessary for a given view into one js file. In that JS file I start off with a services section where I define a new module like:
angular.module("loginModule.services",[/*dependencies*/]).service("loginService", [/*dependencies*/function(){ return {get:function(){return "what up!"}};}]);
Then lower in the file I define my controllers like
angular.module("loginModule.controllers",[/*dependencies*/]).controller("LoginCtrl" ,["$scope", function($scope) { /* code here* /}]);
then at the bottom of the file
angular.module("loginModule", ["loginModule.services", "loginModule.controllers"]);
And finally in my mainApp.js (with the main ng-app module)
angular.module("mainApp", ["loginModule"]);
I've defined my script blocks in the head at the moment.
<script type="text/javascript" src="components/loginModule.js"></script>
<script type="text/javascript" src="mainApp.js"></script>
Also to note my loginModule actually depends on other services for which the javascript files come afterward. This doesn't seem to be an issue since the mainApp.js is being deferred until the end.
I just checked some of this in Chrome and it appears the JS files from my computer do load in order and as pairs (only two per domain at a time then when a response comes back the next request is fired). I also tried moving the script blocks from the head to the bottom of the HTML and I can't tell any difference (my laptop has an SSD and mostly local files so probably more appreciable in a production scenario).

Unit test when loading things at app run with AngularJS

I need my app to run some configuration at runtime vi an HTTP endpoint.
I wrote a simple service to do that:
module.factory('config', function ($http, analytics) {
return {
load: function () {
$http.get('/config').then(function (response) {
analytics.setAccount(response.googleAnalyticsAccount);
});
}
}
});
Next, I call this module in a run block of my app module:
angular.module('app').***.run(function(config) {
config.load();
});
All is working well when the app is running but in my unit tests, I get this error: "Error: Unexpected request: GET /config"
I know what it means but I don't know how to mock it when it happens from a run block.
Thanks for your help
EDIT to add spec
Calling this before each
beforeEach(angular.mock.module('app'));
Tried this to mock $httpBackend:
beforeEach(inject(function($httpBackend) {
$httpBackend.expectGET('/config').respond(200, {'googleAnalyticsAccount':});
angular.mock.module('app')
$httpBackend.flush();
}));
But got:
TypeError: Cannot read property 'stack' of null
at workFn (/Users/arnaud/workspace/unishared-dredit/test/lib/angular/angular-mocks.js:1756:55)
TypeError: Cannot read property 'stack' of null
at workFn (/Users/arnaud/workspace/unishared-dredit/test/lib/angular/angular-mocks.js:1756:55)
TypeError: Cannot read property 'stack' of null
at workFn (/Users/arnaud/workspace/unishared-dredit/test/lib/angular/angular-mocks.js:1756:55)
EDIT since update to AngularJS 1.0.6
Since I've updated to AngularJS 1.0.6, advised by Igor from the Angular team, the issue is gone but now I've now got this one, which sounds more "normal" but I still can't figure out how to make it works.
Error: Injector already created, can not register a module!
I struggled with this error for a little while, but managed to come up with an sensible solution.
What I wanted to achieve is to successfully stub the Service and force a response, on controllers it was possible to use $httpBackend with a request stub or exception before initiating the controller.
In app.run() when you load the module it initialises the object and it's connected Services etc.
I managed to stub the Service using the following example.
describe('Testing App Run', function () {
beforeEach(module('plunker', function ($provide) {
return $provide.decorator('config', function () {
return {
load: function () {
return {};
}
};
});
}));
var $rootScope;
beforeEach(inject(function (_$rootScope_) {
return $rootScope = _$rootScope_;
}));
it("defines a value I previously could not test", function () {
return expect($rootScope.value).toEqual('testing');
});
});
I hope this helps your app.run() testing in the future.
I don't know if you are still looking for an answer to this question. But here is some information that might help.
$injector is a singleton for an application and not for a module. However, angular.injector will actually try to create a new injector for each module (I suppose you have a
beforeEach(module("app"));
at the beginning.
I had the same problem while using Angular, RequireJS, Karma and Jasmine and I figured out two ways to solve it. I created a provider for the injector function as a separate dependency in my tests. For example MyInjectorProvider which provides a singleton instance of $injector.
The other way was to move the following statements:
beforeEach(module("app"));
beforeEach(inject(function($injector){
...
})
inside the test suite description. So here is how it looked before:
define(['services/SignupFormValidator'], function(validator){
var validator;
beforeEach(module("app"));
beforeEach(inject(function($injector){
validator = $injector.get("SignupFormValidator");
})
describe("Signup Validation Tests", function(){
it("...", function(){...});
});
});
After applying the fix it looks like this:
define(['services/SignupFormValidator'], function(validator){
var validator;
describe("Signup Validation Tests", function(){
beforeEach(module("app"));
beforeEach(inject(function($injector){
validator = $injector.get("SignupFormValidator");
});
it("...", function(){...});
});
});
Both the solutions worked in my case.
You should mock every HTTP request with ngMock.$httpBackend. Also, here is a guide.
Update
You don't need the angular.mock.module thing, just need to inject your app module. Something like this:
var httpBackend;
beforeEach(module('app'));
beforeEach(inject(function($httpBackend) {
httpBackend = $httpBackend;
$httpBackend.expectGET('/config').respond(200, {'googleAnalyticsAccount': 'something'});
}));
In your tests, when you need the mocked http to answer, you will call httpBackend.flush(). This is why we have a reference to it, so you don't need to inject it in every single test you have.
Note you will need to load angular-mock.js in order to it work.

Categories

Resources