How to test angular decorator functionality

All we need is an easy explanation of the problem, so here it is.

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);
    }));
  });
});

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 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. 😀

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply