web-dev-qa-db-ja.com

受け入れテストでEmber-CLIサービスをモックする方法は?

要約/ tldr:

  • Emberのコンテナールックアッププロセス+ Ember-CLIのモジュールリゾルバーでは、resolverを使用して元のサービスを解決できる場合、サービスを手動で登録解除してから交換を登録できないようです(メソッドを実行したい ここで説明 ですが、機能しません)
  • ハッピーなカスタムリゾルバを使用せずに、受け入れテストでEmber-CLIサービスをモックするにはどうすればよいですか? ( ここにサンプルプロジェクト/受け入れテスト

詳細説明+例

コントローラに注入される新しいサービスを作成します。

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

これは、 Testing Ember Apps:Managing Dependency によるトークに基づいています。mixonicは、既存のサービスの登録を解除してから再登録することを推奨しています。モックバージョン:

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メソッドが内部のresolverをチェックする前にregistryをチェックすることです。 application.registerコマンドは、モックされたサービスをコンテナーのregistryに登録しますが、resolveが呼び出されると、コンテナーはresolverを照会してからregistryを照会します。 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プロジェクトは、githubの このサンプルプロジェクトにあります。

31
CraigTeegarden

ソリューションの短いバージョン:登録したモックサービスには、モックしようとしている「実際の」サービスとは異なる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);
});
17
ArtHare

モックを登録して、元のサービスの代わりにインモックすることができます。

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

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

私はこのアプローチを使用して、サードパーティのログイン受け入れテストでtoriiライブラリをモックしています。将来的にはもっと良い解決策があることを願っています。

7
kunerd

既存の回答はうまく機能しますが、サービスの名前を変更せずにインジェクトをスキップする方法があります。

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#L1

これを、以前ここにあったテストヘルパーの更新としてここに提示します。これはドロップインの置き換えですが、代わりに上記のリンクをたどることができます。

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