如何在接受测试中模拟Ember-CLI服务?

31

快速总结/tldr:

  • 看起来Ember的容器查找过程+Ember-CLI的模块解析器不允许手动取消注册一个服务,然后注册一个替换服务,如果可以使用resolver解析原始服务(我想要执行这里描述的方法,但不起作用)
  • 如何在接受测试中模拟一个Ember-CLI服务,而不使用hacky、自定义的解析器?(此处有示例项目/接受测试

详细说明+示例

创建一个新的服务,并将其注入到控制器中:

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访问该服务:

在控制器中使用该服务

templates/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

测试/接受/应用测试.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});

不幸的是,这在Ember-CLI中不起作用。罪魁祸首是Ember容器中的这行代码

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

这是容器查找链的一部分。问题在于容器的resolve方法在检查其内部的registry之前会先检查resolver。当使用resolve调用容器时,容器首先查询resolver,而不是内部的registry。Ember-CLI使用自定义的resolver来匹配模块,这意味着它始终会解析原始模块,而不使用新注册的模拟服务。解决此问题的方法看起来很糟糕,涉及修改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项目可以在GitHub上的这个示例项目中找到。


你解决这个问题了吗?如果是,请分享一下解决方法。谢谢。 - Bilal Budhani
1
这显然是一个已知的问题。Stefan Penner在他的一个项目中创建了一些辅助方法(https://github.com/stefanpenner/ember-jobs/commit/5b027e78a3980f14067d6237e41e86994fe9019a#diff-9c6c3a9ea701f78a2f4140aa824db351),并且有一些初步的工作将它们直接集成到Ember-Cli中,但似乎还没有完成: https://github.com/ember-cli/ember-cli/pull/3306 - CraigTeegarden
1
这项工作的进展状况你有没有了解?这将会非常有帮助。 - les2
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);
});

感谢提供集成示例。正是我所需要的。 - jrjohnson
谢谢!这正是我也在寻找的东西! - Ken Hirakawa
2
使用Ember 2.1时,我遇到了错误:“this.container.register”不是一个函数。现在似乎通过“this.container.registry.register”和“this.container.registry.injection”(注意“registry”属性)进行访问。 - sammy34
谢谢。已将其添加到示例中。 - ArtHare
在 Ember 2.x 中,register 直接可用于 this 上。因此,您可以使用 this.register 而不是 this.container.registry.register - PSWai
使用 Ember Qunit 4.2.0 或更高版本时,您需要使用 this.owner 而不是 this.application - Bill Brower

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和helper注册步骤后,我可以像这样调用它,在我的示例中存根重定向(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');
});

你能再详细解释一下吗?如果每个步骤都有一个Ember Twiddle的话,那就太棒了。 - j4v1
该方法还允许您将一个服务替换为另一个服务。这是使用上面的注入方法无法实现的。 - gordonc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接