Стабильность develop в Android

Всем привет! Меня зовут Костя, я тимлид платформенной мобильной команды в hh.ru. Мы уже рассказывали о практиках, которые помогают нам выпускать еженедельные релизы мобильных приложений: автоматизация тестирования, Release Train, GitHub Flow, Continuous Integration. И нам стали задавать вопросы: «А как дорого обходится обслуживание всех этих практик и автоматизаций в дальнейшем? С какими проблемами вы чаще всего сталкиваетесь и как их решаете?».

В статье (у которой, кстати, есть видеоверсия в нашем влоге) я отвечу на эти вопросы, а также расскажу о том, как в Android-направлении мобильной разработки HeadHunter мы поддерживаем стабильность нашей develop-ветки.

image-loader.svg

Стабильность develop-ветки и что проверяем на CI

Для начала стоит определиться, что такое «стабильный develop», и почему для нас это важно.

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

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

  • Статические анализаторы. В Android мы используем Detekt и Android Lint, Они подсвечивают нарушения кодстайла и потенциальные проблемы в рантайме.

  • Ну и, конечно же, мы запускаем авто- и unit-тесты. Автотесты у нас интеграционные: взаимодействуют с реальным инстансом бэкенда, развернутом в тестовом окружении с синтетическими тестовыми данными. 

Итак, стабильный develop — это состояние нашей develop-ветки, в котором она успешно проходит все перечисленные автоматизированные проверки.

Казалось бы, что может пойти не так с develop-веткой? Ведь перед тем, как туда попадает что-то новое из фиче-веток, мы прогоняем все автоматизации и не даем подмерджить, если хотя бы одна не проходит проверки.

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

Метрика стабильности develop

Посмотрим на нашу метрику стабильности develop.

image-loader.svg

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

Благодаря ночным прогонам утром разработчик может узнать, не сломал ли он чего-нибудь в своей фиче-ветке, что нужно пофиксить перед мерджем в develop и насколько стабилен сам develop — не попали ли в него фатальные изменения.

Далее мы поговорим о факторах, которые мешают графику быть исключительно ровной прямой на уровне 100%.

Изменения на бэкенде

Первый возможный фактор нестабильности develop — изменения на бэкенде, которые каким-то образом затрагивают выполнение автотестов. Например, на бэкенде изменился статичный текст, который получает приложение и отображает на экране. Этот текст мог стать больше, что повлияло на расположение других UI-элементов на экране, из-за чего наши автотесты, взаимодействующие с приложением через UI, стали промахиваться при выполнении своих проверок.

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

Устаревание CI-проверок

Следующий фактор нестабильности — это устаревание CI-проверок на PR, открытых в develop. Такую ситуацию я бы назвал гонкой пул-реквестов. Давайте визуализируем по шагам пример воспроизведения такой ситуации:

image-loader.svg

  • Допустим, у нас был открыт пул-реквест фиче-ветки в develop (PR 1). На CI мы проверили, что состояние develop-ветки будет стабильным, если к ней подмерджить фиче-ветку из PR 1;

  • Параллельно разработчик из другой команды открыл PR 2 со своей фичей, и проверки показали успех. Пока всё отлично;  

  • Дальше мы подмерджили PR 1 в develop. Теперь PR 2 может сделать состояние develop нестабильным, ведь проверки для PR 2 мы запускали с тем состоянием develop, когда в нем еще не было PR 1.

В идеале, чтобы этого не допускать, следует перезапускать все проверки на открытых пул-реквестах, если меняется та ветка, в которую они открыты.

Раньше мы этого не делали, и примерно пару раз в месяц наш develop падал. Например, где-то неявно мог сломаться автотест или сборка. Тогда дежурные с платформенной командой срочно бежали смотреть, в чем проблема, и быстро фиксить.

Чтобы не повторять таких ошибок, мы решили запретить мерджить пул-реквест в develop, если он не был проверен на последней актуальной версии develop.

image-loader.svg

Первое время это ощутимо нас замедляло, было неудобно и непривычно. Но из плюсов могу отметить, что такой подход мотивирует поскорее проверять и подмердживать пул-реквесты. Также это мотивирует ускорять CI-проверки, чтобы можно было без особых проблем запускать их как можно чаще. За последние полгода мы ускорили прогон наших проверок на CI в среднем от 50 до 30 минут, и это с учетом регресса через автотесты. Без жертв для полноты проверок и существенного наращивания мощностей железа, исключительно за счет более оптимального использования CI-агентов. 

Ахтунг! Up-to-date требование для веток на PR может подойти не любой команде. Все зависит от масштабов разработки и среднего количества PR в основную ветку вашего репозитория в течение условной недели или дня. В нашем случае за неделю в develop открывается не больше 10 PR фиче-веток, а в среднем 6–8 за неделю (около 1–2 PR в день). Конечно же, общее количество PR у нас значительно больше, благодаря PR с задачами, открытыми в фиче-ветки, но для них мы не включаем up-to-date требование.

Проблемы с инфраструктурой

Следующий фактор нестабильности develop — это инфраструктура. Причем, как CI-инфраструктура, так и инфраструктура тестовых стендов. Сейчас я высокоуровнево расскажу, как устроена наша инфраструктура для автотестов с точки зрения Android-разработки.

image-loader.svg

  • Начнем с CI-агента — машины, на которой запускается CI-прогон. Для того чтобы запустить автотесты на CI, мы сперва отправляем запрос кластеру эмуляторов и просим его предоставить нам необходимое количество эмуляторов для запуска автотестов;

  • Используя адреса эмуляторов, мы подключаемся к ним по adb и запускаем автотесты. При этом автотесты во время запуска будут взаимодействовать с API, которое развернуто на заранее подготовленном тестовом стенде;

  • Тестовые стенды — это машины, на которых развернут бэк в тестовом режиме. Таких стендов у нас много, и мы можем накатывать на них инстансы бэкэнда независимо друг от друга. Так мы получаем изолированное окружение, где любой разработчик или QA может тестировать, разрабатывать и дебажить, не мешая другим.

С чем вообще у нас возникали проблемы в плане инфраструктуры?

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

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

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

Валидация занимает меньше минуты и может сэкономить примерно полчаса полного прогона всех наших 360 автотестов, распараллеленных на 15 эмуляторах.

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

Общая стабильность develop не может быть выше, чем стабильность нашей тестовой инфраструктуры.

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

Однако в наших CI-скриптах тоже могут быть баги. Не так давно мы начали сталкиваться с большим количеством ретраев автотестов из-за необъяснимых на первый взгляд 500-ых ошибок с тестового стенда. Начав разбираться в проблеме, мы поняли, что она возникает из-за ошибки в скрипте валидации тестового стенда. А именно из-за отправки невалидных тестовых данных на стенд, которые бэкэнд не мог правильно обработать.

Вывод: если у вас в CI-инфраструктуре достаточно сложная и объемная логика, возможно, unit-тесты для нее — не самая плохая идея. Даже если вы запускаете ее руками, чтобы не писать дополнительный CI, который проверяет основной CI.

2cad82728d7ef8730244bab798591024.jpeg

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

Нестабильные автотесты

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

Для того, чтобы минимизировать нестабильность  автотестов, мы в Android используем надежные Kaspresso и Marathon.

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

Marathon — это тест-раннер, который тоже можно применять для ретраев, только на более высоком уровне. Например, когда каждый отдельный тест в наборе может быть  автоматически заретраен. Тест признается упавшим только по достижении допустимой квоты на ретраи.

Но, к сожалению, даже с этими инструментами у нас всё еще остается пространство для потенциальных проблем. Приведу конкретные примеры происходящего в автотестах, из-за чего они могут стать «флакующими».

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

В своих автотестах мы пока что не решили эту проблему, но планируем реализовать способ, аналогичный тому, что используют для проверки снэкбаров ребята из Avito. Идея в том, чтобы проверять одноразовые UI-события не по факту их отображения на экране, а записывать историю событий их показа, которую можно проверить постфактум. Такой способ мы уже долгое время применяем для тестирования отправки событий аналитики. Тем не менее, при реализации такой идеи стоит обратить внимание на то, какое именно событие логируется в истории: факт показа UI-элемента или намерение его показать. Это важно учитывать, так как оба способа дают разные гарантии в автотестах. 

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

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

step("Открываем автопоиск") {
    autosearchRecyclerView {
        childAt(1) {
            click()
        }
    }
}

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

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

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

private fun KRecyclerItem<*>.isNotSkeletonItem() {
    notMatches {
        withDescendant {
            withClassName(StringContains("ShimmerFrameLayout"))
        }
    }
}

// ...

step("Открываем автопоиск") {
    autosearchRecyclerView {
        childAt(1) {
            isNotSkeletonItem()
            click()
        }
    }
}

Еще одна распространенная проблема, с которой мы неоднократно сталкивались —  не самая стабильная обработка кликов средствами нативного Espresso, который скрывается под капотом Kaspresso. Например, мы часто ловили ситуацию, когда клики срабатывают как длинные, хотя нам было нужно, чтобы они срабатывали как одинарные. Подробное описание этой проблемы и вариант ее решения можно посмотреть в инфраструктурном репозитории ребят из Avito.

Отчеты по автотестам

Чтобы следить за здоровьем наших автотестов, мы используем различные отчеты об их прогонах. Например, Allure-отчеты, которые можно генерировать при помощи Kaspresso или Marathon. Разбираться в проблемах хорошо помогает разделение теста на шаги, успешность которых прикрепляется в отчет, а также запись видео и Logcat с устройства.

image-loader.svg

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

image-loader.svg

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

Пример нестабильного теста на графиках выглядит так:

image-loader.svg

А вот пример достаточно стабильного теста:

image-loader.svg

Такая статистика помогает быстро понять — нужно бежать и искать баг в приложении или поработать над стабильностью автотеста.

Что делать с нестабильными автотестами

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

image-loader.svg

Вывод такой: если у вас есть тесты, которые ведут себя нестабильно, но всё-таки остаются зелеными только за счет ретраев, то рано или поздно ждите беды. Такие тесты могут подвести в самый неудобный момент. 

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

Также наши QA-инженеры имеют право всё-таки подмерджить ветку, даже с неудачным прогоном автотестов на CI. Но только в том случае, если они проверили вручную, что в реальных условиях сценарий воспроизводится успешно. Тогда автотесты все еще сэкономят тестировщику массу времени, ведь ему не придется тратить время на ручной регресс сотен других сценариев, которые оказались успешными по другим автотестам.

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

Для оперативного реагирования на проблемы со стабильностью у нас еженедельно назначается дежурный разработчик с каждой платформы, а также есть платформенная команда, в приоритетах которой помогать в таких ситуациях. Это позволяет всем остальным разработчикам продолжать спокойно работать над своими фичами в случае проблем с develop и не тратить время на переключение контекста.

Итоги

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

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

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

  • Чтобы экономить время на безуспешных прогонах, валидируем корректность настройки нашего тестового окружения и стараемся быть fail fast.

  • Для борьбы с нестабильными автотестами мы используем надежные инструменты — Kaspresso и Marathon, плюс подготовили инструменты для проверки стабильности автотестов во время их написания автотестировщиками.

  • Остаемся гибкими. Иногда, чтобы не блочиться в случае проблем, можем позволить себе нарушать правила, но только когда это действительно разумно и оправдано.

А как там с деньгами обстоит вопрос?

Так всё-таки, насколько дорого поддерживать стабильность инфраструктуры и автоматизаций? Стоит ли оно того? Окупают ли затраты получаемый эффект?

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

Проблемы с автоматизациями не требуют переключения внимания всех разработчиков. Нам вполне достаточно одного еженедельного дежурного. На самом деле, большую часть времени он занимается не проблемами с инфраструктурой, а фиксами багов, которые просачиваются в релиз в том случае, когда связанная с ними функциональность еще не покрыта автотестами. Также у нас есть платформенная команда по два человека на каждую платформу. Они тратят около 10% своего времени на оперативное реагирование в случае возникновения проблем.

Вполне возможно, что на других масштабах описанные мной подходы будут работать совсем по-другому. И соотношение затрат к эффекту будет отличаться. На текущий момент у нас 11 разработчиков на каждую платформу, 2 приложения и 5 команд.

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

Всем стабильных автоматизаций и надежной инфраструктуры, до новых встреч в нашем оhэhэнном блоге!

Полезные ссылки

  • ТОП-5 вопросов начинающего автоматизатора про автотесты: YouTube, Habr

  • Джентельменский релиз (подробнее об автоматизациях релизного цикла в hh): YouTube, Habr

  • Эволюция CI в Android (подробнее про технические аспекты нашего CI): YouTube, Habr

  • Kaspresso

  • Marathon

  • avito-android

  • Наш Telegram-чат, где вы можете обсудить с нами любые вопросы по мобильной разработке и тестированию

© Habrahabr.ru