Pull to refresh

Comments 15

Pixel perfect больше не в моде? А если у меня некоторые блоки подгружаются по fetch и встраиваются с анимацией при этом время подгрузки и окончательный рендеринг не всегда заканчивается в одно время, что зависит от межсерверного трансфера? Например Page Insights даже не всегда выдает скриншот до конца отрендеренным и жалуется на анимацию, как на время задержки рендера. Раньше в PP просто наложил PNG и сверяешь — предложенный инструмент корректно работает с динамическими интрефейсами? Например можно ли указать вызов модального окна при тестировании?

Постараюсь разбить текст на отдельные вопросы. Надеюсь не ошибусь и не пропущу ничего


Pixel perfect больше не в моде?

А зачем следовать моде? Нужно подходить к вопросу здраво:


  • Вашему проекту или вашим клиентам важен pixel perfect?
  • Дизайнеры сразу делают адаптивные решения, которые идеально живут на реальных устройствах пользователей?

Если да — возьмите инструмент для pixel-perfect сравнения вёрстки и макетов. Возьмите storybook и сделайте историю на каждое состояние макета, найдите/напишите свой аддон для pixel-perfect сравнения скриншота с эталонной картинкой из photoshop.


А если у меня некоторые блоки подгружаются по fetch и встраиваются с анимацией при этом время подгрузки и окончательный рендеринг не всегда заканчивается в одно время, что зависит от межсерверного трансфера?

Можно:


  1. Отделить бизнес-логику от верстки. Тогда компоненты можно ставить в любые ситуации и проверять как они выглядят, прокидывая правильные пропсы
  2. Поднять стаб-сервер который будет отдавать всё быстро. Или использовать вместо стаб сервера msw, который зашивает мок сети в servive worker
  3. Подменить в тестовом окружении (jest\storybook) слой, который делает запросы, чтобы запросы возвращали данные из фикстур и моментально
  4. Настроить инструмент для скриншотов (loki/creevey) так, чтобы они дожидались конца запроса. loki.js из коробки дожидается конца всех асинхронных взаимодействий, если мне память не изменяет.

Зависит от того, что вы хотите проверить. Анимацию, процесс загрузки, конечное состояние, все сразу?


Например можно ли указать вызов модального окна при тестировании?

creevey позволяет писать полноценные тесты в рамках сторибука (с кликами по кнопкам, вводом данных в инпуты, ожиданиями и тп). В других инструментах, как правило, есть способ сказать ранеру, когда история готова к скриншоту.


Либо можно написать функциональный тест на cypress/testcafe/selenium и там сделать скриншот в удобный вам момент.
Можно на уровне пропсов выделить состояние "модалка открыта" и сделать скриншот с открытой модалкой, а в функциональном тесте убедится что модалка открывается по клику.

Интеграционные тесты позволяют проще рефакторить внутренности разных модулей… Очень спорное утверждение… По факту функциональный тест из статьи зависит и от разметки (h3, div), и от контента («Найти»), и от endpoint url ('api/search'). А мы проверили всего один простейший сценарий.

По своему опыту могу сказать, что такого рода тесты (функциональные, интеграционные, e2e) очень хрупкие. Ломаются неожиданно, фиксятся не очевидно, всякие проблемы с await waitRerendering(), await waitMounting(), setTimeout. И писать моки и pageObjects не радует, это сложные низкоуровневые манипуляции, как их не пряч. Даже переиспользование утильных функций для тестов в перспективе ни к чему хорошему не приведёт.

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

Выделяйте модель, пишите на все кейсы юниты, пишите один-два интеграционных (e2e), чтобы убедиться, что модель к компоненту приклеилась ровно, и пейте кофе.

Никак в разработке не убежать от навыков проектирования. Если у вас проблемы с юнитами, значит вы что-то делаете неправильно. А обмазывание снепшотами, e2e тестами, переворачивание пирамид — это самоуспокоение.

В общем, заранее извиняюсь, фигню какую-то написал, удалять жалко, а настроение холиварное

Пример юнит тестов
interface SearchApi {
  search(term: string): Promise<SearchResult>;
}

interface SearchResult {}

class SearchPresenter {
  pending: boolean = false;

  result?: SearchResult;

  constructor(private api: SearchApi) {}

  async search(term: string): Promise<void> {
    try {
      this.pending = true;
      this.result = await this.api.search(term);
    } finally {
      this.pending = false;
    }
  }
}

describe(SearchPresenter.name, () => {
  describe(SearchPresenter.prototype.search.name, () => {
    it('should resolve result', async () => {
      const result = {} as SearchResult;
      const searchPresenter = new SearchPresenter({ search: async () => result });
      await searchPresenter.search('term');
      expect(searchPresenter.result).toBe(result);
    });

    it('should change pending', async () => {
      const searchPresenter = new SearchPresenter({ search: async () => ({} as SearchResult) });
      const searchPromise = searchPresenter.search('term');
      expect(searchPresenter.pending).toBe(true);
      await searchPromise;
      expect(searchPresenter.pending).toBe(false);
    });

    it('should set pending to false on error', async () => {
      const searchPresenter = new SearchPresenter({ search: () => Promise.reject({}) });
      await searchPresenter.search('term').catch(() => {});
      expect(searchPresenter.pending).toBe(false);
    });

    it('should preserve prev result on error', async () => {});

    it('should use last search term on double call', async () => {});
  });
});


По факту функциональный тест из статьи зависит и от разметки (h3, div), и от контента («Найти»), и от endpoint url ('api/search').

Можно сказать и так. По факту тест и должен зависеть от каких-то внутренних имлпементация. Ведь мы хотим, чтобы если мы изменили что-то важное (например url ендпоинта), то тест бы упал. Я не хотел бы опираться на h3, div, "Найти", но пока тестовым фреймворкам нельзя сказать "найди там кнопку, максимально похожую на сабмит, и кликни её", приходится писать как минимум текст этой кнопки.


Я всего-лишь предлагаю не описывать такие вещи (h3, div, "Найти", url) явно в тесте и не делать низкоуровневые тесты. Тогда и тесты будут читаемые, и при правках этих признаков страницы (h3, div, "Найти", endpoint) не придется менять тест, достаточно будет поменять только pageObject, который обновится во всех тестах разом.


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

Я в докладе упоминал, что термин unit — очень неудачный. Для меня, мои функциональные тесты — это юнит-тесты. Но я вокруг себя никого в этом убедить не могу, все говорят, что это интеграционные тесты.


Если у вас проблемы с юнитами, значит вы что-то делаете неправильно. А обмазывание снепшотами, e2e тестами, переворачивание пирамид — это самоуспокоение.

"Если ваша система хорошо покрывается тестами — значит ваша система хорошо спроектирована" — это очень популярный миф.


Есть и обратное мнение, что юнит-тесты не нужны:



Это только те ссылки, которые я без проблем могу найти в своей коллекции ссылок на raindrop.io.


К сожалению, мир тестирования чуть сложнее, чем "юниты-интеграционные-e2e". Если бы мы представляли тестирование в виде пирамиды, это определенно была бы как минимум 3D пирамида, а не простой треугольник.


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


Известные проблемы низкоуровневых тестов:


  • мешают рефакторить (делать изменения кода без изменения поведения). В вашем примере я не могу заменить булевый pending на state: 'pending' без изменения тестов. Хотя при функциональных тестах я мог бы поменять эту часть реализации и быть уверенным, что если тесты проходят — значит я совершил безопасный рефакторинг. С рефакторингами сложнее чем изменение поля, например выделение из одной сущности двух разных сущностей, низкоуровневые тесты можно смело выкидывать
  • Низкоуровневных тестов нужно писать много. Кроме самого поведения, как минимум на каждую связь двух модулей нам нужны тесты взаимодействия этих сущностей. Как следствие предыдущего пункта, их нужно будет исправлять при нулевых, с точки зрения функционала, изменениях.
  • Они дают слабые гарантии работы приложения.

В реальности же часто бывает, что сложность написания низкоуровневых тестов и функциональных тестов одинакова. Т.е. нам часто ничего не стоит вместо прямого вызова метода какой-нибудь сущности, переписать немного arrange, act и assert фазы теста так, чтобы мы кликали на кнопку и проверяли html/вызов spy'ев.


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

В последних проектах, в которых я работал, у меня всегда были отделены данные от view. Я всегда мог написать кучу юнит-тестов (тест на сервис, тест на стор, тест на компонент, тест на коннектор, тест на отсылку метрик), но это всегда было нецелесообразно, мешало разработке и не гарантировало работу фичи. В результате экспериментов всегда получалось придти к ситуации "пишем меньше тестов, но уверенности в работе кода больше" и другие разработчики с радостью это перенимали.


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


Главное в этом деле:


  • думать своей головой
  • не упарываться
  • писать тесты. Юниты, e2e, плохие, хорошие — любые, пока они приносят вам пользу.
  • эксперементировать, чтобы найти наилучшие способы тестирования функционала в вашем контексте/проекте.
Спасибо за развернутый ответ! Да, интеграционные тесты игнорирует некоторый слой деталей реализации. Это можно использовать во благо.

найди там кнопку, максимально похожую на сабмит, и кликни её


Я использовал атрибуты testId=«sumbit-button».

Я использовал атрибуты testId=«sumbit-button».

и к этому тоже есть альтернативное мнение, что не нужно использовать data-test-id для всего подряд.


tldr: пользователи не видят ваших data-test-id, они оперируют кнопками, заголовками и текстами — оперируйте и вы.


По моему опыту, не сильно важно использовать data-test-id или сразу матчится на текст/label/etc в html. Сделать любой из этих селекторов очень просто и занимает очень мало времени, относительно проектирования теста и написания кода.
Хотя для компонентов больше 3х контролов, я обычно описываю контролам data-test-id

Для мока API есть mirage.js. Не привязана к какой-то конкретной библиотеке, будет работать и с axios и с голым fetch. Однажды довелось попользоваться по причине ещё не готовой апишки на проекте, впечатления положительные

Выглядит круто!


Из непривязанных к библиотеке я знаю msw и nock, но сам использую обычно nock. Надо попробовать mirage.js.

Максим, а не поделитесь примером стека, на котором у вас все это работает? Начиная с версии ноды думаю даже. Просто предпринимаю 2-ю попытку привнести все это в наш Vue JS проект и терплю фиаско — какие-то странные проблемы возникают — банально вот сейчас не работает автоматический резолвинг расширений в импортах при сборке storybook — ну казалось бы…

Имел пару лет назад опыт настройки окружения тестирования для vue.js@2 и могу сказать, что работа с экосистемой vue.js — это путь боли и страдания :)


К сожалению, не могу подсказать, как сейчас обстоят с этим дела в vue.js. 2 года назад я использовал связку vue-test-utils + jest и storybook/vue. Кажется в обоих случаях пришлось напилиньком дорабатывать конфиги jest и storybook.

все-таки завел я эту машинерию — действительно пришлось сильно с webpack повозиться.

Вот Вы упомянули vue-test-utils и jest — подозреваю как-то они использовались, чтобы мокать глобальные параметры/запросы в stories. Не поделитесь опытом как это делается? А то storybook/vue lack of documentation и сколько ни рыл просторы инета по этой казалось бы основнополагающей теме — безрезультатно( Если есть пример кода где манкипатчится какой-нибудь глобальный Vue параметр для конкретной истории (чтобы это не затрагивало остальные) — вот был бы прям реально благодарен)
Вот Вы упомянули vue-test-utils и jest — подозреваю как-то они использовались, чтобы мокать глобальные параметры/запросы в stories

Они использовались для тестов логики (клик на кнопку => сделали запрос => обработали ответ)


Не поделитесь опытом как это делается?

Сниппетов кода на Vue под рукой у меня нет, а с Vue я уже давненько не работал и не помню точно, как там все делается. Но сам подход не привязан к какому-то конкретному фреймворку, попробую описать.


Допустим, у нас есть фича, которая использует данные о cookie и делает http запрос. В этом случае, для того чтобы протестировать верстку в storybook мы можем:


Разделить логику и представление


Вынести знания об использовании cookie и делании http запроса из компонента. По сути вынести логику из компонента. Это можно вынести в Vuex, но насколько я знаю Vuex — это уже не торт и сейчас торт — хуки. В любом случае, мы можем разделить сущность на 2: компонент, который рендерит верстку и что-то, что отвечает за логику (другой компонент или стор+компонент, суть которого — взять данные и колбеки из стора и прокинуть их в наш рендер-компонент). В этом случае наш рендер-компонент будет чистым, а тут задача со сторибуком решена.


image


Плюсы:


  • легко получаем любые возможные состояния компонента.
    Минусы:
  • логику нужно тестировать иначе.

Выделить сущности CookieManager и HttpClient и провайдить их через контекст


Мы выделяем явные сущности для доступа к cookie и деланию запросов: CookieManager и HttpClient, которые мы достаем в компоненте из контекста/глобальной переменной. В приложении мы это делаем с помощью AppContextProvider (заполняет какой-то контекст), а для storybook историй пишем специальный HoC, который умеет заполнять тот же контекст моками (в случае глобального контекста делаем это перед mount). Таким образом в каждой истории перед маунтом мы будем подменять сущности на нужные моки и истории будут изолированы.


Структура в приложении
image


Структура в storybook
image


Минусы:


  • глобальный контекст — это не очень здорово
  • может быть сложнее получать нужные для теста состояния компонента

Хардкорные моки
Мы не выделяем никаких сущностей, а сразу по хардкору мокаем сеть, например с помощью msw или любого другого решения. Ну и cookie чем нибудь мокаем.


Плюсы:


  • Чуть больше уверенности в работе всего кода

Минусы:


  • может быть нетривиально и неудобно
Спасибо за столь развернутый ответ)
Таким образом в каждой истории перед маунтом мы будем подменять сущности на нужные моки и истории будут изолированы.

Вопрос мой как раз про сей момент, на самом деле. Как там запросы мокать и компонент с минимумом логики оформлять (без походов в сеть) — тут все понятно, не впервой. Вот как кастомный контекст подсунуть во Vue компонент, в котором оный как раз представлен в виде глобальной переменной — это не получается пока осилить. vue-test-utils позволяет в mount передавать mocks и при написании юнит тестов мы эту проблему решаем ровно таким образом. Вот в случае со storybook/vue не припоминаете что-нибудь подобное?

Готовых решений не знаю. Я бы на коленке попробовал бы сделать микроплагин в котором свойство будет прокидываться как в Vuex. Насколько помню, в vuex плагине при инициализации инстанса компонента он копирует себе ссылку на $store от родителя.


Там что-то типа


if (this.parent.$store) {
  this.$store = this.parent.$store;
}

Можно также сделать и провайдить моки на верхнем компоненте истории

В общем, разобрался я — есть же декораторы в Storybook и в них контекст каждой истории приезжает — в случае с storybook/vue это выглядит так:
.addDecorator((storyFn, constext) => {
  ...
  return storyFn(context);
})

С помощью контекста выясняем что за история сейчас будет показываться (св-во name) ну и формируем нужный нам глобальный контекст:
if (context.name === 'Story with special state') {
  Vue.use({
    install: (vueObj) => {
      vueObj.prototype.$globalProp = ...;
    }
  });

Решение далеко от идеала, конечно, потому что аффектит все инстансы Vue, создаваемые после такого инджекта. По-хорошему бы восстанавливать исходный стэйт после отработки истории — наверняка и такое есть, будем копать дальше.

В любом случае спасибо за ликбез)
Sign up to leave a comment.