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

Posted

技术标签:

【中文标题】如何在验收测试中模拟 Ember-CLI 服务?【英文标题】:How to mock an Ember-CLI service in an acceptance test? 【发布时间】:2015-04-09 22:28:40 【问题描述】:

快速总结/tldr:

似乎Ember的容器查找过程+ Ember-CLI的模块解析器不允许手动取消注册服务然后注册替换如果原始服务可以使用resolver解决(我想做的方法described here,但它不起作用) 如何在验收测试中模拟 Ember-CLI 服务而不使用 hacky 的自定义解析器? (example project/acceptance test here)

详解+示例

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

ember generate service logger

services/logger.js

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

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

controllers/application.hs

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

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

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

ember generate acceptance-test application

tests/acceptance/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);

很遗憾,这对 Ember-CLI 不起作用。罪魁祸首是 Ember 容器中的 this line:

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

这是容器查找链的一部分。问题是容器的resolve 方法在检查其内部registry 之前检查resolverapplication.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.

【问题讨论】:

您有解决方法吗?如果是,请分享。谢谢。 这显然是一个已知问题。 Stefan Penner 在他的一个项目 (github.com/stefanpenner/ember-jobs/commit/…) 中创建了一些辅助方法,并且有一些初步工作可以将它们直接集成到 Ember-Cli 中,但这似乎还没有完成:github.com/ember-cli/ember-cli/pull/3306 知道这项工作的状态吗?这将非常有帮助。 【参考方案1】:

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

application.register('service:mockLogger', mockLogger, 
  instantiate: false
);

application.inject('route', 'loggerService', 'service:mockLogger');
application.inject('controller', 'loggerService', 'service:mockLogger');

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

【讨论】:

【参考方案2】:

解决方案的简短版本:您注册的模拟服务必须具有与您尝试模拟的“真实”服务不同的 service:name。

验收测试:

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

【讨论】:

感谢集成示例。正是我需要的。 谢谢!这也是我一直在寻找的东西! 在 Ember 2.1 中我遇到了错误:“this.container.register”不是函数。似乎现在可以通过“this.container.registry.register”和“this.container.registry.injection”访问(注意“registry”属性)。 谢谢。将其添加到示例中。 在 Ember 2.x 中,register 可直接在 this 上使用。所以你可以用this.register 而不是this.container.registry.register【参考方案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 ember twiddle 真是太棒了。 此方法还允许您在另一个服务上替换一个服务。上面的注入方法无法做到这一点。

以上是关于如何在验收测试中模拟 Ember-CLI 服务?的主要内容,如果未能解决你的问题,请参考以下文章

如何在没有嵌入式服务器或新集群的情况下模拟 couchbase 进行验收测试?

验收测试重定向到特定的 URL

用于节点 JS 验收测试的开发 API

软件测试的分类

如何在 Javascript 应用程序中进行冒烟测试和验收测试?

涉及时间触发 azure 功能的验收测试功能