Автоматическое тестирование
Интеграционные, e2e и юнит-тесты: как мы пришли к кластеру для запуска 1500 интеграционных тестов
Мы уже как-то писали обзор систем управления тест-планами. Сегодня расскажем о новом экспириенсе: как разгоняли автотесты и за счёт чего нам удалось сократить время их прохождения почти втрое.
Владимир
CEO & Founder
Опыт, который мы собрали по автотестам — это опыт хождения по граблям. Мы собрали все возможные косяки и грабли, и есть подозрение, что радиусе 250 км ни у кого подобного опыта нет. То что получили в итоге, то как смогли разогнать тестирование — это шикарный экспириенс. Интеграционные тесты по критическим путям — это то, что стоит проделывать, но абсолютно на всех проектах делать юнит-тесты — очень дорого. У нас сейчас 2 человека фулл-тайм занимаются только тестами.
Что тестировали
SingularityApp — планировщик задач, основанный на идеях хаос-менеджмента. Написан на Electron и React. На момент экспериментов с автотестами в нашем стеке были: GitLab CI/СD, Jest, Spectron (WebDriver) и Robot.js.
Автотестами мы проверяли приложение по трём параметрам:
Автотестами мы проверяли приложение по трём параметрам:
- проектирование возможного поведения пользователя,
- проверка на баги, в том числе чтобы исключить возможные регрессии — внезапные повторы уже исправленных багов,
- проверка заявленных возможностей у несистемных модулей (большую часть которых писали сами).
Как было сначала и что пошло не так
Мы тестировали приложение на нескольких компьютерах с MacOS — вместе они объединялись в кластер. На каждом запускался раннер в шелл-режиме (создавалась невиртуальная среда). Раннер — это программа от GitLab, отвечающая за поочередный запуск — «прогон» — задач.
Очевидно, что при таком раскладе во время прохождения тестов кодеры не могли параллельно использовать компьютер для написания кода. Поэтому в таком режиме мы пробыли примерно полдня — пока поднимали GitLab.
Чтобы запараллелить работу раннера и программиста на одном компьютере, мы создали две разных учетных записи пользователей: раннер вынесли в отдельный сеанс, а кодер работал, зайдя под другим пользователем. И тут началось «веселье».
Очевидно, что при таком раскладе во время прохождения тестов кодеры не могли параллельно использовать компьютер для написания кода. Поэтому в таком режиме мы пробыли примерно полдня — пока поднимали GitLab.
Чтобы запараллелить работу раннера и программиста на одном компьютере, мы создали две разных учетных записи пользователей: раннер вынесли в отдельный сеанс, а кодер работал, зайдя под другим пользователем. И тут началось «веселье».
Проблема 1 — тесты стали жить своей жизнью
Предполагалось, что процесс написания кода и тестирования будет идти параллельно. Однако очень сложно писать код, когда GitLab предлагает раннерам в то же самое время потестировать приложение. Поверх IDE (интегрированной среды разработки) выскакивает окно тестируемого приложения и в темпе вальса начинает совершать какие-то действия с мышью и клавиатурой.
Денис
Разработчик scrum-студии Сибирикс
Хоть убейте, как это происходит — загадка. Либо хитрость MacOS, либо хитрость тестовых фреймворков. Вопрос, конечно, интересный, но проблему можно было решить другой «серебрянной пулей» — запуском тестов в виртуальной среде.
Проблема 2 — тратилось очень много ресурсов
Так как раннер и программист работают на одном компьютере, они невольно отбирают друг у друга ресурсы. Если раннер от такого вряд ли расстроится (максимум — завалит тест), то у специалиста знатно пригорает, когда все окружение начинает дико тормозить.
Ожидалось, что тест запустит приложение, после чего он должен присоединиться к процессу приложения и начать выполнять в нем кейс. Однако из-за разных факторов он не всегда мог получить доступ к процессу. Тест по факту падал, но приложение уже было запущено, и тестовое окружение его не контролировало. Следом автоматически запускался следующий тест, и там картина повторялась — в итоге было уже два приложения, которые не контролировало тестовое окружение. Если это вовремя не отследить, получалась такая картина:
Ожидалось, что тест запустит приложение, после чего он должен присоединиться к процессу приложения и начать выполнять в нем кейс. Однако из-за разных факторов он не всегда мог получить доступ к процессу. Тест по факту падал, но приложение уже было запущено, и тестовое окружение его не контролировало. Следом автоматически запускался следующий тест, и там картина повторялась — в итоге было уже два приложения, которые не контролировало тестовое окружение. Если это вовремя не отследить, получалась такая картина:
Какого-то простого способа ограничить ресурсы раннеру в шелл-режиме мы применить не смогли. Поэтому решили настроить докеры.
Решение 1 — Docker вместо шелл-режима
Докер создаёт в операционной системе компьютера обособленную среду, которая убирает часть проблем (например — фокус-покус появления приложения поверх IDE программиста). Но по факту докеры также работают параллельно с написанием кода, и также отбирают ресурсы — а значит, мешают работе кодеров. Плюс в том, что в докере можно выставить лимиты на забор ресурсов для раннеров (чтобы те не сжирали всю-всю память или процессор в случае проблем при исполнении тестов).
Проблему с нехваткой ресурса решили, докупив ещё несколько новых мощных серверов на Linux, заточенных только под тестирование и собрав из них кластер. Фактически теперь тестирование шло на одних компьютерах, а написание кода — на других. Итого, за счёт докеров и нескольких новых (ТОЛЬКО) тестовых компьютеров мы суммарно смогли сократить время тестов на треть.
Проблему с нехваткой ресурса решили, докупив ещё несколько новых мощных серверов на Linux, заточенных только под тестирование и собрав из них кластер. Фактически теперь тестирование шло на одних компьютерах, а написание кода — на других. Итого, за счёт докеров и нескольких новых (ТОЛЬКО) тестовых компьютеров мы суммарно смогли сократить время тестов на треть.
Но возникла ещё одна параллельная проблема — Flow (модель командной работы): мы делали много фич в одной ветке и отправляли на тест. Поначалу ситуация нас почти устраивала — тестов было немного, и мы смирились с некоторыми недостатками, поскольку решать их было затратно по времени.
Но мирились мы до тех пор, пока очередь задач с тестами не выросла настолько, что не могла рассосаться даже за ночь. Ночью, как известно, все программисты спят и ожидаемо снижают нагрузку на GitLab. А у нас есть повод надеяться, что к утру все накопленные тесты пройдут. Ну-ну. В итоге время одного прогона тестов опять возросло до часа и более, а самих прогонов могло быть 10−12. Умножайте.
Но мирились мы до тех пор, пока очередь задач с тестами не выросла настолько, что не могла рассосаться даже за ночь. Ночью, как известно, все программисты спят и ожидаемо снижают нагрузку на GitLab. А у нас есть повод надеяться, что к утру все накопленные тесты пройдут. Ну-ну. В итоге время одного прогона тестов опять возросло до часа и более, а самих прогонов могло быть 10−12. Умножайте.
Динамика автотестов за год. Успешно пройдённые тесты — зелёным
Денис
Разработчик scrum-студии Сибирикс
При взгляде на график прохождения тестов складывается впечатление, что у нас успешно проходит 20−30% тестов, однако это не так. Каждый зеленый пункт означает, что после внесения изменений в проект он прошел абсолютно все тесты. Если завалился хотя бы один тест из полутора тысяч — значит, успешная статистика в пролете. У нас 20 наборов (пачек) тестов, в пачке может быть до 70 тестов — если упадет хоть один, то вся итерация тестирования приговаривается к «серости».
Из-за такой модели работы при возникновении проблемы в каком-то куске кода на графике прохождения всё становилось красным — и было непонятно, из-за какой именно фичи всё сломалось. Это усиливало уже описанные проблемы — больше запусков для отдельных фич, больше очередь на прохождение тестов. Поэтому Flow пришлось пересмотреть — мы перешли к последовательному тестированию: сделали фичу, проверили, довели до зеленого состояния, приступаем к другой.
Решение 2 — эксперименты с видами тестирования
По классике: unit и e2e-тесты
Обычно автотесты делят на две большие группы:
- unit-тестирование (тестирование отдельного модуля, элемента, компонента),
- e2e-тестирование (сквозное или UI-тестирование).
В чем разница:
Мы рассматривали два подхода: либо делать ставку на юнит-тесты во всех их проявлениях (интеграционные и компонентные) и тратить минимум ресурсов на проверку UI в приложении вручную, либо — наоборот.
Мы долго спорили и решали, что больше подходит для нас — в итоге победило e2e: в приложении много сложных UI-компонентов, и тестирование юзер-интерфейса крайне важно. Но и от юнит-тестов не отказались.
Мы долго спорили и решали, что больше подходит для нас — в итоге победило e2e: в приложении много сложных UI-компонентов, и тестирование юзер-интерфейса крайне важно. Но и от юнит-тестов не отказались.
Серый ящик — Enzyme
Изначально у нас были только е2е-тесты плюс около 20 юнит-тестов на определенные виды функционала. Проблема е2е-тестов в том, что если они валятся, то увеличивают время на прохождение тестирования за счёт перезапуска. И тут мы подумали: а нет ли в мире чего-то ещё?! Да пожалуйста — ведь есть отдельная вселенная под названием Enzyme.
Есть среднее между юнит-тестами и е2е-тестированием — тестирование с помощью Enzyme (утилиты для тестирования JavaScript для React, которая облегчает тестирование компонентов React). Такие тесты обычно зовут интеграционными.
Есть среднее между юнит-тестами и е2е-тестированием — тестирование с помощью Enzyme (утилиты для тестирования JavaScript для React, которая облегчает тестирование компонентов React). Такие тесты обычно зовут интеграционными.
Денис
Разработчик scrum-студии Сибирикс
«Интеграционными» — это очень условно. Можно написать юнит-тест на компонент, а можно написать тест на компонент со всеми вложенными в него компонентами — например, компонент целой страницы приложения. Тогда это можно назвать интеграционным тестом и даже замахнуться на e2e-формулировку. Но у нас тесты именно интеграционные: тестируются не самые простые компоненты со всеми подкомпонентами.
Enzyme позволяет взять отдельный React-компонент, смонтировать его внутри теста и сымитировать браузер (его не придётся по-настоящему запускать, за это отвечает отдельная библиотека) — своеобразная маленькая песочница. Такие тесты на порядок быстрее е2е-тестов, но требуют подготовки мок-объектов (объектов, реализующих заданные аспекты моделируемого программного окружения). Иногда требуются очень сложные мок-объекты, и их тяжело подготовить. Во всех таких ситуациях мы смогли слегка поменять код тестируемого компонента, чтобы освободить его от этой сложной зависимости, что является и плюсом (чем чище код, тем больше в нем уверенности), и минусом (на это тратится дополнительное время).
Для перехода на Enzyme нам пришлось менять некоторые компоненты приложения, так как те не были к нему готовы — мы их отрефакторили (разбили на модули). Для пробы мы протестировали с помощью Enzyme календарь в приложении и попап повторения задачи. Для календаря запускается 3000 тестов, и они проходят менее чем за 5 минут, что является очень хорошим результатом.
Для перехода на Enzyme нам пришлось менять некоторые компоненты приложения, так как те не были к нему готовы — мы их отрефакторили (разбили на модули). Для пробы мы протестировали с помощью Enzyme календарь в приложении и попап повторения задачи. Для календаря запускается 3000 тестов, и они проходят менее чем за 5 минут, что является очень хорошим результатом.
Enzyme-тесты похожи на юнит-тесты тем, что нужно подготавливать входные данные. А на e2e-тестирование тем, что они также отлично тестируют UI.
Решение 3 — оптимизация
Мы подумали, а нельзя ли ещё ускорить процесс — и начали его оптимизировать. Причём, первые два эксперимента начались еще задолго до решения приобрести новые компьютеры специально для прогона тестов.
1) Эксперименты с количеством пачек автотестов
Для начала решили поэкспериментировать с количеством пачек автотестов: 5, 10, 20. Если пачек 5, из 1000 тестов в каждой пачке по 200. И если упадёт какой-то один, то заново придётся прогонять все 200 из этой пачки. Это долго. Сначала мы увеличили количество пачек до 10 (по 100 тестов в каждой), а потом до 20 (по 50 в каждой) — повторные запуски сильно ускорились.
1) Эксперименты с количеством пачек автотестов
Для начала решили поэкспериментировать с количеством пачек автотестов: 5, 10, 20. Если пачек 5, из 1000 тестов в каждой пачке по 200. И если упадёт какой-то один, то заново придётся прогонять все 200 из этой пачки. Это долго. Сначала мы увеличили количество пачек до 10 (по 100 тестов в каждой), а потом до 20 (по 50 в каждой) — повторные запуски сильно ускорились.
Вячеслав
Разработчик scrum-студии Сибирикс
В длительность прохождения пачки включена подготовительная работа (клонируется репозиторий, устанавливаются библиотеки и подгружается собранное на предыдущем этапе приложение) — на всё уходит примерно 1 минута 20 секунд. И именно 20 пачек стали золотой серединой между длительностью этой работы и временем прохождения самих тестов.
2) Балансировка прохождения пачек автотестов
Сначала приложение находило тесты рандомно, но мы решили это систематизировать. Сгруппировали тесты с помощью скрипта, который раскидал все тесты по пачкам так, чтобы общее время пачки было примерно одинаковым. И теперь каждый тест выполняется в строго определённой для него пачке.
Сначала приложение находило тесты рандомно, но мы решили это систематизировать. Сгруппировали тесты с помощью скрипта, который раскидал все тесты по пачкам так, чтобы общее время пачки было примерно одинаковым. И теперь каждый тест выполняется в строго определённой для него пачке.
3) Повторный прогон только упавших тестов
Чтобы при падении одного теста не запускалась заново вся пачка, мы прописали, чтобы перезапускался только упавший тест. Схема такая: после прохождения тестов мы смотрим, какие упали, сохраняем в артефакты информацию об упавших тестах, и затем при перезапуске упавших тестов проверяем, были ли предыдущие неудачные запуски текущей пачки с таким же именем (tests-1, tests-2, …). За счёт этого решения на повторный прогон каждой пачки стало тратиться всего 1,5 минуты — это огромный прирост, и после внедрения этой фичи все вздохнули с облегчением.
Чтобы при падении одного теста не запускалась заново вся пачка, мы прописали, чтобы перезапускался только упавший тест. Схема такая: после прохождения тестов мы смотрим, какие упали, сохраняем в артефакты информацию об упавших тестах, и затем при перезапуске упавших тестов проверяем, были ли предыдущие неудачные запуски текущей пачки с таким же именем (tests-1, tests-2, …). За счёт этого решения на повторный прогон каждой пачки стало тратиться всего 1,5 минуты — это огромный прирост, и после внедрения этой фичи все вздохнули с облегчением.
4) Отказ от многократного запуска приложения
При тестировании мы обнаружили, что большая часть времени уходит именно на запуск приложения. Поэтому мы придумали сделать сброс (обнуление) текущего состояния приложения к начальному. Выделили системы, которые могут критично влиять на прохождение теста, и каждую из них сбрасывали по отдельности (заново инициализировали). Простыми словами, приложение не запускается каждый раз заново для нового теста: приложение запустилось, тест прошёл, мы сбросили состояние приложения до начального, приступили к следующему тесту. За счёт этого мы увеличили скорость 1500 тестов ещё на 15−20%: вместо 40 минут стало уходить 30.
При тестировании мы обнаружили, что большая часть времени уходит именно на запуск приложения. Поэтому мы придумали сделать сброс (обнуление) текущего состояния приложения к начальному. Выделили системы, которые могут критично влиять на прохождение теста, и каждую из них сбрасывали по отдельности (заново инициализировали). Простыми словами, приложение не запускается каждый раз заново для нового теста: приложение запустилось, тест прошёл, мы сбросили состояние приложения до начального, приступили к следующему тесту. За счёт этого мы увеличили скорость 1500 тестов ещё на 15−20%: вместо 40 минут стало уходить 30.
Вячеслав
Разработчик scrum-студии Сибирикс
Для ускорения автотестов мы сменили Flow (модель командной работы) и внедрили новые виды тестов. Теперь вместо написания теста с полным запуском приложения, мы можем в некоторых случаях использовать Enzyme и написать более быстрый тест (по сравнению с e2e) — при этом отрабатывается всё так же, как и на e2e-тестах, а время экономится. В итоге у нас получился не только экстенсивный (за счёт добавления новых компьютеров), но и интенсивный рост (за счёт комплекса мер) скорости автотестов :)