【问题标题】:Is it possible to override constants for module config functions in tests?是否可以在测试中覆盖模块配置函数的常量?
【发布时间】:2014-01-29 21:25:15
【问题描述】:

我已经花了很长时间反对尝试覆盖提供给模块配置函数的注入常量。我的代码看起来像

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

这适用于我的控制器,但配置函数似乎没有查看模块外部的 $provided 是什么。它没有获取模拟值,而是获取定义为模块一部分的早期值。 (在我们的测试中未定义;在下面的 plunker 中,'foo'。)

下面是一个工作的 plunker(查看控制台);有谁知道我做错了什么?

http://plnkr.co/edit/utCuGmdRnFRUBKGqk2sD

【问题讨论】:

  • 常量被设计成你不能改变它们
  • 当然,但这适用于控制器,但不适用于配置功能。如果常量根本无法改变,那它根本就不能工作,对吧?
  • javascript 本身没有 const,所以 angular 必须使用 const 的唯一方法是:他们不会为 const 定义 $watch。所以不会反映 const 的变化。所以你可以做的是,将 const 定义为对象而不是属性,并根据需要使用 const 值。顺便说一句,这样做破坏了 const 的真正含义。就像在 c# 中一样,一旦你定义了 const,即使是为了测试你也不会改变它......
  • 我必须诚实地同意@Conner。你刚刚发现了一个 AngularJS 的错误,你让它工作。

标签: angularjs angularjs-module


【解决方案1】:

首先:jasmine 在您的 plunkr 中似乎无法正常工作。但我不太确定——也许其他人可以再检查一次。尽管如此,我还是创建了一个新的 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"});
});

你需要两件事:

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

我希望它会像你期望的那样工作!

【讨论】:

  • 我正在与@joe-drew 合作;是否需要为每个it 调用inject,或者它可以作为beforeEach 的一部分吗?
  • 这没有回答问题。 plunker 中的配置块仍使用原始值。 OP 希望通过注入不同的常量值来测试配置块
【解决方案2】:

虽然在定义 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;
  });
});

如上所述,您应该保存常量的原始状态,并在测试后将其恢复,以确保此测试不会干扰您现在或将来可能拥有的任何其他状态。

【讨论】:

    【解决方案3】:

    您可以覆盖模块定义。我只是把它作为另一种变体扔出去。

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

    重新定义一个模块会清除之前定义的模块,这通常是在意外情况下完成的,但在这种情况下可以为您所用(如果您想以这种方式打包东西)。请记住,在该模块上定义的任何其他内容也将被清除,因此您可能希望它是一个仅限常量的模块,这对您来说可能有点矫枉过正。

    【讨论】:

    • 欣赏答案,但这并不能回答问题。同样,问题在于测试使用常量的配置块
    • 它确实可以满足您的需求。请查看修改
    【解决方案4】:

    我认为根本问题是您在配置块之前定义常量,因此每次加载模块时,可能存在的任何模拟值都将被覆盖。我的建议是将常量和配置分离到单独的模块中。

    【讨论】:

    • 感谢您的回答...我尝试在这里将常量分离到它自己的模块中:plnkr.co/edit/QykUj9ChZePJOoRS1CMQ,正如您所看到的,配置块仍在使用原始值
    • 问题是两件事的结合,你没有正确地模拟常量(即在配置块之前)并且测试本身没有运行(因此没有模拟发生)。我使用@michal 的 Jasmine 模板修改了您的示例:plnkr.co/edit/ucj1bJyxPURyKsZLIKSF?p=preview
    • 为了进一步详细说明,您第一次看到注销的是由于ng-app 指令而导致的模块实例化,这可能是导致混淆结果的原因
    • 啊,我明白我做错了什么。非常感谢您的帮助!
    【解决方案5】:

    我将通过一系列带注释的测试来介绍一个更糟糕的解决方案。这是针对无法覆盖模块的情况的解决方案。这包括原始常量配方和配置块属于同一模块的情况,以及提供者构造函数使用常量的情况。

    您可以在 SO 上运行内联代码(太棒了,这对我来说是新的!)

    请注意有关在规范之后恢复先前状态的注意事项。我不推荐这种方法,除非你们 (a) 对 Angular 模块生命周期有很好的理解,并且 (b) 确定不能以任何其他方式测试某些东西。三个模块队列(调用、配置、运行)不被视为公共 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 无法解决的一个非常常见的场景。场景是,有一些窗口配置的配置值决定了应用程序的行为——比如,启用或禁用“调试模式”——你需要测试不同的夹具会发生什么,但这些值通常用于配置,是需要的早期。我们可以将这些窗口值作为固定装置提供,然后通过 module.constant 配方将它们路由到“角度化”它们,但我们只能这样做一次,因为 Karma/Jasmine 通常不会给我们一个新鲜的每个测试甚至每个规范的环境。当值将在运行阶段使用时没关系,但实际上,在 90% 的情况下,像这样的环境标志将在配置阶段或提供程序中感兴趣。

    您可能可以将此模式抽象为更强大的辅助函数,以减少搞乱基线模块状态的机会。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-03-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-04-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多