При работе над более крупным проектом вы можете столкнуться с проблемой постоянно растущего набора тестов, которые со временем начинают работать медленнее на вашем сервере непрерывной интеграции (CI). У меня возникла эта проблема во время работы над проектом в Ruby on Rails, где тесты RSpec на CircleCI занимали около 15 минут.

Поскольку это меня беспокоило, я решил что-то с этим сделать, что привело к созданию библиотеки гемов Knapsack Ruby с открытым исходным кодом (название происходит от задачи о рюкзаке), которая занимается распределением тестов между параллельными серверами CI. В этой статье вы узнаете о двух подходах к разделению тестов на параллельных серверах непрерывной интеграции - статическом и динамическом.

Если в вашем проекте есть набор тестов, выполнение которого на сервере CI занимает около десятка минут, а может быть, даже несколько часов, вы знаете, насколько это неудобно для программистов. Когда вы работаете над какой-то новой функцией и отправляете новый коммит git в репозиторий, вам придется долго ждать, пока ваш CI-сервер не выполнит CI-сборку.

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

Проблема с запуском параллельных тестов на CI-сервере

Чтобы ускорить выполнение сборки CI, вы можете использовать параллелизм на сервере CI, то есть запуск нескольких параллельных машин CI (контейнеров CI, например, в Docker), где каждый параллельный сервер будет выполнять часть набора тестов. Однако существует проблема с тем, какие тесты следует запускать на каких серверах (узлах CI), чтобы их распределение было достаточно равномерным и вам не приходилось ждать узла CI, который является узким местом.

Ниже вы можете увидеть пример неоптимального распределения тестов на 4 серверах CI, где второй сервер, отмеченный красным, является узким местом, поэтому время ожидания завершения всей сборки CI составляет до 20 минут.

Оптимальное распределение тестов на параллельных CI-серверах

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

Ниже вы можете увидеть пример оптимального распределения тестов, где каждая параллельная машина CI выполняет тесты в течение 10 минут, благодаря чему вся сборка CI длится всего 10 минут, а не 20, как в предыдущем примере.

Статическое разделение тестов детерминированным способом

Один из способов определить, как разделить тесты между параллельными машинами на сервере CI, чтобы каждый сервер выполнял тесты в одно и то же время, - это использовать измеренное время выполнения файлов в наборе тестов. Это был первый подход, который я реализовал в геме Knapsack Ruby.

После измерения времени выполнения теста мы можем назначить отдельные тестовые файлы между параллельными серверами CI, чтобы убедиться, что сборка CI не имеет узких мест.

С помощью библиотеки ранца вы можете запускать тесты для многих средств выполнения тестов на Ruby, таких как RSpec, Minitest, Cucumber, Spinach и Turnip. Используя среду выполнения тестов, гем Knapsack может создать список тестов, которые будут выполняться на конкретном узле CI.

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

Проблема со статическим разбиением тестов

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

Например, тесты с использованием браузера могут иметь колебания во времени выполнения (тесты в Capybara в Ruby или тесты E2E в JavaScript).

Проблема также растет в зависимости от того, какой CI-сервер вы используете. Обладает ли каждая из параллельных машин CI одинаковой производительностью или они совместно используют ресурсы, такие как ЦП или ОЗУ? Контейнер CI работает в общей среде? Если узел CI перегружен, наши тесты, конечно, могут быть медленнее.

Кроме того, возникнут проблемы с тем, запускаются ли все параллельные машины в одно и то же время или нет. Если вы приобрели пул параллельных серверов CI, его может использовать кто-то другой, например другая сборка CI из текущего проекта или другого проекта из вашей организации.

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

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

Сплит динамических тестов

Решение вышеуказанной проблемы заключается в динамическом разделении тестов между параллельными машинами в рамках одной сборки CI. Это проблема, над которой я работал в последние годы, создав библиотеку Knapsack Pro и режим очереди для Ruby и JavaScript с поддержкой нескольких популярных средств запуска тестов, таких как Jest или Cypress.

Идея проста. У нас есть набор тестов, которые ставятся в очередь на сервере Knapsack Pro. Отдельные параллельные машины CI используют очередь с Knapsack Pro API, пока она не закончится. Благодаря этому тесты оптимально распределяются между CI-серверами, помогая избежать узкого места в виде перегруженного (слишком медленного) CI-сервера. Ниже вы можете увидеть пример:

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

Посмотрите, как работает разделение динамического набора тестов в режиме очереди для Knapsack Pro.

Реализация Knapsack Pro на Ruby и JavaScript

Knapsack Pro имеет встроенную поддержку многих популярных CI-серверов. Это также независимый инструмент CI, поэтому вы можете использовать любой сервер CI. Все, что вам нужно сделать, это настроить команду Knapsack Pro для каждого параллельного сервера CI, работающего в рамках одной сборки CI. Ниже вы можете увидеть общий пример того, как config YAML может искать CI-сервер с Knapsack Pro:

Выводы

Knapsack Pro поддерживает Ruby и несколько средств запуска тестов на JavaScript, таких как Jest и Cypress, но есть планы добавить поддержку дополнительных средств запуска тестов и языков программирования. Я хотел бы услышать, что вы используете для тестирования приложений и какие серверы CI. Вы можете связаться со мной в LinkedIn, а более подробную информацию об описанном решении вы можете найти на KnapsackPro.com. Надеюсь, эта статья была вам полезна. :)

Первоначально опубликовано на https://docs.knapsackpro.com 2 февраля 2020 г.