Автоматизация микрофронтендов, или как в Тинькофф тестируют библиотеки компонентов
Кажется, уже сложно представить себе веб-приложение, которое не использует микрофронтендную архитектуру для возможности реализовать гибкое и функциональное приложение. И как в любом архитектурном подходе, в микрофронтенде необходимо обеспечивать качественное тестирование разрабатываемых компонентов.
Но с чего стоит начать и что ждет каждого, кто вступит на путь автоматизации микрофронтенда, когда многие привыкли тестировать уже собранные из кусочков приложения?
Привет. Меня зовут Александр Воробей, я ведущий специалист по автоматизации тестирования в Тинькофф. В этой статье я постараюсь вкратце рассказать, с чего мы начинали автоматизацию микрофронтенда, с какими проблемами встретились и какие результаты получили.
Статья основана на моем докладе с конференции Heisenbug. Она состоит из двух разделов:
- Рассказ о предмете тестирования и выборе инструментов.
- Автоматизация: способы, проблемы и их решения.
Те, кто хочет как можно скорее узнать о способах тестирования микрофронтенда, могут смело переходить к разделу «Автоматизация тестирования микрофронтенда».
Позвольте начать с рассказа о самом предмете тестирования — микрофронтенде в Тинькофф. По этой ссылке можно почитать что такое микрофронтенд в общих чертах.
До перехода на микрофронтенд все используемые компоненты — формы, блоки с текстами, иконками и изображениями — находились в одном репозитории и над всеми компонентами работали разные команды. Сейчас почти все компоненты (или даже все) разрабатываются разными командами, подключаются каждый по отдельности через пакеты или другие технологии. Далее речь пойдет об одном из таких компонентов — о сложных формах и их составляющих.
Сложными я их называю по той причине, что, кроме выполнения стандартной функции отправки введенных данных на сервер, форма:
- Сохраняет состояние введенных данных. Попробуйте ввести данные первого шага формы для получения кредитной карты и потом нажать F5 — форма предложит вам заполнить данные с того же момента. Но работает это, только если введен номер телефона.
- Состоит не из одного шага, а из нескольких, связанных между собой.
- Отправляет множество запросов на разные действия. Вводите ФИО — отправляются запросы, переходите между шагами — отправляются запросы, восстановили форму — отправляются запросы.
- Отправляет разные системные метрики.
И таких форм в Тинькофф более 20 штук для разных продуктов с разным поведением (это только для неавторизованной зоны, на самом деле их гораздо больше). Все они имеют одни и те же строительные блоки: инпуты, кнопки, наборы полей, плагины и разные экраны. Поэтому, кроме форм, существует микрофронтенд под названием Form Builder, основной ролью которого является поставка более мелких компонентов для форм. Поэтому дальше речь пойдет о формах и Form Builder
Раньше тестировщики проводили регресс только собранного приложения. Например, изменилась кнопка или набор полей — ее могли проверить только в собранном приложении. В микрофронтенде нет собранного приложения — есть только компоненты, из которых он состоит. В связи с этим небольшие микрофронтенды могут релизится чаще, так как они меньше по объему и имеют меньшее количество зависимостей, а соответственно регресс для конкретного микрофронтенда может проводиться чаще, возможно даже несколько раз в день. Поэтому первой и основной целью является автоматизация регресса тестирования.
Немаловажно и то, что сегодня хорошей практикой считается хранение автоматизированных тестов рядом с кодом приложения. Мы также последовали этой практике, но поняли, что поддерживать такие тесты в любом случае придется разработчикам, которые раньше не имели особого опыта в автоматизации тестирования UI.
Поэтому второй целью стало построение простой и удобной инфраструктуры тестирования. А третьей — привлечение разработчиков к написанию автотестов.
В результате имеем три цели:
- Полный регресс.
- Простая инфраструктура.
- Привлечение разработчиков.
Большинство QA-инженеров привыкли к тестированию собранного приложения. В микрофронтенде, как я уже говорил, есть только компоненты, которые нужно как-то отображать и уметь проверить функционал.
При анализе инструментов, позволяющих отображать каждый из компонентов в разных состояниях, мы выбрали Storybook.
Внешний вид стенда с использованием Storybook:
На стенде имеется:
- Список Stories — этот термин используется по отношению к отображаемым компонентам. То есть каждый компонент в сторибуке — это Story.
- Сам компонент, который отображается в том состоянии, которое ему задали.
- Панель с Knobs — на ней отображается список всех возможны props, которые можно изменить и в live-режиме посмотреть, как компонент себя поведет.
- Код сториса — над самим компонентом располагается кнопка, позволяющая открыть код сториса, который можно использовать у себя в приложении.
В результате этот инструмент позволил сделать:
- Демостенд, в котором каждый член команды (дизайнер, аналитик, продакт-менеджер, разработчик, тестировщик) может пощупать компонент, меняя ему доступные состояния.
- Стенд для ручного тестирования, где тестировщик, меняя состояние компонента, тестирует его поведение.
- Стенд для автотестов. Автотесты открывают компоненты через сторибук в разных браузерах.
Вот главные требования к инструменту:
- Возможность писать тесты в одном стиле, используя разные движки — Puppeteer, WebdriverIO (далее wdio). Wdio был необходим для запуска тестов в Firefox, Safari и IE 11.
- Возможность из коробки сгенерировать подробный отчет о тестировании в Allure Report.
- Простота понимания для функциональных тестировщиков.
Выбор пал на инструмент CodeceptJS. Так как данный инструмент позволяет, написав один раз тест, запускать его на любом движке — будь то Puppeteer, WebDriverIO, Appium или даже новый Playwright, — подключив который не придется переписывать тесты.
Пример синтаксиса:
Scenario('Должен перевалидировать документ тачки, когда меняется др', async (I) => {
await I.amOnStory(stories('url'))
await I.fillField('Дата рождения', '01.01.1990')
await I.fillField('Серия и номер ВУ', '1111111111')
await I.fillField('Дата выдачи текущего ВУ', '01.01.2008')
await I.fillField('Год выдачи первого ВУ', '2008')
await I.clearFieldBySymbols('Дата рождения', 1)
await I.appendFieldManual('Дата рождения', '9')
await I.seeFieldErrorMessage('Дата выдачи текущего ВУ')
await I.seeFieldErrorMessage('Год выдачи первого ВУ')
})
Для тестировщиков такой синтаксис прост в понимании, и они могут самостоятельно писать тесты. При этом по результатам теста генерируется понятный Allure-отчет, который содержит и каждый шаг, и время выполнения шага, и разные аттачментсы.
Почему не использовали другие инструменты?
- Cypress. На тот момент, когда мы выбирали инструмент, Cypress не имел кроссбраузерности, а также до сих пор не умеет в разные вкладки (а нам в некоторых случаях нужны были кейсы по работе с вкладками).
- Wdio. Во-первых, у нас много работы с запросами, а Wdio не умел явно перехватывать запросы и переопределять ответы. А во-вторых, основные пользователи тестов — это разработчики и не хотелось для них придумывать инфраструктуру, которая требует дополнительных утилит в виде селениума. Но для тестирования в браузерах кроме «Хрома» он был нужен.
- Puppeteer. Нам он всячески подходил, но не умел в другие браузеры. Именно поэтому нам стало важно писать тесты, используя и Wdio, и Puppeteer.
Собственно в решении наших потребностей помог CodeceptJS.
В первую очередь важно отметить, что для CI/CD мы используем Gitlab, соответственно стенд и тесты запускаются на каждый мерж-реквест, создаваемый в Gitlab, тем самым проверяя качество кода.
Так как одной из целей было привлечение разработчиков к покрытию автотестами кода, а разработчики уже согласились писать тесты на выбранных инструментах, мы решили, что для более качественного контроля за написанием тестов необходимо внедрить тестировщика в процесс ревью мердж-реквеста.
Но на самом деле нам хватило несколько таких ревью, и мы поняли, что это только замедляет разработку.
Почему? Потому что в этот момент разработчик сам придумал тесты на свой код, реализовал их. И, когда проходило ревью тестировщиком, он находил кучу проблем с тестами, которые нужно дорабатывать. Согласитесь, не каждый разработчик будет рад ждать заново пайплайна только для того, чтобы поправить пару тестов. Кроме того, поздно написанные тесты увеличивали время жизни МРа.
Поэтому решением возникшей проблемы стало то, что в процессе мы вернули тестировщика на ранний этап после создания задачи, где основной целью тестировщика является написание тест-кейсов. В результате, когда разработчик реализует новый функционал или исправляет дефект, у него уже есть тест-кейс, по которому нужно написать автотест, и больше не тратится время на переписывание тестов.
Дальше — интереснее. Дальше — автоматизация!
В этом разделе я рассмотрю несколько пунктов:
- Тестирование верстки компонентов с использованием сторибука. И тут же — ускорение тестирования.
- Тестирование форм в пайплайне Form Builder`а (в библиотеке, которая поставляет формам мелкие компоненты в виде инпутов и кнопок).
- Тестирование внутри браузера. Ускорение тестирования в несколько раз.
- Тестирование только изменившегося кода.
Поехали.
Сторибук нас устроил не только тем, что он является отличным демостендом, но и тем, что с его помощью можно качественно и удобно построить автоматизацию тестирования.
Начну с одного из важных, на мой взгляд, способов тестирования компонентов — с тестирования верстки компонентов.
В основном все компоненты имеют какие-то состояния, которые достаточно проверить визуально: например, размер кнопок или их цвет, или состояние чек-бокса, или состояние заполненного инпута. Но для того, чтобы тестировать верстку, необходимо иметь актуальный стенд (тестируемый), на котором будет сниматься скриншот, и эталонный скриншот, с которым мы можем сравнить.
Обычно эталонные скриншоты держат внутри репозитория, используя LFS. Но мы решили, что не хотим так, потому что хранение эталона в репозитории влечет за собой необходимость поддержки этих эталонов в актуальном состоянии, а при локальном обновлении скриншотов и тестировании обязательно запускать в докер-контейнере (чтобы не было эффектов от использования разных ОС) и, естественно, поддержка всего этого. А еще мы не хотели писать тесты на каждый новый компонент при тестировании верстки, нам нужно было автоматическое решение.
Так как мы используем Storybook, то все необходимые компоненты хранятся в нём. При этом в нашем случае есть два вида Storybook:
- Master Storybook — такой Storybook собирается на мастер ветке, после того как в неё влили мерж реквест. И соответственно в нём находятся самые актуальные компоненты.
- Merge Request Storybook — а этот Storybook собирается исключительно на мерж реквесте.
Учитывая что в Master Storybook находятся самые актуальные компоненты, их же можно использовать как эталонные скриншоты. И получается что мы избавляемся от необходимости хранить эталоны внутри git репозитория.
Чтобы автоматически тестировать верстку без участия разработчиков, нам пришлось покопаться в возможностях сторибука, где мы обнаружили, что можно получить список всех существующих компонентов в сторибуке, используя его API — window.__STORYBOOK_CLIENT_API.getStorybook()
. Но важной особенностью данного API стало то, что он доступен только в собранном сторибуке. То есть только если мы переходим явно на урл сторибука, открываем консоль и вводим там. В nodejs этот API не доступен.
Но решением оказался следующий код:
const html = fs.readFileSync(`${normalizedPath}/iframe.html`, { encoding: 'utf8' });
const htmlDom = new JSDOM(html);
const scripts = htmlDom.window.document.querySelectorAll('script');
scripts.forEach((script) => {
if (script.src && script.src.endsWith('bundle.js')) {
require(path.resolve(normalizedPath, script.src));
}
});
window.__STORYBOOK_CLIENT_API__.getStorybook().forEach((story) => {...})
Так как собранный сторибук — это набор js-файлов, представленный код с помощью конструкции require внедряет собранный js-код внутрь самой node, в результате чего из node теперь есть доступ к API сторибука.
Имея данную возможность, мы можем сформировать список компонентов и урлов к ним.
С учетом этих данных был написан скрипт, который генерирует автотесты на CodeceptJS, следующего вида:
Входными данными для такого теста является список компонентов. Алгоритм теста следующий:
- Открывает по сформированному урлу компонент на тестируемом сторибуке.
- Делает скриншот.
- Открывает по сформированному урлу компонент на мастер сторибуке (эталоне).
- Делает скриншот.
- Сравнивает полученные скриншоты.
Всё. Больше ничего не нужно делать.
Но не все было так гладко — этот подход оказался достаточно медленным.
Вот пример:
42 компонента тестировались 3 минуты. Согласитесь, это слишком долго!
Причина была в том, что для переходов между компонентами иногда тратилось от 2 до 14 секунд. Причиной могли быть перегруженные раннеры, долгий рендер компонентов.
Решением стало опять же API сторибука, которое появилось в более поздних версиях — window.__STORYBOOK_CLIENT_API__._storyStore.setSelection()
. Этот метод позволяет переключать сторисы, используя их id, без необходимости перезагружать страницу — прямо в момент работы сторибука.
Изменив способ перехода между компонентами, скорость выполнения тестов стала значительно привлекательнее — за то же самое время, 3 минуты, мы стали тестировать 554 компонента:
Результаты:
- Тесты генерируются автоматически. Если добавлен новый компонент — для него не нужно писать новые тесты.
- Нет необходимости хранить эталонный скриншот внутри репозитория и поддерживать его в актуальном состоянии.
- Достаточно высокая скорость тестирования!
Ранее я упоминал, что у нас используется 20+ форм. И при каком-либо изменении функционала в Form Builder мы достаточно поздно узнавали об ошибках, так как новая версия Form Builder могла быть использована через несколько дней. Для ускорения обратной связи мы придумали запускать тесты ключевых форм внутри пайплайна Form Builder.
Для реализации данной идеи нам потребовалось немного усилий:
- git-репозиторий формы;
- пакетный менеджер yarn.
Мы сделали джобу, внутри которой выгружали репозиторий формы через git clone. Дальше перемещались в директорию формы и с помощью команды yarn link form-builder прилинковывали текущую версию From Builder, которая хранится локально. А потом запускали тесты на Happy-path этой формы.
В результате наши разработчики могли узнавать о проблемах внутри форм еще до релиза библиотеки Form Builder, не беспокоясь, что в формах сломается основной функционал.
Со временем жизни проектов From Builder и форм количество функциональных тестов начало значительно расти и время jobs с тестами уже переваливало за 30 минут, а время всего пайплайна могло перевалить и за 50 минут. Естественно, для нас это стало проблемой, и мы искали решение уменьшить скорость.
Для решения мы поняли, что нам нужно нарушить одну цель, к которой мы шли, — запуск одного теста с использованием любого драйвера (Puppeteer или Wdio). Мы попробовали запускать тесты внутри браузера, используя инструмент karma.
Что значит запуск внутри браузера?
При использовании Puppeteer или Wdio взаимодействие с браузером происходит через протоколы CDP (Chrome DevTools Protocol) и WebDriver Protocol соответственно. На примере Puppeteer можно взять метод type, входящий в состав его API. В официальной документации к этому методу написано: "Sends a keydown, keypress/input, and keyup event for each character in the text". То есть Puppeteer посылает как минимум три запроса через протокол DevTools браузеру на каждый вводимый символов. Это достаточно хорошо имитирует поведение пользователя, но скорость тестов из-за этого все равно увеличивается.
Подход при тестировании в браузере (с использованием karma) заключается в следующем:
- Все написанные тесты перед запуском собираются в некий index.html с тестами внутри (например, с помощью webpack пример по ссылке).
- Запускается сервер, который начинает раздавать этот файл на localhost.
- При переходе на localhost, где раздаются тесты, сразу же запускаются тесты прямо внутри страницы.
- При этом тесты возможно писать с использованием Web API.
В результате в проекте Form Builder была переписана часть тестов на использование karma, что позволило ускорить тестирование компонентов с 70 тестов за 3 минуты до 366 тестов за 50 секунд.
До:
После:
Недостатки данного подхода:
- Все действия выполняются через DOM (Web API), то есть нет той важной имитации действий, которые имеются в Puppeteer или Wdio.
- Не умеет работать с браузером (вкладками). Но на самом деле у нас большая часть тестов все же не нуждалась в работе с браузером, поэтому данный подход отлично отрабатывал.
- Не умеет делать скриншот. А зачем? У нас есть для этого другие инструменты.
- Не умеет работать с ОС — не будет возможности взять файл из ОС и приаттачить к полю.
- Не имитирует события клавиш. Придется делать dispatch-события для каждого элемента.
Но если ничего из этого списка вам и не нужно, можно смело пользоваться инструментом и получать удовольствие от скорости тестирования.
И последняя тема, о которой хочется рассказать, — это анализ изменившихся файлов и запуск соответствующих тестов.
Спустя какое-то время мы поняли, что нет смысла нагружать наши машины лишним тестированием, если изменилась только одна кнопочка. Представляете, раньше мы запускали 1000 тестов, в состав которых входят чек-боксы, инпуты, радио при исправлении какого-нибудь незначительного дефекта в кнопке. В этом нет смысла.
Поэтому мы написали инструмент, похожий на lerna, который умеет анализировать, какие файлы были изменены, соответственно, какой компонент изменится и какие зависящие от этого компоненты могут быть затронуты. На основе этой информации решаем, какой набор тестов запускать.
Например:
- Разработчик внес изменения в файл пакета инпута.
- От этого инпута наследуется компонент инпут-календарь, который оборачивает базовый инпут и добавляет свой функционал.
- Для инпута и календарь-инпута есть свои набор тестов.
- Так вот, в результате будут запущены только тесты на инпут и календарь-инпут, а тесты на кнопку и чек-бокс будут просто пропущены, так как изменения на них 100% не повлияли.
При этом данный подход используется как для тестирования верстки в сторибуке — собирается сторибук только с измененными компонентами, так и для функциональных тестов и даже unit-тестов.
Результат:
- Меньше тестов запускаем — быстрее проходит пайплайн.
- Хотели полный регресс — получили тестирование только изменившихся компонентов.
При использовании описанных мною подходов к автоматизации тестирования микрофронтенда в Тинькофф получили следующие результаты:
- Разработчики участвуют в покрытии автотестами разрабатываемого микрофронтенда.
- Автотесты пишутся для каждого компонента отдельно.
- Тесты запускаются по-умному — только на изменившиеся компоненты.
- Быстро и качественно тестируется верстка микрофронтенда.
- Функциональные тесты писать просто, они проходят быстро и не страшно поменять движок с Puppeteer на Playwright.
- Узнаём о сбоях в производных микрофронтендах еще на этапе разработки core микрофронтенда.