首页 文章

如何在验收测试中模拟Ember-CLI服务?

提问于
浏览
30

快速摘要/ tldr:

  • 似乎Ember 's container lookup process + Ember-CLI'的模块解析器不允许手动取消注册服务然后注册替换,如果可以使用 resolver 解析原始服务(我想做方法described here,但它不起作用)

  • 如何在不使用hacky自定义解析器的情况下在验收测试中模拟Ember-CLI服务? (example project/acceptance test here

详细说明示例

创建注入控制器的新服务:

ember generate service logger

服务/ logger.js

export default Ember.Object.extend({
  log: function(message){
    console.log(message);
  }
});

初始化/ Logger -service.js

export function initialize(container, application) {
  application.inject('route', 'loggerService', 'service:logger');
  application.inject('controller', 'loggerService', 'service:logger');
}

该服务通过其注入名称 loggerService 在应用程序控制器上的操作处理程序中访问:

在控制器中使用该服务

模板/ application.hbs

<button id='do-something-button' {{action 'doSomething'}}>Do Something</button>

控制器/ application.hs

export default Ember.Controller.extend({
  actions: {
    doSomething: function(){
      // access the injected service
      this.loggerService.log('log something');
    }
  }
});

尝试测试此行为是否正确发生

我创建了一个验收测试,检查按钮单击是否触发了服务 . 目的是模拟服务并确定是否在没有实际触发服务实现的情况下调用它 - 这避免了实际服务的副作用 .

ember generate acceptance-test application

测试/接受/应用test.js

import Ember from 'ember';
import startApp from '../helpers/start-app';

var application;
var mockLoggerLogCalled;

module('Acceptance: Application', {
  setup: function() {

    application = startApp();

    mockLoggerLogCalled = 0;
    var mockLogger = Ember.Object.create({
      log: function(m){
        mockLoggerLogCalled = mockLoggerLogCalled + 1;
      }
    });

    application.__container__.unregister('service:logger');
    application.register('service:logger', mockLogger, {instantiate: false});

  },
  teardown: function() {
    Ember.run(application, 'destroy');
  }
});

test('application', function() {
  visit('/');
  click('#do-something-button');
  andThen(function() {
    equal(mockLoggerLogCalled, 1, 'log called once');
  });
});

这是基于mixonic的谈话Testing Ember Apps: Managing Dependency,建议取消注册现有服务,然后重新注册模拟版本:

application.__container__.unregister('service:logger');
application.register('service:logger', mockLogger, {instantiate: false});

不幸的是, this does not work 与Ember-CLI . 罪魁祸首是Ember的容器中的this line

function resolve(container, normalizedName) {
  // ...
  var resolved = container.resolver(normalizedName) || container.registry[normalizedName];
  // ...
}

这是容器的一部分's lookup chain. The issue is that the container' s resolve 方法在检查其内部 registry 之前检查 resolver . application.register 命令使用容器的 registry 注册我们的模拟服务,但是当调用 resolve 时,容器在查询 registry 之前检查 resolver . Ember-CLI使用自定义 resolver 来匹配模块的查找,这意味着它将始终解析原始模块而不使用新注册的模拟服务 . 对此的解决方法看起来很糟糕,并且涉及修改 resolver 以永远不会找到原始服务的模块,这允许容器使用手动注册的模拟服务 .

修改解析器以避免解析为原始服务

在测试中使用自定义 resolver 可以成功模拟服务 . 这通过允许解析器执行正常查找来工作,但是当查找我们的服务名称时,修改后的解析器就像没有与该名称匹配的模块一样 . 这会导致 resolve 方法在容器中查找手动注册的模拟服务 .

var MockResolver = Resolver.extend({
  resolveOther: function(parsedName) {

    if (parsedName.fullName === "service:logger") {
      return undefined;
    } else {
      return this._super(parsedName);
    }
  }
});

application = startApp({
  Resolver: MockResolver
});

这似乎不应该是必要的,并且与上述幻灯片中建议的服务模拟不匹配 . 有没有更好的方法来模拟这项服务?

此问题中使用的ember-cli项目可在this example project on github.找到

3 回答

  • 17

    解决方案的简短版本:您注册的模拟服务必须具有不同的服务:名称而不是您尝试模拟的“真实”服务 .

    验收测试:

    import Ember from 'ember';
    import { module, test } from 'qunit';
    import startApp from 'container-doubling/tests/helpers/start-app';
    
    var application;
    
    let speakerMock = Ember.Service.extend({
      speak: function() {
        console.log("Acceptance Mock!");
      }
    });
    
    module('Acceptance | acceptance demo', {
      beforeEach: function() {
        application = startApp();
    
        // the key here is that the registered service:name IS NOT the same as the real service you're trying to mock
        // if you inject it as the same service:name, then the real one will take precedence and be loaded
        application.register('service:mockSpeaker', speakerMock);
    
        // this should look like your non-test injection, but with the service:name being that of the mock.
        // this will make speakerService use your mock
        application.inject('component', 'speakerService', 'service:mockSpeaker');
      },
    
      afterEach: function() {
        Ember.run(application, 'destroy');
      }
    });
    
    test('visit a route that will trigger usage of the mock service' , function(assert) {
      visit('/');
    
      andThen(function() {
        assert.equal(currentURL(), '/');
      });
    });
    

    集成测试(这是我最初的工作,这导致我的问题)

    import { moduleForComponent, test } from 'ember-qunit';
    import hbs from 'htmlbars-inline-precompile';
    import Ember from 'ember';
    
    
    let speakerMock = Ember.Service.extend({
      speak: function() {
        console.log("Mock one!");
      }
    });
    
    moduleForComponent('component-one', 'Integration | Component | component one', {
      integration: true,
    
      beforeEach: function() {
        // ember 1.13
        this.container.register('service:mockspeaker', speakerMock);
        this.container.injection('component', 'speakerService', 'service:mockspeaker');
    
        // ember 2.1
        //this.container.registry.register('service:mockspeaker', speakerMock);
        //this.container.registry.injection('component', 'speakerService', 'service:mockspeaker');
      }
    });
    
    test('it renders', function(assert) {
      assert.expect(1);
    
      this.render(hbs`{{component-one}}`);
    
      assert.ok(true);
    });
    
  • 7

    您可以注册您的模拟并注入它而不是原始服务 .

    application.register('service:mockLogger', mockLogger, {
      instantiate: false
    });
    
    application.inject('route', 'loggerService', 'service:mockLogger');
    application.inject('controller', 'loggerService', 'service:mockLogger');
    

    我使用这种方法在我的第三方登录验收测试中模拟 torii 库 . 我希望将来有一个更好的解决方案 .

  • 3

    现有的答案效果很好,但有一种方法可以避免重命名服务并跳过注入 .

    https://github.com/ember-weekend/ember-weekend/blob/fb4a02353fbb033daefd258bbc032daf070d17bf/tests/helpers/module-for-acceptance.js#L14并在https://github.com/ember-weekend/ember-weekend/blob/fb4a02353fbb033daefd258bbc032daf070d17bf/tests/acceptance/keyboard-shortcuts-test.js#L13使用

    我将在这里作为我之前在这里的测试助手的更新来呈现它,所以它是替代品,但您可能只想按照上面的链接 .

    // tests/helpers/override-service.js
    // Override a service with a mock/stub service.
    // Based on https://github.com/ember-weekend/ember-weekend/blob/fb4a02353fbb033daefd258bbc032daf070d17bf/tests/helpers/module-for-acceptance.js#L14
    // e.g. used at https://github.com/ember-weekend/ember-weekend/blob/fb4a02/tests/acceptance/keyboard-shortcuts-test.js#L13
    //
    // Parameters:
    // - newService is the mock object / service stub that will be injected
    // - serviceName is the object property being replaced,
    //     e.g. if you set 'redirector' on a controller you would access it with
    //     this.get('redirector')
    function(app, newService, serviceName) {
      const instance = app.__deprecatedInstance__;
      const registry = instance.register ? instance : instance.registry;
      return registry.register(`service:${serviceName}`, newService);
    }
    

    加上https://guides.emberjs.com/v2.5.0/testing/acceptance/#toc_custom-test-helpers执行jslint和帮助程序注册步骤

    然后我可以这样调用它,在我的例子中省略了一个重定向(window.location)服务,我们想要这样做,因为重定向中断了Testem:

    test("testing a redirect's path", function(assert) {
      const assertRedirectPerformed = assert.async();
      const redirectorMock = Ember.Service.extend({
        redirectTo(href) {
          assert.equal(href, '/neverwhere');
          assertRedirectPerformed();
        },
      });
    
      overrideService(redirectorMock, 'redirector');
      visit('/foo');
      click('#bar');
    });
    

相关问题