Как насмехаться над сервисом Ember-CLI в приемочном тесте?
Краткая сводка /TL;DR:
- Кажется, что процесс поиска контейнера Ember + модуль-преобразователь Ember-CLI не позволяет вручную отменить регистрацию службы, а затем зарегистрировать замену, если исходная услуга может быть разрешена с помощью
resolver
(я хочу сделать метод описанный здесь, но он не работает)
- Как я могу издеваться над сервисом Ember-CLI в приемочном тесте без использования хакерского, настраиваемого разрешения? (пример проекта/приемочный тест здесь)
Подробное объяснение + пример
Создайте новую службу, которая вводится в контроллер:
ember generate service logger
услуги/logger.js
export default Ember.Object.extend({
log: function(message){
console.log(message);
}
});
инициализаторы/Регистратор-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');
});
});
Это основано на разговоре Тестирование Ember Apps: Управление зависимостью от 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
, чтобы никогда не находить оригинальный сервисный модуль, который позволяет контейнеру использовать зарегистрированную вручную mock-службу.
Измените 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.
Ответы
Ответ 1
Краткая версия решения: ваша зарегистрированная макетная служба должна иметь другую службу: имя, чем "реальный" сервис, который вы пытаетесь высмеять.
Приемочный тест:
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);
});
Ответ 2
Вы можете зарегистрировать свой макет и ввести его вместо первоначальной службы.
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);
}
Плюс выполнение шагов регистрации jslint и помощников из https://guides.emberjs.com/v2.5.0/testing/acceptance/#toc_custom-test-helpers
Тогда я могу называть его так, в моем примере, который запустил службу перенаправления (window.location), которую мы хотим сделать, поскольку перенаправление перерывов Testem:
test("testing a redirect 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');
});