Как мы ускоряли е2е-тесты на Cypress в GitLab

80f62638bf605ce8a61db4c77e03d94f.png

Всем привет! На связи Николай Мезинов, разработчик фронтенда в продуктовой команде DevPlatform. Хочу поделиться опытом, как мы ускоряли прохождение e2e-тестов на Cypress в пайплайнах GitLab.

Зачем нам Cypress

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

Направления, которые составляют контекстНаправления, которые составляют контекст

Мы стараемся объединить все пункты в одном разработчике, чтобы увеличить качество и скорость поставки. На выходе получается сильный инженерный продукт, который разрабатывается небольшой командой из 12 человек. 

Чтобы поддерживать качество продукта, мы используем интеграционные и unit-тесты. А e2e-тесты на Cypress работают как последний рубеж для тестирования всей системы целиком от лица конечного пользователя.

Фреймворк Cypress выбрали из-за его простоты. У него много возможностей и плагинов «из коробки», а также большое сообщество.

Сначала работа с репозиторием e2e-тестов была простой и приятной для контрибьюторов, пока не начали блокироваться релизы. Это произошло потому, что любое новое приложение в нашей системе должно было пройти все е2е-тесты, иначе кнопка «Релиз» не активировалась в пайплайне GitLab. Накопилось большое количество критических сценариев и увеличилось количество тестов. Это стало для нас бутылочным горлышком и блокером релизов.

Тесты в Cypress проходят последовательно — и такой порядок замедляет их прохождение. Пайплайны в GitLab построены так, что деплой в продакшен недоступен, пока не пройдут e2e-тесты. Мы ждали по 25—30 минут, чтобы нажать кнопку «Deploy prod». А если по какой-то причине один из тестов падал, приходилось делать «Retry» и снова ждать.

Запуск тестов и параллелизация

Перед параллелизацией расскажу, как запускаем e2e-тесты. Мы используем Node.js-проект, а команды для запуска находятся в блоке scripts в файле package.json.

"scripts": {
   "e2e": "cypress run --browser chrome --reporter mocha-multi-reporters --reporter-options configFile=cypress-reporters.json"
 }

Нужна определенная структура папок, чтобы писать тесты на Cypress. Сами тесты хранятся в папке ./cypress/integration, вспомогательные утилиты — в папке ./cypress/support. Когда запускаем команду cypress run, Cypress начинает последовательно гонять все тесты из папки ./cypress/integration. Прогнать только часть файлов можно с помощью опции spec. В ней указываем, какие тесты хотим запустить:

yarn e2e --spec "./cypress/integration/first-test.spec.ts,./cypress/integration/third-test.spec.ts"

Запустить все тесты и равномерно распределить их по разным нодам можно с помощью инструкции parallel. Она нужна для того, чтобы запускать одну и ту же джобу несколько раз параллельно.

e2e:
  image: some-image-for-cypress
  stage: e2e
  parallel: 4
  script:
    - yarn e2e

Запуск джоб с опцией parallelЗапуск джоб с опцией parallel

При этом для каждой джобы в рантайме дополнительно доступны 2 ENV-переменные: CI_NODE_INDEX и CI_NODE_TOTAL. Индекс текущей ноды CI_NODE_INDEX начинается с единицы, а не с нуля, что может быть непривычно для языков программирования. Благодаря этим ENV-переменным появляется гибкость, которая позволяет на разных нодах запускать разный набор тестов. Остается определить, какой набор тестов необходимо прогонять на каждой ноде, чтобы потом дополнить конфигурацию джобы:

 e2e:
   image: some-image-for-cypress
   stage: e2e
   parallel: 4
   script:
     - yarn e2e --spec "$specs" 

Осталось определить список тестов, помеченных как переменная $specs. Мы отказались от Bash, потому что даже простой алгоритм на нем становится трудночитаемым.

Мы используем инструмент Zх, который позволяет использовать Bash-инструкции в JavaScript-like коде. К тому же «из коробки» доступно несколько полезных функций, которые не нужно заранее подключать. Подробнее о возможностях Zx можно прочитать в официальной документации на GitHub. Покажу алгоритм с пояснениями, чтобы продемонстрировать все, о чем рассказал.

#!/usr/bin/env zx
const nodeTotal = Number(process.env.CI_NODE_TOTAL) || 1; // Количество нод, на которых параллельно запускаются тесты
const nodeIndex = Number(process.env.CI_NODE_INDEX) || 1; // Индекс текущей ноды (индексация в GitLab начинается с 1)
 
// Найдем все тесты, утилита globby доступна «из коробки» Zx
const allSpecs = await globby('./cypress/integration/*.spec.ts');
 
// Из списка всех тестов определим те, что будут запущены на текущей ноде
const specsForCurrentNode = [];
 
for (let i = 0; i < allSpecs.length; i++) {
  if (i % nodeTotal === nodeIndex - 1) {
    specsForCurrentNode.push(allSpecs[i]);
  }
}
 
// Объединим найденные тесты в строку, соединенную запятой, так как в таком формате ожидает опция --spec
const specsForCurrentNodeString = specsForCurrentNode.join(',');
 
// Выведем результат в консоль
console.log(specsForCurrentNodeString);

С помощью несложного цикла мы определили, какие тесты будут запущены на текущей ноде. Такой скрипт будет запускаться на каждой ноде, и для каждой сформируется свой список тестов. При этом все тесты будут распределены максимально равномерно. Теперь остается доработать конфигурацию джобы в GitLab.

e2e:
  image: some-image-for-cypress
  stage: e2e
  parallel: 4
  script:
    - npm i -g zx
    - specs=$(./scripts/get-specs-for-current-node.mjs)
    - yarn e2e --spec "$specs"

В каждой джобе запускается свой набор тестов, что позволяет существенно ускорить прохождение пайплайна.

e2e-тесты до и после параллелизацииe2e-тесты до и после параллелизации

Вместо заключения

До параллелизации тесты гонялись примерно 25—30 минут, на иллюстрации выше приведен пример прогона тестов за 28 минут, после параллелизации тесты на разных нодах гоняются от 4 до 11 минут. Так в нашем конкретном случае мы ускорили прохождение тестов примерно в 2,5 раза.

При увеличении опции parallel можно добиться еще большего ускорения, но оно уже не будет столь значительным. Спасибо, что дочитали статью. Если есть вопросы — готов ответить в комментариях.

© Habrahabr.ru