Как тестируют в Slack: система автоматического обнаружения и подавления flaky-тестов

«Цель команды Mobile Developer Experience Team (DevXp) в Slack — дать разработчикам возможность уверенно отправлять код, наслаждаясь приятным и продуктивным инженерным опытом. Мы используем метрики и опросы для измерения продуктивности и впечатлений у разработчиков, такие как «общее настроение» разработчиков, стабильность CI, время до слияния (TTM) и количество упавших тестов.

Команда DevXp постоянно вкладывается в развитие тестовой инфраструктуры и улучшение CI, но одним из самых больших препятствий на пути нашего прогресса является высокий — и постоянно растущий — процент упавших тестов, из-за их нестабильности. До того, как мы стали автоматически обрабатывать такие тесты, стабильность основной ветки колебалась в районе 20 %. При внимательном изучении 80 % проблемных билдов мы обнаружили, что:

  • 57 % проблем были вызваны сбоями при выполнении тестов; состоящих из нестабильных и падающих автотестов
  • 13% проблем из-за ошибок разработчиков, проблем с CI или тестовой инфраструктурой
  • 10 % были вызваны конфликтами при слиянии

Заменив ручной подход к работе с отказами тестов на автоматизированную систему определения и подавления flaky-тестов, мы снизили количество отказов с 57 % до менее чем 5 %.

В этом посте описывается путь, по которому мы пошли, чтобы минимизировать количество нестабильных тестов с помощью подхода автоматического обнаружения и подавления нестабильности. Это не новая проблема, которую мы пытаемся решить; многие компании публиковали статьи о системах обработки flaky-тестов. В этой статье рассказывается о том, как нестабильность тестов становится все более серьезной проблемой в масштабах компании — и как мы в Slack взяли ее под контроль.

Ситуация

Для мобильных кодовых баз у нас 120+ разработчиков, создающих 550+ запросов на доработку (пулл-реквестов, далее PR) в неделю. 16 000+ автотестов на Android и 11 000+ автотестов на iOS, пирамида тестирования состоит из E2E, функциональных и модульных тестов. Все тесты запускаются при каждом PR-коммите на GitHub и при каждом PR, слитом в основную ветку. 

Разработчики отвечают за написание и поддержку всех автотестов, связанных с их группой продуктов.

По мере того как мы продолжали масштабироваться, мы задавались вопросом: можем ли мы обеспечить надежность и производительность при переносе изменений в основную ветку? Мы начали с того, что инвестировали значительные средства в стабильность тестов. Однако в больших масштабах оказалось невозможно обеспечить их стабильность, полагаясь на то, что авторы тестов будут делать все правильно. Раньше мы полагались на «коллективное сознание», то есть на то, что разработчики понимали, что тест нестабилен, и активно исследовали его. Это работало в небольшой команде (~10 разработчиков), но в больших масштабах возникает «эффект отстраненности». А вообще разработчики хотят просто слить PR, над которым они работают, а не исследовать какой-то левый нестабильный тест. По результатам опроса разработчиков и членов триаж-команды DevXp, каждый красный тест требовал в среднем около 28 минут на ручной разбор. Неизбежная нестабильность в нашей системе, большое количество сбоев при тестировании и время на разбор заставили нас подумать над автоматизацией этих процессов.

Исследовали причины нестабильности

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

Аналогично, нестабильные тесты — это неизбежная реальность тестовых наборов достаточно большого масштаба. Нестабильные тесты нарушают хрупкий баланс между скоростью и качеством. Под маской нестабильного теста может скрываться серьезный баг; тест теряет свое предназначение, если разработчик просто игнорирует сбой из-за его нестабильности. Кроме того, нестабильные тесты отнимают ценные ресурсы и увеличивают стоимость процессов — особенно если CI опирается на сторонние сервисы, такие как AWS или Firebase Test Lab (FTL). Они снижают стабильность CI, увеличивают TTM-время, снижают уверенность разработчиков в своем коде.

Мы разделили наши нестабильные тесты на два типа:

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

Один из разработчиков выразился так:

«Что касается CI, то должен поблагодарить команду DevXp за то, что у них так много типов тестов. Неплохо бы сократить длительность их выполнения на 50%, и также чтобы тесты E2E и FTL были более стабильны. Почти все мои дефекты в CI, требующие наличия физического устройства, на самом деле являлись результатом нестабильных тестов».

Наша первоначальная реализация была в значительной степени направлена на «системные» тесты.

Ручной разбор flaky-тестов

Разработчикам приходилось вручную разбирать тесты, когда они падали на PR разработчика или в основной ветке.

Процесс был такой: разработчик получал уведомление, что проверка «красная», тестовая задача упала:

  • Начинается разбор, разработчик видит, что задача не прошла из-за упавшего автотеста
  • Разработчик приходит к выводу, что упавший тест не имеет отношения к изменениям кода в PR
  • Разработчик пытается повторить упавшую задачу в надежде, что вторая попытка даст положительный результат и нестабильные тесты на этот раз пройдут (часто они повторяли этот шаг не один раз).

После неудачных попыток повторного выполнения разработчик обращается в канал поддержки DevXp. Сотрудники, находящиеся на смене сортировки, продолжат отладку проблемной задачи:

  • Они проверяют, нет ли проблем с другими PR или с задачами основной ветки из-за этого же проблемного теста.
  • Выясняют, сколько повторных запусков уже сделал разработчик и начал ли тест проходить в какой-то момент.
  • Проверяют историю теста и смотрят, не был ли он недавно изменен.
  • Проверяют, не имеет ли тест склонности к нестабильности.
  • Пытаются определить команду, которой принадлежит тест.
  • Подавляют (откладывают) тест, чтобы разработчик мог продолжить разработку, и разблокировывают возможность слияния PR.
  • Создают тикет в Jira со всеми подробностями расследования и передают тикет ответственной команде.

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

Проект Cornflake

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

Мы запустили специальный «Проект Cornflake» («кукурузные хлопья») для автоматического обнаружения и подавления нестабильный тестов.

Для начала уточним, что такое нестабильный тест, что такое упавший тест

  • Упавший (failed) тест — это достоверно упавший тест, который не проходит даже после нескольких повторных запусков.
  • Нестабильный (flaky) тест — это тест, который в конечном итоге проходит при повторном выполнении, после нескольких попыток

Первый подход: подавление нестабильных тестов после заданного количества попыток

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

Количество отказов тестов может варьироваться в зависимости от типов тестов. Например, юнит-тесты как правило менее надежны, чем функциональные, которые в свою очередь менее надежны по сравнению со сквозными E2E-тестами. Зная это, мы создали решение, основанное на группировке тестов по типам и по уровню допустимых отказов для каждого типа.

Реализация

Это было наше высокоуровневое CI-решение для каждого случая отказа:

  • Разобрать результаты тестов, чтобы выделить упавшие
  • Для каждого упавшего теста извлекаем из бекенда историю тест-кейсов в основной ветке за последние N прогонов (например, N = 50)
  • Рассчитать степень нестабильности каждого неудачного теста: Failed Test Runs / N test runs
    • Если у теста нет истории, или достаточно истории, то он автоматически считается flaky-тестом (т. е. мы считаем тест flaky, пока он не докажет нам, что он стабилен).
  • Удаление нестабильных тестов из результатов тестирования — тех которые пересекают определенный порог нестабильности
  • Обновляем бэкенд, чтобы указать, что этот тест нестабилен, установив ему is_flaky = true.
  • CI получает измененный xml-файл результатов тестирования, и тестовая задача помечается зеленым цветом.

Что получилось

Получилось мгновенное повышение стабильности, как на PR, так и в основной ветке, с момента развертывания проекта Cornflake. Стабильность PR-сборки выросла с 71 % на момент развертывания проекта до 88 % на следующей неделе. Стабильность сборки основной ветки повысилась с 61 % до 90 % на следующей неделе.

Недостатки этого подхода

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

Недостаток № 1: нестабильные тесты превращались в красные тест и проникали в основную ветку.

Рассмотрим следующий сценарий:

  • Разработчик A пишет совершенно новый валидный тест и сливает его в основную ветку.
  • Однако, поскольку этот тест не имеет достаточной истории, созданной на бэкенде, мы классифицируем его как flaky, и удалим из результатов тестирования, отметив задачу как успешную.
    • В этот момент на основной ветке тест по факту упавший, но он продолжает выполняться, и фильтруется на CI из результатов тестирования из-за отсутствия достаточной истории по нему. В то же время тест падает локально и озадачивает разработчиков. Тестовые задачи начинают падать на CI, как только накопится достаточно истории тестов и будет превышен порог «флейковости». Теперь разработчику или «сортировщику» придется долго выяснять, с какого момента этот тест начал падать, что обнуляет плюсы проекта Cornflake.

Недостаток №2: добавление flaky-теста обратно в пул.

Еще сценарий:

  • Разработчик A открывает PR для исправления flaky-теста. Исправление не срабатывает, тест падает.
  • В этом случае задача в CI должна была завершиться неудачей без удаления теста из результатов.
  • Однако система проверяет историю тестов, находит достаточно истории для расчета % flakiness и удаляет тест из результатов, классифицируя его как flaky, и пропуская задачу.
  • Разработчик смотрит на статус задания, видит, что тест был исправлен, и закрывает тикет в Jira, но на самом деле тест не исправлен!

Недостаток № 3: Зависимость от бэкенда.

  • Локальная разработка: Источник информации об истории тестов находится на бэкенде, а это значит, что информация локальных разработчиков при выполнении тестов неполная. Синхронизация системы с локальной средой разработчиков — это большой объем работы.
  • Если бэкенд падает, тестовые задачи в CI останавливаются. Они выдают сообщение «Unable to perform flaky test calculation.» Это приводит к тому, что разработчики не могут мержить свои PR, а основная задача не может обновить бэкенд.

Выводы из первоначального подхода

  • Простая фильтрация результатов flaky-тестов — не лучший подход, поскольку мы просто ограничиваем некоторые тесты, чтобы они не влияли на основной билд. Такой подход затрудняет расследование нестабильности тестов, поскольку не хватает информации о том, когда состояние теста изменилось.
  • Вместо того чтобы заниматься обнаружением и подавлением падения тестов на уровне PR и на уровне основной ветки, лучше подавлять тесты только в основной ветке. Таким образом, разработчики смогут просто получить последнюю версию основной ветки, чтобы предотвратить нестабильность тестов на своем PR.

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

Второй подход: подавление выполнения нестабильных тестов

Мы разделили обработку отказов на три части:

  • Обнаружение: Идентификация отказов теста и разграничение нестабильных и падающих тестов.
  • Подавление: Создание тикета в Jira, открытие PR для подавления теста, автоматическое одобрение PR, затем слияние PR.
  • Уведомления в Slack: Уведомление команды DevXp о создании и слиянии PR.

Метрика успешности

  • Увеличение количества успешных задач основной ветви до 95% и снижение частоты отказов тестовых задач до уровня ниже 5%

Требования

  • Все тесты в основной ветке должны быть пройдены, так как они прошли PR-проверку
  • Поддержка всех типов тестов: E2E, функциональные и модульные
  • Возможность автоматического обнаружения и подавления падающих и нестабильных тестов по отдельности
  • Нельзя подавлять сбои на бэкенде и в API, крэши тестов или сбои инфраструктуры
  • Поддержка подавления тестов для обеих мобильных платформ: iOS и Android
  • Создание карты владения тестами, чтобы прикрепить тесты к соответствующим командам разработчиков
  • Назначать тикет Jira команде, которая владеет тестом
  • Прикреплять к тикету Jira подробную информацию о неудачных результатах тестирования для облегчения расследования
  • Исключение из процесса определенных тестов в случае, если команда отказывается от их отключения
  • Возможность автоматического или ручного слияния PR после подавления тестов
  • Еженедельная отправка оповещений на канал каждой команды со сводкой «подавленных» тестов

Реализация

Обнаружение:

"""
Get list of flaky or failing tests, excluding backend/API failures, test crash, and infra failures as they are unrelated to test logic
"""
def get_test_failures_from_raw_results():
	test_failures = []
	result_files = get_list_of_test_result_files_from_ci()
	for result_file in result_files:
		for test in result_file:
			if (test.status == "failure" or test.status == "flakyFailure") and not test.is_infra_incident and not test.is_crash and not test.is_api_failure:
				test_failures.append(test)
	return test_failures

Отключаем тест, создаем Jira и PR:

"""
- Create a Jira ticket if one doesn't exist
- Create and checkout branch
- Disable test
- Commit and push changes
- Open PR with description, auto approve it, and add it to MergeQueue
"""
def disable_test_with_jira_and_pr_creation(test_failures):
	for test_name in test_failures:
		owner_team, jira_project_id = get_team_owner_and_jira_project()
		jira_ticket = find_or_create_jira_ticket(jira_project_id)
		branch_name = create_and_checkout_git_branch()
		disable_test(test_name, jira_ticket)
		commit_and_push_changes(branch_name)
		pr = open_pr_and_assign_reviewer(jira_ticket, branch_name)
		approve_pr_and_merge(pr)

Отключаем тест на платформе:

"""
Modify the test file to disable test based on platform: iOS or Android
"""
def disable_test(test_name, jira_ticket):
	test_file_path = get_file_path_for_test(test_name)
	with in_place.InPlace(test_file_path) as test_file:
		for line_num, line in enumerate(test_file, 1):     
			# Regex to detect test name
			test_found = re.search(test_name + "`?\(", line, re.MULTILINE)  
			if test_found: 
				if self.platform == "ios":
					disable_ios_test()
				elif self.platform == "android":
					disable_android_test()
			test_file.write(line)

"""
This function disables a test by renaming it and adds a Jira ticket to the comment
Example input: func testShouldShowInvite() {
Example output: // https://jira.com/PROJ-123
				func disabled_testShouldShowInvite() {
"""
def disable_ios_test(jira_ticket):
	...

"""
This function disables a test by renaming it and adds a Jira ticket to the comment
Example input: fun testShouldShowInvite() {
Example output: @Ignore('https://jira.com/PROJ-123')
				fun testShouldShowInvite() {
"""
def disable_android_test(jira_ticket):
	...

Что получилось

Как мы работали с метриками успешности.

Повысили стабильность главной ветки до 96 %: С 19,82 % на 27 июля 2020 года до 96 % на 22 февраля 2021 года. Области, негативно влияющие на стабильность — 3rd party сервисы и конфликты слияний.

Снижение количества отказов при выполнении тестовых задач до 3,85%: С 56,76 % на 27 июля 2020 года до 3,85 % на 22 февраля 2021 года. Негативное влияние оказали такие области, как 3rd-party сервисы, простои инфраструктуры и CI.

Сэкономлено 553 часа времени на сортировку: Ручная сортировка нестабильных тестов занимала ~ 28 минут на PR. Благодаря автоматизации мы создали 693 PR для Android и 492 PR для iOS, что позволило сэкономить 553 часа (23 дня рабочего времени разработчиков).

Улучшение настроения разработчиков: Цитата одного из разработчиков:

«Вижу, что iOS-CI теперь намного стабильнее и быстрее. Спасибо за работу! Это значительно повысило производительность. Благодарю!»

Повышение доверия разработчиков: Мы обратились к нашим разработчикам, чтобы получить обратную связь, и по результатам опроса стало ясно, что доверие выросло.

  • 74 % сказали, что #proj-cornflake положительно повлиял на стабильность основной ветки
  • 64 % сказали, что #proj-cornflake уменьшил количество повторов на PR

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

Доработка проекта

Во время собеседования с разработчиками мы задали следующий вопрос:

В общем: 26 % сказали, что им было нелегко освоить процесс, а 58 % ответили, что более-менее легко.

Остались некоторые проблемы: после нескольких месяцев действия проекта многие проблемные тесты были подавлены, и разработчики испытывали трудности с повторной активацией тестов, поскольку сбой теста мог быть устаревшим, его было трудно воспроизвести, или функция изменилась.

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

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

Источник

Какой была ваша первая зарплата в QA и как вы искали первую работу?

Мега обсуждение в нашем телеграм-канале о поиске первой работы. Обмен опытом и мнения.

Подписаться
Уведомить о
guest

0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии

Мы в Telegram

Наш официальный канал
Полезные материалы и тесты
Готовимся к собеседованию
Project- и Product-менеджмент

? Популярное

? Telegram-обсуждения

Наши подписчики обсуждают, как искали первую работу в QA. Некоторые ищут ее прямо сейчас.
Наши подписчики рассказывают о том, как не бояться задавать тупые вопросы и чувствовать себя уверенно в новой команде.
Обсуждаем, куда лучше податься - в менеджмент или по технической ветке?
Говорим о конфликтных ситуациях в команде и о том, как их избежать
$1100*
медианная зарплата в QA в июне 2023

*по результатам опроса QA-инженеров в нашем телеграм-канале

Собеседование

19%*
IT-специалистов переехало или приняло решение о переезде из России по состоянию на конец марта 2022

*по результатам опроса в нашем телеграм-канале

live

Обсуждают сейчас