Accessing real AngularJS service in Jasmine tests when mocked globally - angularjs

Let assume I have some service in AngularJS: ComplexService. It performs complex operations on init and has got a complex interface...
In Karma/Jasmine tests, to simplify other components unit tests, I have defined a mock globally[1] (outside of all describe declarations in Karma global scope):
beforeEach(function () {
angular.mock.module('MYMODULE', function ($provide) {
$provide.value('ComplexService', buildComplexServiceMock());
});
});
[1](The reason of that decision was to avoid declaring it in each test suite again -we have about 50 of them and each eventually uses the service indirectly or by default)
Let now suppose, that I decided to Write some unit test for the complex service.
My question is: Does it exist a way to access the real service now? (not mock)
My temporary solution is to make my service accessible in global scope too and access it directly:
function ComplexService(Other, Dependencies) {
//code here
}
angular.module('MYMODULE')
.service('ComplexService', ['Other', 'Dependencies', ComplexService]);
window.ComplexService = ComplexService;
But I am not happy with it. (I don't want production code to be accessible globally, maybe except in tests)
Can somebody please, give me some clue?
Edit
Another thing I would like to avoid if possible is specifying ComplexService dependencies in test directly (in a case the order would change in future)
Temporary solution which is bad:
let complexServiceTestable;
beforeEach(function () {
inject(function (Other, Dependencies) {
//If order of dependencies would change, I will have to modify following line:
complexServiceTestable = window.ComplexService(Other, Dependencies);
});
});
Something I would appreciate most if possible:
let complexServiceTestable;
beforeEach(function () {
angular.mock.module('MYMODULE', function ($provide) {
//some magic here
});
});
beforeEach(function () {
inject(function (ComplexService) {
complexServiceTestable = ComplexService;
});
});

You could do is to explicity import the real service in your test and override the $provide mock with the real one:
import ComplexService from '../your-complex-service-path/ComplexService';
describe('....', function(){
beforeEach(function () {
angular.mock.module('MYMODULE', function ($provide) {
$provide.value('ComplexService', ComplexService);
});
});
});
I understand the design decision but maybe the best thing would be to make a factory capable of injecting the $provide mocks of any Service passed as a parameter, name or path, It can be a little tricky but It might ended up being a more maintainable and descriptive approach.

Thanks to #MatiasFernandesMartinez hints in comments, after some experiments I finally reached working solution (using provider):
In global Karma context:
beforeEach(function () {
angular.mock.module('MYMODULE', function ($provide, ComplexServiceProvider) {
$provide.value('ComplexServiceBackup', ComplexServiceProvider);
$provide.value('ComplexService', buildComplexServiceMock());
});
});
In ComplexService tests:
describe('ComplexService', function () {
let complexServiceTestable;
beforeEach(function () {
inject(function (ComplexServiceBackup) {
complexServiceTestable = ComplexServiceBackup.$get();
});
});
});

Related

Should I mock ALL external Services in angular?

I'm currently getting started with angular unit testing. As the first controller I wanted to tes looked like this, I got confused.
angular.module('sgmPaperApp')
.controller('AccountCtrl', function ($mdToast, user, $firebaseArray, Ref) {
var vm = this;
vm.data = user;
vm.save = saveUser;
vm.comments = $firebaseArray(Ref.child('comments').orderByChild('person').equalTo(user.$id));
function saveUser() {
vm.data.$save().then(function () {
$mdToast.showSimple('Data saved');
});
}
});
Should I really mock all external services I use? After all that controller isn't very much more then external services and mocking the firebaseArray could be difficult.
Thanks for your advice and helping me get started with testing
You don't need to worry about what the external dependencies do, just mock their APIs.
These are the only mocks I can see. I'm going to assume you're using Jasmine
var Ref, $firebaseArray, $mdToast, user, vm;
beforeEach(function() {
Ref = jasmine.createSpyObj('Ref', ['child', 'orderByChild', 'equalTo']);
Ref.child.and.returnValue(Ref);
Ref.orderByChild.and.returnValue(Ref);
Ref.equalTo.and.returnValue(Ref);
$firebaseArray = jasmine.createSpy('$firebaseArray').and.returnValue('comments');
$mdToast = jasmine.createSpyObj('$mdToast', ['showSimple']);
user = jasmine.createSpyObj('user', ['$save']);
user.$id = 'id';
module('sgmPaperApp'); // you should consider separate modules per "thing"
inject(function($controller) {
vm = $controller('AccountCtrl', {
$mdToast: $mdToast,
user: user,
$firebaseArray: $firebaseArray,
Ref: Ref
});
});
});
Then you can easily create your tests
it('assigns a bunch of stuff on creation', function() {
expect(vm.data).toBe(user);
expect(vm.comments).toEqual('comments'); // that's what the mock returns
expect(Ref.child).toHaveBeenCalledWith('comments');
expect(Ref.orderByChild).toHaveBeenCalledWith('person');
expect(Ref.equalTo).toHaveBeenCalledWith(user.$id);
expect($firebaseArray).toHaveBeenCalledWith(Ref);
});
You can even test promise based methods like saveUser
it('saves the user and makes some toast', inject(function($q, $rootScope) {
user.$save.and.returnValue($q.when()); // an empty, resolved promise
vm.saveUser();
expect(user.$save).toHaveBeenCalled();
expect($mdToast.showSimple).not.toHaveBeenCalled(); // because the promise hasn't resolved yet
$rootScope.$apply(); // resolves promises
expect($mdToast.showSimple).toHaveBeenCalledWith('Data saved');
}));
So to answer the question we need to consider what we're actually trying to do. If we are trying to unit test, then yes, we need to mock all dependencies.
Mocking your dependencies won't be hard though. You only need to mock what you're using.
For example, $firebaseArray starts off as a function that receives a paramter, we know that much:
var mockFirebaseArray = function(ref) {
};
Next, before we can finish it, we need to mock the Ref:
var mockRef = {
child: function(path) {
this.orderByChild = function(path) {
this.equalTo = function(val) {
};
return this;
};
return this;
}
};
With these things in place we can decide how the test will "pass". We could just use spies. Or, we could set local variables that we can assert later on our way through.
Spies are my preferred method because you can even verify they were called with specific values:
expect(mockFirebaseArray).toHaveBeenCalled();
expect(mockRef.child).toHaveBeenCalledWith('comments');
Now, if you're wanting to write an integration test that's different. In that case I'd still use spies, but you'd actually be executing those dependencies. Generally speaking there is no need to test your dependencies because they should be tested in isolation as well. Furthermore, there is less need to test other people's API's if they are from trustworthy sources.

Jasmine: Trying to test an AngularJS factory function

I am totally new to testing in AngularJS. I have setup karma, and am now attempting to test a certain function in a factory I have written.
Here is a snippet of my factory:
app.factory('helpersFactory', ['constants', function (constants) {
return {
someFunction: function() {
},
is24x24Icon: function (iconNum) {
return ((iconNum >= 10090 && iconNum <= 10125) ;
}
};
}]);
I then have this test:
describe('Factory: helpersFactory', function () {
beforeEach(module('ppMobi'));
var fct;
beforeEach(inject(function ($factory) {
fct = $factory('helpersFactory');
}));
it('should detect iconNum 10090 is a 24 x 24 icon', function () {
var iconNum = 10090;
var is24x24Icon = fct.is24x24Icon(iconNum);
expect(is24x24Icon).toBeTruthy();
});
});
I get an error from Karma telling me it cannot read 'is24x24icon' of undefined. Therefore I can only assume my factory has not been created properly during the test. I do have a dependency on constants in the factory used by other functions. This is just an angular.constant() I have setup on my main application module.
I have found some other posts, but am unsure how to proceed, do I need to inject my constants dependency into my test?
Kind of new myself but I think you need to use the underscore name underscore trick to inject your factory:
var fct;
beforeEach(inject(function (_helpersFactory_) {
fct = _helpersFactory_;
}));
This blog uses mocha but I found it useful and the Karma stuff should be the same: https://www.airpair.com/angularjs/posts/testing-angular-with-karma
And yes you will need to inject the constants as well (the link shows how) but your posted code does not seem to use constants so you won't need it for this particular test.

Unit testing decorators in Angular, decorating $log service

While it is fairly easy to unit test services/controllers in angular it seems very tricky to test decorators.
Here is a basic scenario and an approach I am trying but failing to get any results:
I defined a separate module (used in the main app), that is decorating $log service.
(function() {
'use strict';
angular
.module('SpecialLogger', []);
angular
.module('SpecialLogger')
.config(configureLogger);
configureLogger.$inject = ['$provide'];
function configureLogger($provide) {
$provide.decorator('$log', logDecorator);
logDecorator.$inject = ['$delegate'];
function logDecorator($delegate) {
var errorFn = $delegate.error;
$delegate.error = function(e) {
/*global UglyGlobalFunction: true*/
UglyGlobalFunction.notify(e);
errorFn.apply(null, arguments);
};
return $delegate;
}
}
}());
Now comes a testing time and I am having a really hard time getting it working. Here is what I have come up with so far:
(function() {
describe('SpecialLogger module', function() {
var loggerModule,
mockLog;
beforeEach(function() {
UglyGlobalFunction = jasmine.createSpyObj('UglyGlobalFunctionMock', ['notify']);
mockLog = jasmine.createSpyObj('mockLog', ['error']);
});
beforeEach(function() {
loggerModule = angular.module('SpecialLogger');
module(function($provide){
$provide.value('$log', mockLog);
});
});
it('should initialize the logger module', function() {
expect(loggerModule).toBeDefined();
});
it('should monkey patch native logger with additional UglyGlobalFunction call', function() {
mockLog.error('test error');
expect(mockLog.error).toHaveBeenCalledWith('test error');
expect(UglyGlobalFunction.notify).toHaveBeenCalledWith('test error');
});
});
}());
After debugging for a while I have noticed that SpecialLogger config section is not even fired.. Any suggestions on how to properly test this kind of scenario?
You're missing the module('SpecialLogger'); call in your beforeEach function.
You shouldn't need this part: loggerModule = angular.module('JGM.Logger');
Just include the module and inject the $log. Then check if your decorator function exists and behaves as expected.
After some digging I came up with a solution. I had to create and inject my own mocked $log instance and only then I was able to check weather or not calling error function also triggers call to the global function I was decorating $log with.
The details can be found on a blog post I wrote to explain this problem in detail. Plus I open sourced an anuglar module that makes use of this functionality available here

How to mock service in angularAMD with karma/jasmine?

I have a project using AngularAMD/RequireJS/Karma/Jasmine, that I have the basic configuration all working, most unit tests run and pass successfully.
I cannot get a mocked service injected correctly using either angular.mock.module or angularAMD.value().
I have:
// service definition in services/MyService.js
define(['app'],
function(app) {
app.factory('myService', [ '$document', function($document) {
function add(html) {
$document.find('body').append(html);
}
return { add: add };
}]);
}
);
// test
define(['angularAMD', 'angular-mocks', 'app', 'services/MyService'],
function(aamd, mocks, app) {
describe('MyService', function() {
var myBodyMock = {
append: function() {}
};
var myDocumentMock = {
find: function(sel) {
// this never gets called
console.log('selector: ' + sel);
return myBodyMock;
}
};
var svc;
beforeEach(function() {
// try standard way to mock a service through ng-mock
mocks.module(function($provide) {
$provide.value('$document', myDocumentMock);
});
// hedge my bets - try overriding in aamd as well as ng-mock
aamd.value('$document', myDocumentMock);
});
beforeEach(function() {
aamd.inject(['myService',
function(myService) {
svc = myService;
}]);
});
it('should work', function() {
// use svc expecting it to have injected mock of $document.
spyOn(myDocumentMock, 'find').andCallThrough();
spyOn(myBodyMock, 'append');
svc.add('<p></p>');
expect(myDocumentMock.find).toHaveBeenCalledWith('body');
expect(myBockMock.append).toHaveBeenCalledWith('<p></p>');
});
});
}
);
Does anyone know where I'm going wrong ? Any help would be much appreciated.
Angular isn't asynchronous, I think is not a good ideia use both. If you're trying to reach to a good modularization method, okay, but use the RequireJS optimizer to build everything before you put this on your browser, and about the tests, I think you can just use RequireJS optimizer to build your modules before, it will let you free from "CommonJS environment even in tests".
Looks like it'll be an issue with variable scopes, karma is very finicky about that. I think you should initialize your mock objects globally, then set them in the beforeEach.
The top line of my test files always looks something like:
var bodyMock, svcMock, foo, bar
Then in the beforeEach'es I set the values
Edit: Since bodyMock is only a scope variable, at the point where the tests are actually running and the browser is looking for an object 'bodyMock', it can't find anything.

How to test angular decorator functionality

I have a decorator in Angular that is going to extend the functionality of the $log service and I would like to test it, but I don't see a way to do this. Here is a stub of my decorator:
angular.module('myApp')
.config(function ($provide) {
$provide.decorator('$log', ['$delegate', function($delegate) {
var _debug = $delegate.debug;
$delegate.debug = function() {
var args = [].slice.call(arguments);
// Do some custom stuff
window.console.info('inside delegated method!');
_debug.apply(null, args);
};
return $delegate
}]);
});
Notice that this basically overrides the $log.debug() method, then calls it after doing some custom stuff. In my app this works and I see the 'inside delegated method!' message in the console. But in my test I do not get that output.
How can I test my decorator functionality??
Specifically, how can I inject my decorator such that it actually decorates my $log mock implementation (see below)?
Here is my current test (mocha/chai, but that isn't really relevant):
describe('Log Decorator', function () {
var MockNativeLog;
beforeEach(function() {
MockNativeLog = {
debug: chai.spy(function() { window.console.log("\nmock debug call\n"); })
};
});
beforeEach(angular.mock.module('myApp'));
beforeEach(function() {
angular.mock.module(function ($provide) {
$provide.value('$log', MockNativeLog);
});
});
describe('The logger', function() {
it('should go through the delegate', inject(function($log) {
// this calls my mock (above), but NOT the $log decorator
// how do I get the decorator to delegate the $log module??
$log.debug();
MockNativeLog.debug.should.have.been.called(1);
}));
});
});
From the attached plunk (http://j.mp/1p8AcLT), the initial version is the (mostly) untouched code provided by #jakerella (minor adjustments for syntax). I tried to use the same dependencies I could derive from the original post. Note tests.js:12-14:
angular.mock.module(function ($provide) {
$provide.value('$log', MockNativeLog);
});
This completely overrides the native $log Service, as you might expect, with the MockNativeLog implementation provided at the beginning of the tests because angular.mock.module(fn) acts as a config function for the mock module. Since the config functions execute in FIFO order, this function clobbers the decorated $log Service.
One solution is to re-apply the decorator inside that config function, as you can see from version 2 of the plunk (permalink would be nice, Plunker), tests.js:12-18:
angular.mock.module('myApp', function ($injector, $provide) {
// This replaces the native $log service with MockNativeLog...
$provide.value('$log', MockNativeLog);
// This decorates MockNativeLog, which _replaces_ MockNativeLog.debug...
$provide.decorator('$log', logDecorator);
});
That's not enough, however. The decorator #jakerella defines replaces the debug method of the $log service, causing the later call to MockNativeLog.debug.should.be.called(1) to fail. The method MockNativeLog.debug is no longer a spy provided by chai.spy, so the matchers won't work.
Instead, note that I created an additional spy in tests.js:2-8:
var MockNativeLog, MockDebug;
beforeEach(function () {
MockNativeLog = {
debug: MockDebug = chai.spy(function () {
window.console.log("\nmock debug call\n");
})
};
});
That code could be easier to read:
MockDebug = chai.spy(function () {
window.console.log("\nmock debug call\n");
});
MockNativeLog = {
debug: MockDebug
};
And this still doesn't represent a good testing outcome, just a sanity check. That's a relief after banging your head against the "why don't this work" question for a few hours.
Note that I additionally refactored the decorator function into the global scope so that I could use it in tests.js without having to redefine it. Better would be to refactor into a proper Service with $provider.value(), but that task has been left as an exercise for the student... Or someone less lazy than myself. :D

Resources