Is it possible to override constants for module config functions in tests?
Asked Answered
D

5

21

I've spent quite a while banging my head against trying to override injected constants provided to modules' config functions. My code looks something like

common.constant('I18n', <provided by server, comes up as undefined in tests>);
common.config(['I18n', function(I18n) {
  console.log("common I18n " + I18n)
}]);

Our usual way to guarantee what I18n is being injected in our unit tests is by doing

module(function($provide) {
  $provide.constant('I18n', <mocks>);
});

This works fine for my controllers, but it seems like the config function doesn't look at what's $provided outside of the module. Instead of getting the mocked values, it gets the earlier value defined as part of the module. (Undefined in the case of our tests; in the below plunker, 'foo'.)

A working plunker is below (look at the console); does anyone know what I'm doing wrong?

http://plnkr.co/edit/utCuGmdRnFRUBKGqk2sD

Devisee answered 10/1, 2014 at 18:57 Comment(4)
Constants are designed so that you cannot change themTerritory
Sure, but this works for controllers, but not for config functions. If constants could not be changed at all, it shouldn't work at all, right?Devisee
javascript as such does not have const, so the only way angular must have used const is : they wont define $watch for const. so the changes in const wont be reflected. so what you can do is, define const as an object rather than as a property and play with const value as you like. btw doing this breaks the real meaning of the const. like in c# once you define const, even for test you wont change it...Zimbabwe
I have to agree with @Conner honestly. You just found a bug with AngularJS that you got this to work.Holiday
I
26

First of all: it seems that jasmine is not working properly in your plunkr. But I am not quite sure – maybe someone else can check this again. Nevertheless I have created a new plunkr (http://plnkr.co/edit/MkUjSLIyWbj5A2Vy6h61?p=preview) and followed these instructions: https://github.com/searls/jasmine-all.

You will see that your beforeEach code will never run. You can check this:

module(function($provide) {
  console.log('you will never see this');
  $provide.constant('I18n', { FOO: "bar"});
});

You need two things:

  1. A real test in the it function – expect(true).toBe(true) is good enough

  2. You must use inject somewhere in your test, otherwise the function provided to module will not be called and the constant will not be set.

If you run this code you will see "green":

var common = angular.module('common', []);

common.constant('I18n', 'foo');
common.config(['I18n', function(I18n) {
  console.log("common I18n " + I18n)
}]);

var app = angular.module('plunker', ['common']);
app.config(['I18n', function(I18n) {
  console.log("plunker I18n " + I18n)
}]);

describe('tests', function() {

  beforeEach(module('common'));
  beforeEach(function() {
    module(function($provide) {
      console.log('change to bar');
      $provide.constant('I18n', 'bar');
    });
  });
  beforeEach(module('plunker'));    

  it('anything looks great', inject(function($injector) {
      var i18n = $injector.get('I18n');
      expect(i18n).toBe('bar');
  }));
});

I hope it will work as you expect!

Impeccant answered 10/1, 2014 at 21:12 Comment(2)
I’m working with @joe-drew on this; does inject need to be called for each it, or can it be as part of the beforeEach?Clareta
This does not answer the question. The config block in your plunker still uses the original value. OP wants to test the config block by injecting a different constant valueMeliamelic
W
5

I think the fundamental issue is you are defining the constants right before the config block, so each time the module is loaded, whatever mock value that may exist will be overridden. My suggestion would be to separate out the constants and config into separate modules.

Warring answered 12/11, 2014 at 0:9 Comment(4)
thanks for the answer... I tried separating out the constant to its own module here: plnkr.co/edit/QykUj9ChZePJOoRS1CMQ and as you can see, the config blocks are still using the original valueMeliamelic
the issue is a combination of two things, you are not mocking the constants correctly (that is, before the config block) and the tests itself are not running (hence no mocking is taking place). I modified your example using @michal’s working Jasmine template: plnkr.co/edit/ucj1bJyxPURyKsZLIKSF?p=previewWarring
just to elaborate further, what you see logged out the first time is the instantiation of the modules due to the ng-app directive, which is probably what lead to confusing resultsWarring
ah i see what I was doing wrong. Thanks a lot for the help on this!Meliamelic
T
4

Although it seems like you can't change which object a AngularJS constant refers to after it's been defined, you can change properties of the object itself.

So in your case you can inject I18n as you would any other dependency, and then alter it before your test.

var I18n;

beforeEach(inject(function (_I18n_) {
  I18n = _I18n_;
});

describe('A test that needs a different value of I18n.foo', function() {
  var originalFoo;

  beforeEach(function() {
    originalFoo = I18n.foo;
    I18n.foo = 'mock-foo';
  });

  it('should do something', function() {
    // Test that depends on different value of I18n.foo;
    expect(....);
  });

  afterEach(function() {
    I18n.foo = originalFoo;
  });
});

As above, you should save the original state of the constant, and restore it after the test, to make sure that this test doesn't interfere with any others that you might have, now, or in the future.

Transilient answered 31/3, 2014 at 6:15 Comment(0)
H
3

You can override a module definition. I'm just throwing this out there as one more variation.

angular.module('config', []).constant('x', 'NORMAL CONSTANT');

// Use or load this module when testing
angular.module('config', []).constant('x', 'TESTING CONSTANT');


angular.module('common', ['config']).config(function(x){
   // x = 'TESTING CONSTANT';
});

Redefining a module will wipe out the previously defined module, often done on accident, but in this scenario can be used to your advantage (if you feel like packaging things that way). Just remember anything else defined on that module will be wiped out too, so you'd probably want it to be a constants only module, and this may be overkill for you.

Holiday answered 9/11, 2014 at 7:33 Comment(2)
Appreciate the answer, but this does not answer the question. Again, the problem is testing a config block that uses a constantMeliamelic
It does in fact do what you wanted. Please view editsHoliday
H
1

I’m going to walk through a nastier solution as a series of annotated tests. This is a solution for situations where module overwriting is not an option. That includes cases where the original constant recipe and config block belong to the same module, as well as cases where the constant is employed by a provider constructor.

You can run the code inline on SO (awesome, this is new to me!)

Please note the caveats about restoring prior state after the spec. I do not recommend this approach unless you both (a) have a good understanding of the Angular module lifecycle and (b) are sure you cannot test something any other way. The three queues of modules (invoke, config, run) are not considered public API, but on the other hand they have been consistent over the history of Angular.

There may well be a better way to approach this — I’m really not sure — but this is the only way I have found to date.

angular
  .module('poop', [])
  .constant('foo', 1)
  .provider('bar', class BarProvider {
    constructor(foo) {
      this.foo = foo;
    }

    $get(foo) {
      return { foo };
    }
  })
  .constant('baz', {})
  .config((foo, baz) => {
    baz.foo = foo;
  });

describe('mocking constants', () => {
  describe('mocking constants: part 1 (what you can and can’t do out of the box)', () => {
    beforeEach(module('poop'));
  
    it('should work in the run phase', () => {
      module($provide => {
        $provide.constant('foo', 2);
      });

      inject(foo => {
        expect(foo).toBe(2);
      });
    });

    it('...which includes service instantiations', () => {
      module($provide => {
        $provide.constant('foo', 2);
      });

      inject(bar => {
        expect(bar.foo).toBe(2);
      });
    });

    it('should work in the config phase, technically', () => {
      module($provide => {
        $provide.constant('foo', 2);
      });

      module(foo => {
        // Code passed to ngMock module is effectively an added config block.
        expect(foo).toBe(2);
      });

      inject();
    });

    it('...but only if that config is registered afterwards!', () => {
      module($provide => {
        $provide.constant('foo', 2);
      });
  
      inject(baz => {
        // Earlier we used foo in a config block that was registered before the
        // override we just did, so it did not have the new value.
        expect(baz.foo).toBe(1);
      });
    });
  
    it('...and config phase does not include provider instantiation!', () => {
      module($provide => {
        $provide.constant('foo', 2);
      });
  
      module(barProvider => {
        expect(barProvider.foo).toBe(1);
      });
  
      inject();
    });
  });

  describe('mocking constants: part 2 (why a second module may not work)', () => {
    // We usually think of there being two lifecycle phases, 'config' and 'run'.
    // But this is an incomplete picture. There are really at least two more we
    // can speak of, ‘registration’ and ‘provider instantiations’.
    //
    // 1. Registration — the initial (usually) synchronous calls to module methods
    //    that define services. Specifically, this is the period prior to app
    //    bootstrap.
    // 2. Provider preparation — unlike the resulting services, which are only
    //    instantiated on demand, providers whose recipes are functions will all
    //    be instantiated, in registration order, before anything else happens.
    // 3. After that is when the queue of config blocks runs. When we supply
    //    functions to ngMock module, it is effectively like calling
    //    module.config() (likewise calling `inject()` is like adding a run block)
    //    so even though we can mock the constant here successfully for subsequent
    //    config blocks, it’s happening _after_ all providers are created and
    //    after any config blocks that were previously queued have already run.
    // 4. After the config queue, the runtime injector is ready and the run queue
    //    is executed in order too, so this will always get the right mocks. In
    //    this phase (and onward) services are instantiated on demand, so $get
    //    methods (which includes factory and service recipes) will get the right
    //    mock too, as will module.decorator() interceptors.
  
    // So how do we mock a value before previously registered config? Or for that
    // matter, in such a way that the mock is available to providers?
    
    // Well, if the consumer is not in the same module at all, you can overwrite
    // the whole module, as others have proposed. But that won’t work for you if
    // the constant and the config (or provider constructor) were defined in app
    // code as part of one module, since that module will not have your override
    // as a dependency and therefore the queue order will still not be correct.
    // Constants are, unlike other recipes, _unshifted_ into the queue, so the
    // first registered value is always the one that sticks.

    angular
      .module('local-mock', [ 'poop' ])
      .constant('foo', 2);
  
    beforeEach(module('local-mock'));
  
    it('should still not work even if a second module is defined ... at least not in realistic cases', () => {
      module((barProvider) => {
        expect(barProvider.foo).toBe(1);
      });
  
      inject();
    });
  });

  describe('mocking constants: part 3 (how you can do it after all)', () => {
    // If we really want to do this, to the best of my knowledge we’re going to
    // need to be willing to get our hands dirty.

    const queue = angular.module('poop')._invokeQueue;

    let originalRecipe, originalIndex;

    beforeAll(() => {
      // Queue members are arrays whose members are the name of a registry,
      // the name of a registry method, and the original arguments.
      originalIndex = queue.findIndex(([ , , [ name ] ]) => name === 'foo');
      originalRecipe = queue[originalIndex];
      queue[originalIndex] = [ '$provide', 'constant', [ 'foo', 2 ] ];
    })

    afterAll(() => {
      queue[originalIndex] = originalRecipe;
    });

    beforeEach(module('poop'));

    it('should work even as far back as provider instantiation', () => {
      module(barProvider => {
        expect(barProvider.foo).toBe(2);
      });
  
      inject();
    });
  });

  describe('mocking constants: part 4 (but be sure to include the teardown)', () => {
    // But that afterAll is important! We restored the initial state of the
    // invokeQueue so that we could continue as normal in later tests.

    beforeEach(module('poop'));

    it('should only be done very carefully!', () => {
      module(barProvider => {
        expect(barProvider.foo).toBe(1);
      });
  
      inject();
    });
  });
});
<!DOCTYPE html>
<html>

  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <script>document.write('<base href="' + document.location + '" />');</script>
    <link href="style.css" rel="stylesheet" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/jasmine.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/jasmine-html.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/boot.js"></script>
    <script src="https://code.angularjs.org/1.6.0-rc.2/angular.js"></script>
    <script src="https://code.angularjs.org/1.6.0-rc.2/angular-mocks.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/jasmine.css">
  </head>

  <body>
  </body>

</html>

Now, you may be wondering why one would do any of this in the first place. The OP is actually describing a really common scenario that Angular + Karma + Jasmine fails to address. The scenario is that there’s some window-provisioned configuration value that determines application behavior — like, say, enabling or disabling ‘debug mode’ — and you need to test what happens with different fixtures, yet those values, typically used for configuration, are needed early on. We can supply these window values as fixtures and then route them through the module.constant recipe to ‘angularize’ them, but we can only do this once, because Karma/Jasmine does not normally give us a fresh environment per test or even per spec. That’s okay when the value is going to be used in the run phase, but realistically, 90% of the time, environmental flags like this are going to be of interest in either the config phase or in providers.

You could probably abstract this pattern into a more robust helper function so as to reduce the chances of messing up the baseline module state.

Halfcocked answered 4/12, 2016 at 7:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.