Интеграционные, e2e и юнит-тесты: как мы пришли к кластеру для запуска 1500 интеграционных тестов
Автоматическое тестирование
Сибирикс
Автоматическое тестирование
Интеграционные, e2e и юнит-тесты: как мы пришли к кластеру для запуска 1500 интеграционных тестов
Мы уже как-то писали обзор систем управления тест-планами. Сегодня расскажем о новом экспириенсе: как разгоняли автотесты и за счёт чего нам удалось сократить время их прохождения почти втрое.
Владимир
CEO & Founder
Опыт, который мы собрали по автотестам — это опыт хождения по граблям. Мы собрали все возможные косяки и грабли, и есть подозрение, что радиусе 250 км ни у кого подобного опыта нет. То что получили в итоге, то как смогли разогнать тестирование — это шикарный экспириенс. Интеграционные тесты по критическим путям — это то, что стоит проделывать, но абсолютно на всех проектах делать юнит-тесты — очень дорого. У нас сейчас 2 человека фулл-тайм занимаются только тестами.

Что тестировали

SingularityApp — планировщик задач, основанный на идеях хаос-менеджмента. Написан на Electron и React. На момент экспериментов с автотестами в нашем стеке были: GitLab CI/СD, Jest, Spectron (WebDriver) и Robot.js.

Автотестами мы проверяли приложение по трём параметрам:
  1. проектирование возможного поведения пользователя,
  2. проверка на баги, в том числе чтобы исключить возможные регрессии — внезапные повторы уже исправленных багов,
  3. проверка заявленных возможностей у несистемных модулей (большую часть которых писали сами).

Как было сначала и что пошло не так

Мы тестировали приложение на нескольких компьютерах с MacOS — вместе они объединялись в кластер. На каждом запускался раннер в шелл-режиме (создавалась невиртуальная среда). Раннер — это программа от GitLab, отвечающая за поочередный запуск — «прогон» — задач.

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

Чтобы запараллелить работу раннера и программиста на одном компьютере, мы создали две разных учетных записи пользователей: раннер вынесли в отдельный сеанс, а кодер работал, зайдя под другим пользователем. И тут началось «веселье».

Проблема 1 — тесты стали жить своей жизнью

Предполагалось, что процесс написания кода и тестирования будет идти параллельно. Однако очень сложно писать код, когда GitLab предлагает раннерам в то же самое время потестировать приложение. Поверх IDE (интегрированной среды разработки) выскакивает окно тестируемого приложения и в темпе вальса начинает совершать какие-то действия с мышью и клавиатурой.
Денис
Разработчик scrum-студии Сибирикс
Хоть убейте, как это происходит — загадка. Либо хитрость MacOS, либо хитрость тестовых фреймворков. Вопрос, конечно, интересный, но проблему можно было решить другой «серебрянной пулей» — запуском тестов в виртуальной среде.

Проблема 2 — тратилось очень много ресурсов

Так как раннер и программист работают на одном компьютере, они невольно отбирают друг у друга ресурсы. Если раннер от такого вряд ли расстроится (максимум — завалит тест), то у специалиста знатно пригорает, когда все окружение начинает дико тормозить.

Ожидалось, что тест запустит приложение, после чего он должен присоединиться к процессу приложения и начать выполнять в нем кейс. Однако из-за разных факторов он не всегда мог получить доступ к процессу. Тест по факту падал, но приложение уже было запущено, и тестовое окружение его не контролировало. Следом автоматически запускался следующий тест, и там картина повторялась — в итоге было уже два приложения, которые не контролировало тестовое окружение. Если это вовремя не отследить, получалась такая картина:
Какого-то простого способа ограничить ресурсы раннеру в шелл-режиме мы применить не смогли. Поэтому решили настроить докеры.

Решение 1 — Docker вместо шелл-режима

Докер создаёт в операционной системе компьютера обособленную среду, которая убирает часть проблем (например — фокус-покус появления приложения поверх IDE программиста). Но по факту докеры также работают параллельно с написанием кода, и также отбирают ресурсы — а значит, мешают работе кодеров. Плюс в том, что в докере можно выставить лимиты на забор ресурсов для раннеров (чтобы те не сжирали всю-всю память или процессор в случае проблем при исполнении тестов).

Проблему с нехваткой ресурса решили, докупив ещё несколько новых мощных серверов на Linux, заточенных только под тестирование и собрав из них кластер. Фактически теперь тестирование шло на одних компьютерах, а написание кода — на других. Итого, за счёт докеров и нескольких новых (ТОЛЬКО) тестовых компьютеров мы суммарно смогли сократить время тестов на треть.
Но возникла ещё одна параллельная проблема — Flow (модель командной работы): мы делали много фич в одной ветке и отправляли на тест. Поначалу ситуация нас почти устраивала — тестов было немного, и мы смирились с некоторыми недостатками, поскольку решать их было затратно по времени.

Но мирились мы до тех пор, пока очередь задач с тестами не выросла настолько, что не могла рассосаться даже за ночь. Ночью, как известно, все программисты спят и ожидаемо снижают нагрузку на GitLab. А у нас есть повод надеяться, что к утру все накопленные тесты пройдут. Ну-ну. В итоге время одного прогона тестов опять возросло до часа и более, а самих прогонов могло быть 10−12. Умножайте.
лето сочи
Динамика автотестов за год. Успешно пройдённые тесты — зелёным
Денис
Разработчик scrum-студии Сибирикс
При взгляде на график прохождения тестов складывается впечатление, что у нас успешно проходит 20−30% тестов, однако это не так. Каждый зеленый пункт означает, что после внесения изменений в проект он прошел абсолютно все тесты. Если завалился хотя бы один тест из полутора тысяч — значит, успешная статистика в пролете. У нас 20 наборов (пачек) тестов, в пачке может быть до 70 тестов — если упадет хоть один, то вся итерация тестирования приговаривается к «серости».
Из-за такой модели работы при возникновении проблемы в каком-то куске кода на графике прохождения всё становилось красным — и было непонятно, из-за какой именно фичи всё сломалось. Это усиливало уже описанные проблемы — больше запусков для отдельных фич, больше очередь на прохождение тестов. Поэтому Flow пришлось пересмотреть — мы перешли к последовательному тестированию: сделали фичу, проверили, довели до зеленого состояния, приступаем к другой.

Решение 2 — эксперименты с видами тестирования

По классике: unit и e2e-тесты

Обычно автотесты делят на две большие группы:

  1. unit-тестирование (тестирование отдельного модуля, элемента, компонента),
  2. e2e-тестирование (сквозное или UI-тестирование).

В чем разница:
Мы рассматривали два подхода: либо делать ставку на юнит-тесты во всех их проявлениях (интеграционные и компонентные) и тратить минимум ресурсов на проверку UI в приложении вручную, либо — наоборот.

Мы долго спорили и решали, что больше подходит для нас — в итоге победило e2e: в приложении много сложных UI-компонентов, и тестирование юзер-интерфейса крайне важно. Но и от юнит-тестов не отказались.

Серый ящик — Enzyme

Изначально у нас были только е2е-тесты плюс около 20 юнит-тестов на определенные виды функционала. Проблема е2е-тестов в том, что если они валятся, то увеличивают время на прохождение тестирования за счёт перезапуска. И тут мы подумали: а нет ли в мире чего-то ещё?! Да пожалуйста — ведь есть отдельная вселенная под названием Enzyme.

Есть среднее между юнит-тестами и е2е-тестированием — тестирование с помощью Enzyme (утилиты для тестирования JavaScript для React, которая облегчает тестирование компонентов React). Такие тесты обычно зовут интеграционными.
Денис
Разработчик scrum-студии Сибирикс
«Интеграционными» — это очень условно. Можно написать юнит-тест на компонент, а можно написать тест на компонент со всеми вложенными в него компонентами — например, компонент целой страницы приложения. Тогда это можно назвать интеграционным тестом и даже замахнуться на e2e-формулировку. Но у нас тесты именно интеграционные: тестируются не самые простые компоненты со всеми подкомпонентами.
Enzyme позволяет взять отдельный React-компонент, смонтировать его внутри теста и сымитировать браузер (его не придётся по-настоящему запускать, за это отвечает отдельная библиотека) — своеобразная маленькая песочница. Такие тесты на порядок быстрее е2е-тестов, но требуют подготовки мок-объектов (объектов, реализующих заданные аспекты моделируемого программного окружения). Иногда требуются очень сложные мок-объекты, и их тяжело подготовить. Во всех таких ситуациях мы смогли слегка поменять код тестируемого компонента, чтобы освободить его от этой сложной зависимости, что является и плюсом (чем чище код, тем больше в нем уверенности), и минусом (на это тратится дополнительное время).

Для перехода на Enzyme нам пришлось менять некоторые компоненты приложения, так как те не были к нему готовы — мы их отрефакторили (разбили на модули). Для пробы мы протестировали с помощью Enzyme календарь в приложении и попап повторения задачи. Для календаря запускается 3000 тестов, и они проходят менее чем за 5 минут, что является очень хорошим результатом.
лето сочи
Enzyme-тесты похожи на юнит-тесты тем, что нужно подготавливать входные данные. А на e2e-тестирование тем, что они также отлично тестируют UI.
лето сочи

Решение 3 — оптимизация

Мы подумали, а нельзя ли ещё ускорить процесс — и начали его оптимизировать. Причём, первые два эксперимента начались еще задолго до решения приобрести новые компьютеры специально для прогона тестов.

1) Эксперименты с количеством пачек автотестов
Для начала решили поэкспериментировать с количеством пачек автотестов: 5, 10, 20. Если пачек 5, из 1000 тестов в каждой пачке по 200. И если упадёт какой-то один, то заново придётся прогонять все 200 из этой пачки. Это долго. Сначала мы увеличили количество пачек до 10 (по 100 тестов в каждой), а потом до 20 (по 50 в каждой) — повторные запуски сильно ускорились.
Вячеслав
Разработчик scrum-студии Сибирикс
В длительность прохождения пачки включена подготовительная работа (клонируется репозиторий, устанавливаются библиотеки и подгружается собранное на предыдущем этапе приложение) — на всё уходит примерно 1 минута 20 секунд. И именно 20 пачек стали золотой серединой между длительностью этой работы и временем прохождения самих тестов.
2) Балансировка прохождения пачек автотестов
Сначала приложение находило тесты рандомно, но мы решили это систематизировать. Сгруппировали тесты с помощью скрипта, который раскидал все тесты по пачкам так, чтобы общее время пачки было примерно одинаковым. И теперь каждый тест выполняется в строго определённой для него пачке.
лето сочи
3) Повторный прогон только упавших тестов
Чтобы при падении одного теста не запускалась заново вся пачка, мы прописали, чтобы перезапускался только упавший тест. Схема такая: после прохождения тестов мы смотрим, какие упали, сохраняем в артефакты информацию об упавших тестах, и затем при перезапуске упавших тестов проверяем, были ли предыдущие неудачные запуски текущей пачки с таким же именем (tests-1, tests-2, …). За счёт этого решения на повторный прогон каждой пачки стало тратиться всего 1,5 минуты — это огромный прирост, и после внедрения этой фичи все вздохнули с облегчением.
лето сочи
4) Отказ от многократного запуска приложения
При тестировании мы обнаружили, что большая часть времени уходит именно на запуск приложения. Поэтому мы придумали сделать сброс (обнуление) текущего состояния приложения к начальному. Выделили системы, которые могут критично влиять на прохождение теста, и каждую из них сбрасывали по отдельности (заново инициализировали). Простыми словами, приложение не запускается каждый раз заново для нового теста: приложение запустилось, тест прошёл, мы сбросили состояние приложения до начального, приступили к следующему тесту. За счёт этого мы увеличили скорость 1500 тестов ещё на 15−20%: вместо 40 минут стало уходить 30.
Вячеслав
Разработчик scrum-студии Сибирикс
Для ускорения автотестов мы сменили Flow (модель командной работы) и внедрили новые виды тестов. Теперь вместо написания теста с полным запуском приложения, мы можем в некоторых случаях использовать Enzyme и написать более быстрый тест (по сравнению с e2e) — при этом отрабатывается всё так же, как и на e2e-тестах, а время экономится. В итоге у нас получился не только экстенсивный (за счёт добавления новых компьютеров), но и интенсивный рост (за счёт комплекса мер) скорости автотестов :)