web-dev-qa-db-fra.com

Est-il possible de remplacer les constantes des fonctions de configuration des modules dans les tests?

J'ai passé un bon moment à me cogner la tête contre la tentative de remplacer les constantes injectées fournies aux fonctions de configuration des modules. Mon code ressemble à quelque chose

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

Notre façon habituelle de garantir ce que I18n est injecté dans nos tests unitaires est de faire

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

Cela fonctionne bien pour mes contrôleurs, mais il semble que la fonction de configuration ne regarde pas ce qui est $provided en dehors du module. Au lieu d'obtenir les valeurs simulées, il obtient la valeur antérieure définie dans le cadre du module. (Indéfini dans le cas de nos tests; dans le plongeur ci-dessous, 'foo'.)

Un plongeur fonctionnel est ci-dessous (regardez la console); quelqu'un sait-il ce que je fais mal?

http://plnkr.co/edit/utCuGmdRnFRUBKGqk2sD

21
Joe Drew

Tout d'abord: il semble que le jasmin ne fonctionne pas correctement dans votre plunkr. Mais je ne suis pas sûr - peut-être que quelqu'un d'autre pourra vérifier à nouveau. Néanmoins, j'ai créé un nouveau plunkr ( http://plnkr.co/edit/MkUjSLIyWbj5A2Vy6h61?p=preview ) et j'ai suivi ces instructions: https://github.com/searls/ jasmin-tout .

Vous verrez que votre code beforeEach ne s'exécutera jamais. Vous pouvez vérifier ceci:

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

Vous avez besoin de deux choses:

  1. Un vrai test dans la fonction it - expect(true).toBe(true) est assez bon

  2. Vous devez utiliser inject quelque part dans votre test, sinon la fonction fournie à module ne sera pas appelée et la constante ne sera pas définie.

Si vous exécutez ce code, vous verrez "vert":

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

J'espère que cela fonctionnera comme vous vous y attendez!

26
michael

Je pense que le problème fondamental est que vous définissez les constantes juste avant le bloc de configuration, donc à chaque fois que le module est chargé, quelle que soit la valeur fictive qui existe, elle sera remplacée. Ma suggestion serait de séparer les constantes et de configurer en modules séparés.

5
yggie

Bien qu'il semble que vous ne puissiez pas changer à quel objet une constante AngularJS fait référence après avoir été définie, vous pouvez changer les propriétés de l'objet lui-même.

Donc, dans votre cas, vous pouvez injecter I18n comme vous le feriez pour toute autre dépendance, puis modifiez-la avant votre 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;
  });
});

Comme ci-dessus, vous devez enregistrer l'état d'origine de la constante et la restaurer après le test, pour vous assurer que ce test n'interfère pas avec les autres que vous pourriez avoir, maintenant ou à l'avenir.

4
Michal Charemza

Vous pouvez remplacer une définition de module. Je lance juste ceci là comme une variation de plus.

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

La redéfinition d'un module effacera le module précédemment défini, souvent fait par accident, mais dans ce scénario peut être utilisé à votre avantage (si vous avez envie d'emballer les choses de cette façon). N'oubliez pas que tout autre élément défini sur ce module sera également effacé, vous voudrez donc probablement que ce soit un module à constantes uniquement, et cela peut être exagéré pour vous.

3
ProLoser

Je vais parcourir une solution plus méchante sous la forme d'une série de tests annotés. Ceci est une solution pour les situations où l'écrasement de module n'est pas une option. Cela inclut les cas où la recette constante constante et le bloc de configuration appartiennent au même module, ainsi que les cas où la constante est utilisée par un constructeur de fournisseur.

Vous pouvez exécuter le code en ligne sur SO (génial, c'est nouveau pour moi!)

Veuillez noter les mises en garde concernant la restauration de l'état antérieur après la spécification. Je ne recommande pas cette approche sauf si vous (a) avez une bonne compréhension du cycle de vie du module Angular et (b) êtes sûr que vous ne pouvez pas tester quelque chose d'autre. Les trois files d'attente de modules ( invoke, config, run) ne sont pas considérés comme des API publiques, mais d'un autre côté, ils ont été cohérents tout au long de l'histoire d'Angular.

Il y a peut-être une meilleure façon d'aborder cela - je ne suis vraiment pas sûr - mais c'est la seule façon que j'ai trouvée à ce jour.

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>

Maintenant, vous vous demandez peut-être pourquoi on ferait tout cela en premier lieu. L'OP décrit en fait un scénario vraiment courant que Angular + Karma + Jasmine ne parvient pas à résoudre. Le scénario est qu'il existe une valeur de configuration provisionnée par fenêtre qui détermine le comportement de l'application - comme, par exemple, l'activation ou désactiver le "mode de débogage" - et vous devez tester ce qui se passe avec différents appareils, mais ces valeurs, généralement utilisées pour la configuration, sont nécessaires au début. Nous pouvons fournir ces valeurs de fenêtre en tant qu'appareils, puis les acheminer via la recette module.constant vers les "angulariser", mais nous ne pouvons le faire qu'une seule fois , car Karma/Jasmine ne nous donne normalement pas un nouvel environnement par test ou même par spécification. Ce n'est pas grave lorsque la valeur sera utilisée dans la phase d'exécution, mais en réalité, 90% du temps, des indicateurs environnementaux comme celui-ci vont être intéressants soit dans la phase de configuration, soit chez les fournisseurs.

Vous pourriez probablement résumer ce modèle dans une fonction d'assistance plus robuste afin de réduire les risques de perturber l'état du module de base.

1
Semicolon