web-dev-qa-db-ja.com

テストでモジュール構成関数の定数をオーバーライドすることは可能ですか?

モジュールの構成関数に提供された注入された定数をオーバーライドしようとすることに頭を悩ませるのにかなりの時間を費やしました。私のコードは次のようになります

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

ユニットテストでI18nが注入されていることを保証する通常の方法は、

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

これは私のコントローラーでは問題なく機能しますが、config関数はモジュールの外の$providedを調べないようです。モック値を取得する代わりに、モジュールの一部として定義された以前の値を取得します。 (テストの場合は未定義。以下のプランカーでは「foo」。)

動作しているプラ​​ンカーは下にあります(コンソールを見てください)。誰かが私が間違っていることを知っていますか?

http://plnkr.co/edit/utCuGmdRnFRUBKGqk2sD

21
Joe Drew

まず第一に、あなたのプランカーではジャスミンが適切に機能していないようです。しかし、私は確信が持てません–多分誰かがこれを再びチェックすることができます。それにもかかわらず、新しいplunkr( http://plnkr.co/edit/MkUjSLIyWbj5A2Vy6h61?p=preview )を作成し、次の指示に従いました: https://github.com/searls/ jasmine-all

beforeEachコードが実行されないことがわかります。これを確認できます:

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

次の2つが必要です。

  1. it関数の実際のテスト– expect(true).toBe(true)で十分です

  2. テストのどこかでinjectを使用する必要があります。そうしないと、moduleに提供された関数が呼び出されず、定数が設定されません。

このコードを実行すると、「緑」が表示されます。

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

期待どおりに動作することを願っています!

26
michael

基本的な問題は、configブロックの直前に定数を定義しているため、モジュールが読み込まれるたびに、存在する可能性のあるモック値が上書きされることです。私の提案は、定数と構成を別々のモジュールに分離することです。

5
yggie

AngularJS定数が定義された後、それが参照するオブジェクトを変更することはできないようですが、オブジェクト自体のプロパティを変更することができます。

したがって、あなたの場合はI18n他の依存関係と同様に、テストの前に変更します。

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

上記のように、定数の元の状態を保存し、テスト後に復元する必要があります。これにより、このテストが他のユーザー、現在、または将来のユーザーに干渉しないことを確認できます。

4
Michal Charemza

モジュール定義を上書きできます。これをもう1つのバリエーションとして捨てます。

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

モジュールを再定義すると、以前に定義されたモジュールが消去されます。これは、多くの場合、偶然に行われますが、このシナリオでは、(そのようにパッケージ化したい場合)有利に使用できます。そのモジュールで定義されている他のものもすべて消去されることを覚えておいてください。したがって、おそらくそれを定数のみのモジュールにしたいと思うでしょう。これはあなたにとってやり過ぎかもしれません。

3
ProLoser

一連の注釈付きテストとして、より厄介なソリューションについて説明します。これは、状況の解決策ですモジュールの上書きがオプションではない場合。これには、元の定数レシピと構成ブロックが同じモジュールに属している場合や、定数がプロバイダーコンストラクターによって使用されている場合が含まれます。

SO(素晴らしい、これは私にとって新しい!)

仕様後の以前の状態の復元に関する警告に注意してください。 (a)Angularモジュールのライフサイクルを十分に理解しており、(b)他の方法でテストできないことが確実でない限り、このアプローチはお勧めしません。モジュールの3つのキュー(呼び出し、構成、実行)はパブリックAPIとは見なされません一方で、Angularの履歴では一貫しています。

これにはもっと良い方法があるかもしれません—よくわかりません—しかし、これが私がこれまでに見つけた唯一の方法です。

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>

さて、そもそもなぜこれを行うのか疑問に思われるかもしれません。 OPは実際には、Angular + Karma + Jasmineが対処できないという非常に一般的なシナリオを説明しています。このシナリオは、アプリケーションの動作を決定する、ウィンドウでプロビジョニングされた構成値があることです。 「デバッグモード」を無効にする—さまざまなフィクスチャで何が起こるかをテストする必要がありますが、通常は構成に使用されるこれらの値は早い段階で必要です。これらのウィンドウの値をフィクスチャとして指定し、モジュールを通じてルーティングすることができます。定数レシピにそれらを「角度化」しますが、Karma/Jasmineは通常、テストごとまたはスペックごとに新しい環境を提供しないため、これを実行できるのは1回だけです。値が実行フェーズで使用される場合は問題ありませんが、現実的には、このような環境フラグの90%が構成フェーズまたはプロバイダーのいずれかで重要になります。

おそらく、このパターンをより堅牢なヘルパー関数に抽象化して、ベースラインモジュールの状態をめちゃくちゃにする可能性を減らすことができます。

1
Semicolon