Автоматическое тестирование приложений
Тестирование Android-приложений в масштабе Netflix
Тестирование постоянно развивается по мере того, как вы узнаете, что работает, или появляются новые технологии и фреймворки.
Тестирование в Netflix постоянно развивается. Для того чтобы полностью понять, куда оно движется и почему находится в текущем состоянии, важно также понять исторический контекст того, где оно было.
Приложение для Android было запущено 14 лет назад. Изначально это было гибридное приложение (native+webview), но из-за проблем с производительностью и сложностью создания пользовательского интерфейса, который бы ощущался/действовал как нативный, оно было преобразовано в полностью нативное приложение. Как и большинство старых приложений, оно находится в процессе преобразования в Jetpack Compose. Текущая кодовая база составляет около 1 миллиона строк кода на Java/Kotlin, распределенного по 400 с лишним модулям, и, как и в большинстве старых приложений, здесь также есть модуль-монолит, поскольку оригинальное приложение представляло собой один большой модуль. Над приложением работает команда из примерно 50 человек.
Одно время существовала специальная мобильная команда SDET (Software Developer Engineer in Test), которая занималась написанием всех тестов для устройств, следуя обычной схеме работы с разработчиками и менеджерами продукта, чтобы понять, какие функции они тестируют, и создать тест-планы для всех автоматизированных тестов. В Netflix SDET были разработчиками, специализирующимися на тестировании. Они писали автоматизированные тесты с помощью Espresso или UIAutomator, они также создавали фреймворки для тестирования и интегрировали сторонние фреймворки для тестирования. Разработчики функций писали модульные тесты и тесты Robolectric для своего кода. Специальная команда SDET была расформирована несколько лет назад, и теперь автоматизированные тесты принадлежат каждой из функциональных подкоманд, но по-прежнему есть 2 вспомогательных SDET команды, которые помогают различным командам по мере необходимости. QA (Quality Assurance) вручную тестирует релизы перед их загрузкой в качестве финального «дымового теста».
В мире потокового мультимедиа одной из интересных проблем является огромная экосистема устройств воспроизведения, использующих приложение. Нам нравится поддерживать хороший опыт на устройствах с малым объемом памяти/медленных устройствах (например, Android Go), обеспечивая при этом премиальный опыт на более дорогих устройствах. У складных устройств некоторые не сообщают о наличии датчика складывания. Мы поддерживаем устройства до Android 7.0 (API24), но в ближайшее время мы установим минимальную версию Android 9. Некоторые версии Android, разработанные конкретными производителями, также имеют свои причуды. Поэтому физические устройства составляют огромную часть нашего тестирования.
Текущий подход к тестированию
Как уже говорилось, разработчики функций теперь занимаются всеми аспектами тестирования своих фич. Наши уровни тестирования выглядят следующим образом:
Пирамида тестирования, показывающая слои снизу вверх: модульные тесты, скриншот тесты, сквозные автоматизированные тесты, дымовые тесты
Однако из-за интенсивного использования тестирования на физических устройствах и унаследованных частей кодовой базы наша пирамида тестирования больше похожа на песочные часы или перевернутую пирамиду, в зависимости от того, в какой части кода вы находитесь. Новые функции действительно имеют эту более типичную форму пирамиды тестирования.
Скриншот тестирование также проводится на нескольких уровнях: UI компонент, UI макет и макет экрана интеграции с устройством. Первые два уровня — это действительно юнит-тесты, потому что они не делают никаких сетевых вызовов. Последний является заменой большинства ручных QA-тестов.
Фреймворки для модульных тестов
Юнит-тесты используются для тестирования бизнес-логики, которая не зависит от конкретного поведения устройства/интерфейса. В старых частях приложения мы используем RxJava для асинхронного кода и тестирования потоков. В новых частях приложения для потоков состояний используются Kotlin Flows и Composables, которые гораздо проще в рассуждениях и тестировании по сравнению с RxJava.
Для модульного тестирования мы используем следующие фреймворки:
- Strikt: для утверждений, потому что он имеет свободный API, как AssertJ, но написан для Kotlin
- Turbine: для недостающих частей в тестировании Kotlin Flow
- Mockito: для подражания любым сложным классам, не имеющим отношения к текущему тестируемому модулю кода
- Hilt: для подстановки тестовых зависимостей в наш граф инъекций зависимостей
- Robolectric: для тестирования бизнес-логики, которая должна каким-то образом взаимодействовать с сервисами/классами Android (например, parcelables или Services)
- Фреймворк тестирования A/B-тестов/фиче флагов: позволяет переопределить тест автоматизации для конкретного A/B-теста или включить/выключить конкретную функцию
Разработчикам рекомендуется использовать обычные модульные тесты перед переходом на Hilt или Robolectric, так как время выполнения увеличивается в 10 раз с каждым шагом при переходе от обычных модульных тестов -> Hilt -> Robolectric. Mockito также замедляет сборку при использовании инлайн-моков, поэтому использовать инлайн-моки не рекомендуется. Тесты на устройствах на несколько порядков медленнее, чем любые из этих типов юнит-тестов. Скорость тестирования важна в больших кодовых базах.
Нестабильность в модульных тестах
Поскольку юнит-тесты блокируются в нашем CI-конвейере, минимизация ненадежности очень важна. Как правило, есть две причины, приводящие к «дряблости»: оставление некоторого состояния для следующего теста и тестирование асинхронного кода.
Классы юнит-тестов JVM (Java Virtual Machine) создаются один раз, а затем методы тестов в каждом классе вызываются последовательно; инструментальные тесты, по сравнению с этим, запускаются с самого начала, и единственное время, которое вы можете сэкономить, — это установка APK. Из-за этого, если метод тестирования оставит измененное глобальное состояние в зависимых классах, следующий метод тестирования может не сработать. Глобальное состояние может принимать различные формы, включая файлы на диске, базы данных на диске и общие классы. Использование инъекции зависимостей или пересоздание всего, что было изменено, решает эту проблему.
В асинхронном коде всегда возможны сбои, поскольку несколько потоков изменяют разные вещи. Диспетчеры тестов (корутины Kotlin) или планировщики тестов (RxJava) могут быть использованы для контроля времени в каждом потоке, чтобы сделать ситуацию детерминированной при тестировании определенных условий гонки. Это сделает код менее реалистичным и, возможно, пропустит некоторые тестовые сценарии, но предотвратит сбои в тестах.
Фреймворки для скриншот-тестирования
Фреймворки для тестирования скриншотов важны, потому что они тестируют то, что видно, а не поведение. В результате они являются лучшей заменой ручному QA-тестированию любых статичных экранов (анимацию по-прежнему сложно тестировать с помощью большинства фреймворков для скриншот-тестирования , если только фреймворк не умеет управлять временем).
Мы используем различные фреймворки для тестирования скриншотов:
- Paparazzi: для компонентов Compose UI и макетов экрана; сетевые вызовы не могут быть сделаны для загрузки изображений, поэтому вы должны использовать статические ресурсы изображений или загрузчик изображений, который рисует шаблон для запрашиваемых изображений (мы делаем и то, и другое)
- Локализационное тестирование скриншотов: создание скриншотов экранов работающего приложения во всех локализациях, чтобы наши UX-команды могли проверить их вручную
- Скриншот-тестирование на устройствах: тестирование, используемое для проверки визуального поведения работающего приложения
Тестирование доступности Espresso: это также одна из форм скриншот-тестирования, где размеры/цвета различных элементов проверяются на доступность; это также было несколько болезненным моментом для нас, поскольку наша команда UX приняла стандарт WCAG 44dp для минимального размера касания вместо 48dp в Android.
Фреймворки для тестирования на устройствах
Наконец, у нас есть тесты на устройствах. Как уже говорилось, они в разы медленнее, чем тесты, которые могут выполняться на JVM. Они заменяют ручной QA и используются для дымового тестирования общей функциональности приложения.
Однако, поскольку запуск полностью рабочего приложения в тесте имеет внешние зависимости (бэкенд, сетевая инфраструктура, лабораторная инфраструктура), тесты на устройствах всегда будут в той или иной степени нестабильными. Это нельзя не подчеркнуть: несмотря на наличие повторных попыток, автоматические тесты на устройствах всегда будут нестабильными в течение определенного периода времени. Ниже мы расскажем о том, что мы делаем, чтобы справиться с этой проблемой.
Для тестирования на устройствах мы используем следующие фреймворки:
- Espresso: большинство тестов устройств используют Espresso, который является основным фреймворком Android для тестирования пользовательских интерфейсов
- PageObject test framework: внутренние экраны написаны как PageObject, которыми тесты могут управлять, чтобы облегчить миграцию с XML-макетов на Compose (подробнее см. ниже)
- UIAutomator: небольшой набор «дымовых» тестов использует UIAutomator для тестирования полностью обфусцированного бинарного файла, который будет загружен в магазин приложений (также известны как Release Candidate тесты)
- Система тестирования производительности: измеряет время загрузки различных экранов, чтобы проверить наличие регрессий
- Фреймворк перехвата/воспроизведения сетевых запросов: позволяет воспроизводить записанные вызовы API для снижения нестабильности тестов на устройствах
- Фреймворк мокирования бэкенда: тесты могут попросить бэкенд вернуть определенные результаты; например, наша главная страница имеет контент, который полностью управляется алгоритмами рекомендаций, поэтому тест не может детерминированно искать определенные названия, если тест не попросит бэкенд вернуть определенные видео в определенных состояниях (например, «скоро будет») и определенные строки с определенными названиями (например, строка Coming Soon с определенными видео)
- Фреймворк A/B-тестов/фиче флагов: позволяет переопределить тест автоматизации для определенного A/B-теста или включить/выключить определенную функцию
- Фреймворк для тестирования аналитики: используется для проверки последовательности событий аналитики из набора действий на экране; аналитика наиболее подвержена сбоям при смене экранов, поэтому это очень важно для тестирования
PageObjects и шаги тестирования
Шаблон проектирования PageObject зародился как веб-шаблон, но был применен к мобильному тестированию. Он отделяет тестовый код (например, нажатие на кнопку Play) от кода, специфичного для экрана (например, механики нажатия на кнопку с помощью Espresso). Благодаря этому он позволяет абстрагировать тест от реализации (думайте об интерфейсах и реализации при написании кода). Вы можете легко заменить реализацию при необходимости при переходе от макетов XML к макетам Jetpack Compose, но сам тест (например, проверка входа в систему) останется прежним.
В дополнение к использованию PageObject для определения абстракции над экранами, у нас есть концепция «Шаги тестирования». Тест состоит из тестовых шагов. В конце каждого шага наша лаборатория устройств автоматически создает скриншот. Таким образом, разработчики получают раскадровку из скриншотов, показывающих ход выполнения теста. Если шаг теста завершился неудачей, это также будет четко указано (например, «не удалось нажать на кнопку Play»), поскольку шаг теста имеет поле «резюме» и «описание ошибки».
Инфраструктура автоматизации тестирования на устройствах
Netflix, вероятно, была одной из первых компаний, у которой была выделенная лаборатория для тестирования на устройствах. Это было еще до появления сторонних сервисов, таких как Firebase Test Lab. Инфраструктура нашей лаборатории обладает многими функциями, которые вы ожидаете получить:
- таргетинг на определенные типы устройств
- захват видео во время выполнения теста
- захват скриншотов во время выполнения теста
- захват всех логов
Интересные функции инструментария для устройств, которые уникальны для Netflix:
- Сотовая вышка, чтобы мы могли тестировать Wi-Fi и сотовые соединения — Netflix имеет свою собственную физическую сотовую вышку в лаборатории, к которой устройства могут подключаться
- Изменение условий сети, чтобы можно было имитировать медленные соединения
- Автоматическое отключение системных обновлений на устройствах, чтобы их можно было заблокировать на определенном уровне ОС
- Для установки/запуска тестов используются только чистые команды adb (вся эта инфраструктура была создана до появления таких фреймворков, как Gradle Managed Devices или Flank)
- Запуск набора автоматизированных тестов в A/B тестах
- Тестирование аппаратного/программного обеспечения для проверки того, что устройство не теряет кадры, для наших партнеров, чтобы убедиться, что их устройства поддерживают воспроизведение Netflix должным образом — у нас также есть квалификационная программа для устройств, чтобы убедиться, что они поддерживают HDR и другие кодеки должным образом
Если вам интересно узнать больше подробностей, загляните в технический блог Netflix.
Работа с нестабильностью тестов
Как уже говорилось выше, нестабильность тестов — одна из самых сложных вещей, связанных с нестабильными по своей природе тестами на устройствах. Инструментарий должен быть создан таким образом, чтобы:
- минимизировать нестабильность
- выявлять причины сбоев тестов
- уведомлять команды, которые работают с нестабильными тестами
В инструментарий, который мы создали для управления нестабильностью, есть:
- Автоматическое определение PR (Pull Request), в котором тест начал давать сбой, и уведомление авторов PR о том, что они вызвали сбой теста
- Тесты могут быть помечены как стабильные/нестабильные/отключенные вместо использования аннотаций
@Ignore
; это используется для временного отключения подмножества тестов, если есть проблемы с бэкендом, чтобы не было ложных срабатываний в PR - Автоматизация, позволяющая определить, можно ли перевести тест в Stable, используя свободные циклы устройства для автоматической оценки стабильности теста
- Автоматизированные правила IfTTT (If This Then That) для повторного прохождения тестов, игнорирования временных сбоев или ремонта устройства
- Отчеты о сбоях позволяют нам легко фильтровать сбои в зависимости от производителя устройства, ОС или клетки, в которой находится устройство. Например, здесь показано, как часто сбой теста происходит в течение определенного периода времени для этих факторов:
Сбои в тестировании за определенное время, сгруппированные по факторам среды, таким как staging/prod бэкэнд, версия ОС, телефон/планшет.
- Отчет о сбоях позволяет нам сортировать историю ошибок, чтобы определить наиболее распространенные причины сбоев в тесте, а также получить скриншоты:
- Тесты можно вручную настроить на многократное выполнение на разных устройствах, версиях ОС или типах устройств (телефон/планшет), чтобы воспроизвести неработающие тесты
Конвейеры CI/тестирования
У нас есть типичный CI-конвейер PR (Pull Request), который запускает модульные тесты (включая тесты Paparazzi и Robolectric), lint, ktLint и Detekt. Запуск примерно 1000 тестов на устройствах является частью процесса PR. В процессе PR подмножество дымовых тестов также запускается для полностью обфусцированного приложения, которое может быть отправлено в магазин приложений (предыдущие тесты устройств запускаются для частично обфусцированного приложения).
Дополнительные автоматические девайс тесты выполняются как часть нашего набора тестов после слияния. Когда сливаются партии PR, дополнительное покрытие обеспечивается автоматическими тестами, которые не могут быть запущены на PR, поскольку мы стараемся, чтобы набор автоматических тестов на устройствах для PR не превышал 30 минут.
Кроме того, существуют наборы Daily и Weekly. В них выполняются гораздо более длительные тесты автоматизации, поскольку мы стараемся, чтобы набор тестов после слияния не превышал 120 минут. Автоматические тесты, которые входят в эти наборы, обычно представляют собой длительные стресс-тесты (например, можете ли вы посмотреть сезон сериала без того, чтобы приложение не исчерпало память и не упало).
В идеальном мире у вас есть бесконечные ресурсы для проведения всех тестов. Если бы у вас было бесконечное количество устройств, вы могли бы проводить все тесты на них параллельно. Если бы у вас было бесконечное количество серверов, вы могли бы запускать все свои юнит-тесты параллельно. Если бы у вас было и то, и другое, вы могли бы запустить все на каждом PR. Но в реальном мире используется сбалансированный подход, при котором выполняется «достаточное» количество тестов на PR, postmerge и т.д., чтобы предотвратить появление проблем у пользователей, чтобы ваши клиенты получали лучший опыт, а команды разработки работали продуктивно.
Покрытие
Охват устройств — это набор компромиссов. На PR вы хотите максимизировать охват, но минимизировать время. При тестировании после слияния, ежедневном и еженедельном, время менее важно.
При тестировании на устройствах у нас есть двумерная матрица: версия ОС против типа устройства (телефон/планшет). Проблемы с версткой встречаются довольно часто, поэтому мы всегда проводим тесты на телефоне+планшете. Мы также добавляем автоматизацию для складных смартофнов, но у них есть свои сложности, например, возможность тестировать макеты до/после/в процессе сворачивания.
Для PR мы обычно используем так называемую «узкую сетку», что означает, что тест может работать на любой версии ОС. В Postmerge/Daily/Weekly мы используем так называемую «полную сетку», что означает, что тест запускается на всех версиях ОС. Компромисс заключается в том, что если произойдет сбой, характерный для конкретной ОС, это может выглядеть как некачественный тест для PR и это не будет обнаружено до более позднего времени.
Будущее тестирования
Тестирование постоянно развивается по мере того, как вы узнаете, что работает, или появляются новые технологии и фреймворки. В настоящее время мы изучаем возможность использования эмуляторов для ускорения PR. Мы также оцениваем возможность использования Roborazzi для сокращения скриншот-тестирования на устройствах, Roborazzi позволяет тестировать взаимодействия, а Paparazzi — нет. Мы создаем модульную систему «демо-приложений», которая позволит тестировать на уровне функций, а не на уровне приложений. Совершенствование тестирования приложений никогда не заканчивается!
-
Новости4 недели назад
Видео и подкасты о мобильной разработке 2025.16
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.17
-
Разработка4 недели назад
Расширенные архитектурные правила в SwiftLint: часть 1
-
Видео и подкасты для разработчиков4 недели назад
Не два байта переслать: эмуляция бесконтактных карт на мобильных устройствах