7

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);
    }));
  });
});
6
  • But the idea (if I am not mistaken) is that you decorate your $log and then you override the entire $log with a mock. So obviously in your test you will have a function with a simple debug function. I guess I missed something along the way. Commented Mar 18, 2014 at 19:12
  • I created a plunker for you, I had to hack a little bit for angular-mock, but check it out: plnkr.co/edit/kim2NTNBp0eflOhFVhF3?p=preview Commented Mar 18, 2014 at 19:16
  • Also started making a plunker. Note that the call to angular.module() needs two arguments... Commented Mar 18, 2014 at 19:29
  • Plunk using Angular JS 1.2.x, Angular Mocks for Mocha, Mocha, Chai, and Chai Spies: j.mp/1p8AcLT Commented Mar 18, 2014 at 19:54
  • Interesting thoughts all. Thanks! I'll take a look and see what I can get going. Also, @al-the-x, in my code that decorator is in it's own file, so no need for the second arg on angular.module(), but good catch anyway! Commented Mar 18, 2014 at 20:47

1 Answer 1

6

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

Sign up to request clarification or add additional context in comments.

5 Comments

I see how this will work now... but yes, having a global decorator function is not ideal. :) I'll work on extracting that to a service.
Well, I can get this working with the global object, but not the service. Argh. I keep getting Error: [$injector:unpr] Unknown provider: LogDecorator. Anyway, thanks for the other help, I'll see what I can do about it being a service another time.
@jakerella Since this is being used in a config function, you'd probably have to use $provider.provider(), actually, now that I think about it... Or maybe just $provider.constant().
Yeah, I ended up having to do that in other places as well, but those were for circular references.
sure, don't thank me, i just can't stand it if one space is out of place, or if i see non-highlighted code... thanks for that answer though - this has been bugging me for a while, too.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.