Вы застряли в асинхронной отладке в своих тестах? Я расскажу об одном варианте использования, включая то, как выглядели симптомы, основную проблему и решения. Эта статья размещена в моем личном блоге, поскольку у Medium есть серьезные проблемы с доступностью.

Все примеры кода относятся к Ember 3.x.

Симптомы

Сегодня я работал над исправлением следующего устаревшего Ember Data:

УСТАРЕНИЕ: Попытка вызвать store.serializerFor (), но экземпляр хранилища уже был уничтожен. [устаревший идентификатор: тлеющие-данные: вызов-метода-при-уничтоженном-хранилище]

Везде, где мог, я ставил проверки типа this.store.isDestroyed, чтобы я мог найти и пропустить проблемный код, но это не помогло. Он всегда оказывался ложным и поэтому бесполезным для моей отладки.

Я подумал, может быть, это какая-то странная штука с Ember Data, и мне стоит разобраться в этом подробнее.

Спойлеры - это не было странной штукой с Ember Data.

Я попросил помощи в чате сообщества Ember, и Крис Тоберн, он же Runspired, помог мне.

Я почти всегда оставляю с ним разговоры, думая, что мне нужно написать об этом в блоге.

Проблема

Вот что происходило.

В моем приемочном тесте была утечка, что означало, что некоторый асинхронный код разрешался после того, как тест уже прошел и сбросил некоторые ресурсы для следующего теста.

Этот процесс возврата теста к «чистому списку» называется «разборкой теста» в Ember и других средах JavaScript.

В моем случае ловушка afterEach, уничтожившая хранилище, произошла до того, как код был фактически выполнен.

Однако вы можете увидеть то же самое, если используете this.anything после этапов разборки при тестировании.

В поисках причины

Сам по себе тест был довольно нормальным и использовал помощники асинхронного тестирования Ember, как и предполагалось:

test(‘track field dirtiness in owned, related records’, async function(assert) {
  await visit(‘/hub/posts/1’);
  let reviewStatusActionTrigger = findTriggerElementWithLabel.call(this, /Comment #1: Karma/);
  await click(reviewStatusActionTrigger);
  let karmaInput = findInputWithValue.call(this, ‘10’);
  await fillIn(karmaInput, ‘9’);
  assert.dom(‘[data-test-cs-version-control-button-save=”false”]’).exists(‘Save button is enabled’);
  assert.dom(‘[data-test-cs-version-control-button-cancel=”false”]’).exists(‘Cancel button is enabled’);
});

Тестовый код делал то, что должен. Крис подтвердил, что причиной проблемы был код моего приложения, а не проблема с данными Ember или этот тест. Но где это могло скрываться?

Моя первая стратегия заключалась в том, что я комментировал части теста, пока не выяснил, какое именно взаимодействие вызвало проблему. Что-то случилось после выполнения fillIn.

Я следил за кодом, чтобы увидеть, какие действия были активными.

Оказывается, компонент использовал службу для обработки fetch данных, а затем использовал сериализатор данных Ember для анализа ответа:

async validate(model) {
  let responses = this.toValidate.map(async (record) => {
    let { url, verb } = this._validationRequestParams(record);
    let response = await fetch(url, {
      method: verb,
      headers: this._headers(),
      body: JSON.stringify(record.serialize())
    });
  });
let json = await Promise.all(responses);
  // do more work
}

Оскорбительная строка была record.serialize(), для которой нужны данные Ember, и мои тесты не ждали результата.

Исправляем

Я мог придумать три возможных пути вперед:

  • Заглушить сервис
  • Заставьте тесты подождать (как ???)
  • Проверьте среду Ember в моем коде приложения (ew)

В этом случае проще всего было заглушить службу.

Когда вы «заглушаете» услугу, вы создаете фальшивую услугу, которую использует тест.

Фактический код для службы, используемой компонентом, составлял более 200 строк, но компонент использовал только три функции из него!

Чтобы заглушить службу, нужно выполнить всего несколько шагов:

1. Переместите этот сложный тест в отдельный файл

В моем приемочном тесте было много небольших тестов, но только в одном были проблемы с асинхронностью. Мне не хотелось заглушать каждую служебную функцию, используемую в каждом из этих тестов, поэтому я перенес тест запуска устаревания в отдельный файл.

Я могу запустить этот тест один с ember test --server --module “Acceptance | your test name”. Модуль - это то, что указано в тесте: module(‘Acceptance | your test name’, function(hooks) {…}).

В качестве альтернативы, чтобы запустить свой тест в одиночку, я могу использовать ember test --server --filter “some term”, где в строке test(‘here is some term’, async function(assert) {…} находится какой-то термин.

--server означает, что мои тесты будут автоматически запускаться повторно, когда я вношу изменения, и это выполняется намного быстрее, чем повторное выполнение ember test вручную.

2. Создайте заглушку приемочного испытания

Затем я сделал свой фальшивый сервис.

Я посмотрел на компонент, чтобы узнать, какие функции он использует.

Я изучил эти функции, чтобы увидеть, какие данные они должны возвращать, и консоль регистрировала то, что они возвращали при запуске теста.

Асинхронные методы возвращают обещание, а другие методы возвращают данные, которые тест должен отобразить.

Пример ниже - Ember 3, но если вы используете старую версию Ember, эта другая статья может помочь вам с заменой сервисов. Игнорируйте всплывающие окна. Платного доступа нет, просто раздражает то, что делает Medium.

// my stubbed service
let StubCardstackDataService = Service.extend({
  validate() {
    return Promise.resolve({})
  },
  getCardMeta() {
    return 'Comment #1'
  },
  branches() {
    return []
  },
  fetchPermissionsFor() {
    return Promise.resolve({mayUpdateResource: false, writableFields: ['karmaValue', 'karmaType']})
  }
})

3. Добавьте тест в ловушку beforeEach.

Эта специальная функция запускается перед каждым тестом. Вместо того, чтобы использовать мою настоящую службу cardstack-data, компонент будет использовать мою поддельную службу при запуске теста. Для этого мне нужно зарегистрировать свою службу.

hooks.beforeEach(function() {
this.owner.register(‘service:cardstack-data’, StubCardstackDataService);
});

Убедитесь, что информация, которую вы даете register(), совпадает с названием службы, используемой в Компоненте.

Окончательный результат

Если вы хотите увидеть, как заглушка используется в контексте всего теста, посмотрите итоговый PR здесь!

Выводы

Из этого опыта я понял одну вещь: если я пишу компоненты с асинхронным режимом выборки данных, размещение этого кода в Сервисе дает огромное преимущество. Таким образом, я могу легко заглушить его в своих тестах!

Это не единственный способ справиться с этой проблемой - помните, я выделил 3 варианта ранее, и их, вероятно, больше.

Другие мои выводы заключаются в том, что всякий раз, когда я могу, я должен полагаться на Ember Data и обработчик модели для выполнения запросов данных. Придерживание «счастливого пути» в Ember означает, что вы получаете некоторую асинхронную обработку данных бесплатно. Я работал над проектом, в котором нет хуков модели, потому что в нем нет файлов маршрутов (странно, правда?), Но в обычном приложении Ember доступно больше функций обработки асинхронности!

Для получения дополнительной информации

Вот некоторые ресурсы, которые я нашел полезными при обучении и отладке. Спасибо этим авторам за отличную работу!

Спасибо!

Спасибо за чтение и особенно Крису Тоберну за помощь!

Об авторе

Джен Вебер - веб-разработчик и писатель из Бостона, США (она / она или они / они). Как член Ember Core Team, она работает над кодом, документацией и взаимодействует с участниками фронтенд-фреймворка Ember.js. Джен работает в Cardstack, где помогает создавать инструменты для работы с Web 3.0. Она является поклонником открытого исходного кода и превращения технологий в более гостеприимную и инклюзивную отрасль.