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);
}));
});
});
Decorators are a design pattern that is used to separate modification or decoration of a class without modifying the original source code. In AngularJS, decorators are functions that allow a service, directive or filter to be modified prior to its usage.
To run the test, you will only need to run the command ng test . This command will also open Chrome and run the test in watch mode, which means your test will get automatically compiled whenever you save your file. In your Angular project, you create a component with the command ng generate component doctor .
detectChanges(). Delayed change detection is intentional and useful. It gives the tester an opportunity to inspect and change the state of the component before Angular initiates data binding and calls lifecycle hooks.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With