Привет, меня зовут Андрей и я автоматизатор (остальные в кругу хлопают в знак сочувствия) в hh.ru. В статье расскажу, как мы ввели карантин автотестов, повысив стабильность релизов и скорость доставки.

Предпосылки
У нас много тестов: стандартный прогон (как и самые крупные релизы, например, монолитов) включает 2300 тестовых классов, которые содержат несколько тестовых методов, не считая датапровайдеров (мы пользуемся TestNG, в JUnit это @ParameterizedTest
). К сожалению, не все тесты проходят с первого раза — и даже со второго, третьего или N-го (у нас есть встроенные адаптивные ретраи, но эта тема для отдельной статьи).
Ретрай в нашем случае — это автоматический перезапуск упавших тестовых методов класса во время текущего прогона автотестов. Тест, который упал, но был отправлен на перезапуск, сохраняется в статистике запусков с результатом retry.
Некоторые из этих тестов не проходят с первого раза довольно часто, т.е. их падения связаны с нестабильностью, а не с выпускаемыми изменениями.
Падения тестов мы не любим — во время релиза это означает остановку автоматики (наши внутренние системы CI/CD) и ожидание прихода человеков, которые какое-то время разбираются в причинах падения: зовут коллег, которые скажут, что «тест просто нестабильный, перезапусти его несколько раз», или дадут добро на скип — пропуск упавших тестов, когда автор релиза самостоятельно двигает задачу в Jira в следующий статус.
Релизов у нас — несколько десятков в день, так что остановка тормозит не только текущий релиз, но и те, что в очереди за ним. Такое мы любим еще меньше.
Решение
Мы решили попробовать несколько видов карантина для самых нестабильных тестов.
Здесь и далее под тестом мы будем подразумевать тестовый класс, чтобы сэкономить буквы и когнитивную нагрузку.
Карантин в статье — это список тестов, которые наш тестовый фреймворк (TestNG + собственный раннер) не будет запускать, даже если они подпадают под условия для запуска.
Технически хранилище списка тестов в карантине устроено несложно: грубо говоря, это таблица в БД + HTTP API над ней, куда ходит наш раннер за списком тестов. В таблице есть такие поля, как: имя тестового класса, дата добавления в карантин, дата окончания карантина, имя добавившего тест в карантин и т. д.
Самое интересное — это то, как тесты попадают в карантин. Мы придумали три варианта:
1. Ручной карантин
Самый понятный вид карантина: если видим, что тест больше не проходит (обычно — в 100% случаев), то мы вручную добавляем его в карантин, чтобы релизы больше не останавливались в ожидании ручного скипа тестов.
Отключать тесты можно было и раньше — через правки в коде. Но это означало создание PR (pull request), ожидание аппрува PR, мерж — и всё то же самое некоторое время спустя для включения теста обратно.
Также при добавлении теста мы даём выбрать срок карантина:
до завтра — если знаем, что фикс уже есть и вечером вместе с релизом он попадёт в мастер
навсегда (то есть до ручного удаления теста из карантина) — если понимаем, что на исправление ситуации может понадобиться больше времени.
Технически за это отвечает поле expiration_date в БД: по нему мы фильтруем результаты внутри API, отвечая на запросы списка текущих тестов в карантине.
2. Полуавтоматический карантин
Если в результате повторного прогона упавших автотестов на релизе упало менее N* тестов, их можно вручную отправить на контрольный перепрогон на этаноловом эталонном стенде — стенде, где установлены актуальные версии сервисов, так как на релизе как минимум одна версия отличается и тесты могут падать по делу. На эталонном стенде каждый тест запускается K* раз. Если падение тестов на контрольном прогоне продолжается, они автоматически добавляются в карантин до следующего дня, а в командные чаты владельцев тестов отправляются уведомления.
*N и K мы выбирали эмпирически так:
падение более N тестов уже вызывало подозрение на массовую проблему, не связанную с конкретным тестом
падение теста менее K раз ещё вызывало подозрение на невезучесть теста или массовые проблемы
Нюансы
Иногда тесты падают, но в карантин их отправлять нельзя — например, если уже есть рецепт для исправления ситуации и мы не хотим пропускать критические тесты. Поэтому: если сегодня тест уже попадал в полуавтоматический карантин, но был удален из него — API такой тест в карантин сегодня больше не добавит.
3. Автоматический карантин на основе статистики автотестов
Это самый интересный и сложный из наших типов карантина. Мы разработали ряд критериев, по которым считаем, что тесту пора на покой в карантин — или как минимум стабилизироваться.
Все ходы результаты запуска тестов у нас записаны в БД (в разрезе тестовых классов). Раз в день бот (Python + pandas) на основе этой статистики добавляет тестовые классы в перманентный карантин и заводит/обновляет задачи на стабилизацию или ускорение теста (есть и такой критерий).
Например, если медианная стабильность теста за 7 дней без учёта ретраев меньше 70%, мы заводим задачу со средним приоритетом на стабилизацию и добавляем тест в карантин.
Для нас важнее количество падений, чем количество ретраев — любое падение приводит к ожиданию (автоматикой) ручного разбора тестового прогона на релизе.
Есть и более мягкие критерии, по которым мы заводим задачи на стабилизацию теста без его добавления в карантин. Например, если медианная стабильность за 7 дней без учета ретраев находится в диапазоне 85-95%.
Но такие задачи выглядят как техдолг, плавают как техдолг и крякают как техдолг — а его, как известно, можно копить годами (утрированно). Поэтому мы разработали мотивационные механики:
Для каждого из приоритетов задач мы выбрали SLA (Service Level Agreement). В данном случае —- сколько дней задача может находиться в каждом из приоритетов, при нарушении которого приоритет по задаче поднимается и начинается новый отсчет соответствующего SLA
При нарушении SLA по задаче в максимальном приоритете тест добавляется в карантин
Ответственность за баги, пропущенные таким тестом, несёт команда-владелец теста
Нюансы
Некоторые тесты для нас слишком критичные, чтобы их пропускать — в настройки бота мы добавили список таких тестов, чтобы никогда не добавлять их в карантин.
Иногда случаются массовые проблемы — в настройках бота также есть список ошибок, которые не учитываются при анализе стабильности (текст ошибки мы тоже храним в таблице с результатами запуска тестов)
Удаление из карантина
Мы решили не делать автоматическое удаление из карантина. Иначе может возникнуть желание «переждать бурю», ничего не делая с тестом — вдруг само рассосётся. Даже если тест нестабилен сам по себе, а не из-за каких-то массовых проблем со стендами.
Зато в планах — регулярная проверка карантина для следующих случаев:
тест слишком долго находится в карантине
последняя заведенная задача на стабилизацию теста уже закрыта, а он всё ещё находится в карантине — иногда коллеги всё чинят, закрывают задачу, но забывают удалить тест из карантина
Как понять, что тест стабилизирован
В нашем тестовом фреймворке есть специальный режим для проверки стабильности: запускаются все тесты, а параллельно в цикле запускается проверяемый тест. В конце смотрим, сколько раз он успешно прошёл и сколько упал.
Тест считается стабилизированным, если:
не было ни одного падения
было небольшое количество падений, которые не связаны с функциональностью, которую он проверяет — например, сервис не выдержал нагрузки и ответил кодом 5хх вместо главной страницы
А вот теперь слайды ©
Скорость починки тестов
Автоматика по заведению задач у нас была и раньше — тогда она просто создавалась для тестов с совсем плачевной статистикой. Но коллеги не всегда оперативно брались за такие задачи — самым старым из них (задачам) могло быть несколько месяцев.
С введением SLA и автодобавлением в карантин при просрочке ситуация заметно улучшилась: число открываемых и закрываемых задач примерно одинаковое (с поправкой на майские праздники):

Соотношение тестовых прогонов без ошибок к общему числу прогонов

Итоги
Благодаря автоматическому карантину и заведению задач на починку тестов с контролем SLA мы ускорили работы по стабилизации тестов и увеличили среднее количество прогонов, проходящих с первого раза — а значит, ускорили доставку наших изменений пользователям.
Если у вас есть нестабильные тесты и вы хотите себе такое же, то следует:
разработать критерии попадания в карантин и выхода из него
определиться с SLA на починку тестов
решить организационные вопросы: договориться с командами о своевременном реагировании на задачи и т. д.
Вот и всё. С радостью отвечу на все вопросы (в том числе технические) в комментариях.
P.S. Наши мобильные коллеги также рассказали о карантине автотестов в iOS.