Привет Хабр! Я Арман, Senior QA, тестирую android уже 4 года. Если вам интересно узнавать больше полезного и интересного, не только про мобильное тестирование, то держите мой канал t.me/LilBugHunters, здесь я делюсь короткими полезными заметками для QA-специалистов.
Эта статья будет посвящена разбору жизненного цикла activity в android для ручного тестировщика. Начну сразу с самого главного – зачем вам, тестировщику, вообще вникать в то, что такое жизненный цикл activity. После уже рассмотрим как activity работает, какие бывают состояния и вообще какие кейсы можно отловить, зная всё это и какие инструменты для этого нужно использовать.
Некоторые моменты здесь могут показаться сложными. Чтобы упростить понимание, я дописал программу для практики работы с логами. Теперь она отображает (и логирует) вызовы всех основных методов жизненного цикла activity.
В ней вы можете на практике увидеть, что происходит с приложением в различных тестовых кейсах, которые придумайте сами.
Скачать можно здесь: AndroidTestLogsGenerator
Что это и зачем это тестировщику?
Activity – это основной компонент android приложения. Если очень просто, то activity – это экран приложения. При запуске приложения запускается его главное activity. Приложение может иметь несколько activity и переключаться между ними, открывая разные экраны. Каждое activity имеет свой жизненный цикл – набор состояний, через которое оно проходит, в зависимости от действий пользователя или системы.
Давайте взглянем одним глазком на него (пока одним глазком дальше будет подробнее) и попытаемся понять, что он из себя представляет.
Жизненный цикл activity
Мы видим, что каждое состояние приложения сопровождается callback-функцией, которая вызывается при изменении состояния приложения автоматически.
Каждая функция в жизненном цикле activity (onCreate(), onStart() и т.д.) содержит код, который выполняется при наступлении определенных событий. Этот код, как и код любой другой функциональности, нуждается в тестировании.
Что-то из этого вы и так тестируете, но не понимая как оно работает, например, поведение при повороте экрана. Некоторые аспекты трудно догадаться как протестировать без понимания жизненного цикла activity. Или можно посчитать некоторые проверки бесполезными. Как пример, тестирование с включенной функцией “Don't keep activities”. В официальной документации android говорится, что это нужно для экономии батареи, это очень смешно, учитывая, что это настройка для разработчиков и нужна для тестирования, о чём как раз расскажу дальше.
Также знание жизненного цикла позволит вам строить правильные гипотезы при поиске шагов для воспроизведения бага. Например, когда пользователи жалуются, что у них пропадает вся введённая информация после сворачивания приложения, но у вас это не воспроизводится.
Все эти моменты и кейсы мы рассмотрим и разберём далее.
Жизненный цикл activity
Теперь давайте подробнее взглянем на жизненный цикл activity. На схеме от Google представлено 4 состояния activity. Однако, для нас важен не только сам факт нахождения в определенном состоянии, но и процесс перехода между ними. Какие функции вызываются при этом, и к каким багам может привести неправильная работа с ними.
Поэтому давайте попробуем разбить жизненный цикл activity на набор логических процессов:
- Первый запуск
- Приостановка и восстановление работы
- Убийство и перезапуск
- Уничтожение
Первый запуск
В начале activity запускается, при этом подряд вызываются три метода: onCreate(), onStart(), onResume(). На этом этапе выполняются операции, необходимые для корректного отображения экрана.
Обычно тут проблем не возникает. Имею в виду проблемы в жизненном цикле, другие проблемы конечно могут быть. Единственное, если ваше приложение тяжёлое и ресурсов смартфона не хватает, могут быть различные ANR (Application Not Responding), или даже приложение закроется, так и не загрузившись.
Приостановка и восстановление работы
Другое activity может в любой момент перекрыть запущенное. Если оно перекрыло запущенное не полностью, то для запущенного будет вызвана функция onPause(). После возврата с того activity, что было поверх нашего, будет вызван метод onResume() и работа продолжится. При этом сценарии также чаще не возникает проблем.
Если бизнес-логика требует отслеживания таких кейсов приложением, то они также должны быть покрыты тест-кейсами.
Дополнительная информация
Стоит отметить, что при работе приложения в многооконном режиме, при потере и получении фокуса, вместо onPause() и onResume(), вызывается метод onTopResumedActivityChanged(isTopResumedActivity). Параметр isTopResumedActivity указывает, получило или потеряло фокус наше activity.
Если activity полностью перекрывается другим и становится невидимым, то после onPause() вызывается onStop(). Простой пример: вы свернули приложение. После этого вызывается onSaveInstanceState() для сохранения состояния activity. Это позволит приложению восстановиться, если оно будет убито системой. Если activity не было убито, пока находилось в фоне, то при возврате к нему восстановление состояния не производится, так как состояние не было потеряно. Вместо этого вызовется последовательно onRestart() > onStart() > onResume(). В этом кейсе стоит протестировать работу с внутренней базой данных приложения.
Убийство и перезапуск
Система может убить ваше activity по разным причинам, после чего потребуется восстановить его состояние. Процесс сохранения и восстановления данных может быть сложным и привести к потере данных пользователя или крашу приложения. А также могут появиться проблемы с визуальным отображением, при неправильном восстановлении.
Как писал выше, после onStop() вызывается onSaveInstanceState(), чтобы сохранить состояние activity. Это означает, что система готова к вытеснению процесса из-за нехватки ресурсов или смены конфигурации. Например, при повороте экрана или смене языка. Подробнее об этом поговорим в разделе “Тестовые сценарии, баги и инструменты”.
Например, мы свернули приложение и открыли игру. Из-за нехватки памяти система убила наше приложение в фоне. При повторном открытии приложения, если посмотреть на схему жц activity, то после убийства activtiy, вызывается onCreate() точно так же как при старте, но есть разница. Функция onCreate() помимо запуска приложения может принимать параметр savedInstanceState. Этот параметр содержит состояние, сохраненное ранее методом onSaveInstanceState(). Если параметр не пустой, приложение должно восстановить состояние activity из этих данных.
Конечно, приложение может просто запуститься с нуля. Однако, представьте ситуацию: пользователь заполнил длинную анкету, свернул приложение из-за звонка, а вернувшись, обнаружил, что все данные потеряны. Не очень приятно, согласитесь?
Дополнительная информация
В зависимости от решения разработчика, код, отвечающий за восстановление экрана после уничтожения, может выполняться не в onCreate(), а в onRestoreInstanceState(). Она также принимает параметр savedInstanceState. Эта функция вызывается после onStart(), поэтому для тестовых сценариев не имеет большого значения, где именно происходит восстановление данных.
Уничтожение activity
Уничтожение activity происходит тогда, когда система уверена, что данное activity больше не нужно. Например, когда пользователь удаляет приложение из списка недавних или когда нажимает кнопку назад для возврата на предыдущий экран (текущий уничтожается). Если activity было уничтожено, а не убито, то при повторном открытии восстановление состояния не происходит.
Если activity уничтожается, должна вызываться функция onDestroy(). Однако, система не гарантирует, что она будет вызвана.
В течение жизненного цикла activity происходит множество событий, не все из которых отражены на схеме. Например, на схеме не отражены такие важные этапы, как сохранение и восстановление состояния. Но в целом для целей тестирования представленной здесь информации будет достаточно. Однако, если вы хотите узнать больше о событиях и callback-функциях android, можете перейти к официальной документации Activity.
Тестовые сценарии, баги и инструменты
Надеюсь, теперь появилось понимание, как изнутри работает приложение, которое мы тестируем. Теперь нужно понять, что именно мы должны тестировать и какие проблемы могут возникнуть. Также обсудим тестовые инструменты, в частности настройку “Don’t keep activities” и почему тестирования нельзя обходиться без него.
Помимо очевидных проблем, таких как некорректные размеры элементов при повороте экрана или артефакты при смене темы, неправильная работа с жизненным циклом activity может привести к более серьезным последствиям.
Согласитесь, неприятно, когда после случайного поворота экрана всё написанное пользователем стирается, или пропадает какое-нибудь важное всплывающее окно, где была кнопка продолжить. Ещё хуже краши после того, как свернулся на звонок, а потом вернулся в приложение.
Есть несколько сценариев, которые так или иначе надо проверить, если мы говорим о тестировании правильности работы приложения в жц activity в общем и при сохранении и восстановлении activity в частности:
Смена конфигурации
Действия пользователя
Убийство процесса системой
Смена конфигурации
Простыми словами, смена конфигурации – это изменение состояния системы, которое влияет на доступные вашему приложению ресурсы. При изменении конфигурации, система обычно убивает ваше activity и пересоздаёт его с нуля (onCreate()), используя сохранённое состояние.
Параметров конфигурации довольно много, не буду их всех перечислять, если интересно можно найти их в документации android. Опишу только основные категории, которые важно знать, а также, что происходит с вашим приложением в момент возникновения таких событий.
Параметры, влияющие на отображение приложения (поворот экрана, смена темы, изменение размера шрифта в системе и т.д.). Activity будет перезапущено, если при возврате к нему конфигурация изменилась и не будет, если наоборот. Например, если увеличить шрифт, но затем вернуть его как было перед открытием приложения, то activity не будет перезапущено.
Изменение параметров конфигурации, не влияющих напрямую на интерфейс, обычно не приводит к перезапуску activity. Например, вставка SIM-карты вызовет только метод onConfigurationChanged(). Приложение должно самостоятельно обработать это событие, если это необходимо.
Android позволяет приложению самостоятельно обработать любую смену конфигурации, для этого они прописываются в манифесте приложения в специальном поле. Если конфигурация, например, поворот экрана (orientation), прописана в манифесте, система не будет убивать приложение и перезапускать activity. Вместо этого будет вызван метод onConfigurationChanged(), позволяя приложению самостоятельно обработать событие.
Если ваше приложение должно быть чувствительно к параметрам из конфигурации, проверьте вручную и уточните у разработчика как оно должно обрабатывать смену конкретного конфига.
События при смене конфигурации (видео)
При тестировании новой функциональности обязательно проверьте ее поведение при смене конфигурации. Проверьте, как минимум, корректность отображения при поворотах экрана и смене темы. В идеале также проверьте, как приложение реагирует на изменение шрифтов и размеров элементов в настройках системы.
Действия пользователя
Естественно действия пользователя напрямую (свернул приложение) или косвенно (запустил много приложений) влияют на жизненный цикл вашего приложения и activity в частности.
Основные сценарии вызываемые пользователем достаточно очевидны:
Свернуть-развернуть приложение – onStop() -> onRestart();
Остановить приложение, убрать его из списка недавних – приложение завершает свой жизненный цикл. При повторном открытии оно запустится с нуля, без восстановления данных, поскольку пользователь ожидал, что приложение закроется;
Открыть в окне или в режиме splitscreen – по сути тоже, что было описано выше изменение конфигурации. В таком состоянии activity может получать и терять фокус;
Однако, есть еще один интересный сценарий, который также приводит к остановке приложения, но затем восстанавливает его из сохранённого состояния:
Отобрать разрешение у приложения – если у приложения есть постоянное разрешение, то при его отзыве в настройках приложение будет остановлено. Фоновая работа прекратится, однако, при открытии приложения оно восстановит состояние и activity, которое было открыто.
Видим, что в отличие от смены конфигурации, в этом случае приложение останавливается сразу как только пользователь отбирает разрешение.
Данный кейс интересен тем, что с помощью него можно проверить как приложение восстановит работу, если в момент отбора разрешения непосредственно работало с сущностями использующими эти разрешения (например, была открыта камера или галерея). В идеале необходимо заново запросить их при восстановлении.
Убийство процесса системой
Ну и вишенкой на торте у нас убийство процесса системой. О сколько боли и страданий было, когда ты играешь в свою любимую игрушку на слабеньком fly (если помните такое), а она вылетает в самой середине миссии и приходиться начинать всё с начала.
Это я описал один из сценариев, когда приложение убивается системой из-за нехватки ресурсов, в момент активности. Однако, чаще всего приложение или activity убивается системой для высвобождения ресурсов после того, как оно скрылось с экрана (например, после сворачивания или перехода к другому activity).
Таким образом, мы имеем два варианта:
Activity на экране, но ресурсов катастрофически не хватает поэтому система убивает и её
Activity скрыто, и активному приложению/activity требуются ресурсы
В первом случае, к сожалению, при открытии приложения оно запустится с нуля, поскольку не было возможности сохранить состояние activity.
Второй случай более интересен, так как activity успевает вызвать onSaveInstanceState() и сохранить свое состояние перед тем, как будет убито системой. Это означает, что при повторном открытии приложения оно должно восстановить свое состояние из сохранённых данных.
Вы можете спросить: чем же это отличается от смены конфигурации? Недостаточно ли просто проверить эти сценарии, ведь и в том, и в другом случае мы перезапускаем приложение и восстанавливаем его из сохранённого состояния? Однако, здесь есть важное отличие, которое делает обязательной проверку этого сценария отдельно от смены конфигурации.
При смене конфигурации состояние приложения кэшируется. Система, зная, что activity будет пересоздано, использует это кэшированное состояние, не тратя ресурсы на его упаковку и распаковку. Таким образом, то же самое состояние используется в новой конфигурации. Следовательно, код, отвечающий за упаковку и распаковку состояния приложения, не выполняется. Поэтому мы не можем проверить, что этот код работает правильно, если будем тестировать только смену конфигурации приложения.
А вот если activity убито системой в фоне и потом открыто, то здесь происходит выполнение полного сценария запаковки и распаковки состояния приложения. При таком кейсе мы легко отловим краши или потери данных, вызванные некорректным сохранением состояния.
Я постарался объяснить простыми словами, но если хотите подробностей, то здесь есть разбор на английском данной ситуации.
Для подтверждения моих слов, я настроил в приложении подсчёт хэш суммы bundle – пакета с сохранённым состоянием приложения, при вызове callback-функций его использующий, давайте сравним эти два сценария:
Как мы видим, при смене конфигурации, hash у bundle не изменяется, а после убийства системой, когда приложение было свёрнуто, hash во время сохранения и восстановления разный.
Как это тестировать?
Важно отметить, что это довольно распространенный сценарий, когда приложение убивается в фоне и при открытии оно должно правильно восстановить состояния activity. Его тестирование не менее важно, чем проверка поведения приложения при поворотах экрана и других изменениях конфигурации.
Возникает вопрос: как это тестировать? Неужели нужно каждый раз сворачивать приложение и ждать, пока система его убьет?
“Don’t keep activities” (DKA) – к счастью, android предоставляет такую возможность. В меню разработчика есть пункт “Don’t keep activities” (или “Вытеснение фоновых Activity”, или “Не сохранять действия” – каждый производитель переводит по-своему). При включении этой опции все фоновые activity будут убиваться системой сразу после того, как исчезнут с экрана.
logcat в Android Studio – если у вас debug версия приложения, то вы можете убить процесс прямо из logcat в Android Studio. Просто найдите строку логов вашего приложения, нажмите правую кнопку и выберите kill process. Подробнее как работать с logcat можете прочитать в моём гайде.
adb shell am kill <имя_пакета> – сверните ваше приложение и вызовите эту команду, приложение будет закрыто. Эта команда не работает, если приложение активно.
UPD. В комментариях возникли вопросы, отмечу, что DKA позволяет проверить сценарии корректного сохранения и восстановления именно activity, он убивает каждое отдельное activity как только вы с него уходите, но не весь процесс. Убийство процесса собственно наоборот убивает весь процесс, а следовательно и все activity в нём запущенные (разные activity могут быть запущенны в разных процессах, но по умолчанию запускаются в основном).
Хоть и тот и другой способ позволяют проверить корректность восстановления activity из сохранённого состояния, эту разницу нужно держать в голове. Для тестирования честного сценария убийства системой, стоит воспользоваться способом через убийство процесса.
DKA позволяет покрыть такие сценарии, как навигация внутри приложения между activity, а также сценарии утечки памяти, при не правильном освобождении ресурсов при вызове onDestroy(). Убийство же процесса наоборот освобождает все ресурсы с ним связанные полностью, и onDestroy() уже не вызывается.
Заключение
Надеюсь, я помог разобраться с тем, что такое жц activity, зачем его знать, и зачем нужен этот don’t keep activities. Подведем итоги:
Жизненный цикл activity позволяет нам понять, как наше приложение/activity должно вести себя в различных ситуациях
Основная задача – протестировать код, который выполняется при наступлении событий жизненного цикла activity
Приложение, а в частности activity, может быть выгружено из памяти системой в любой момент. При повторном открытии приложение должно корректно восстановить свое состояние
Повороты экрана, изменение темы приложения, увеличение шрифта, отзыв разрешений — все это приводит к перезапуску приложения
Есть два сценария сохранения-восстановления состояния: при смене конфигурации, когда состояние приложения просто кэшируется, и при убийстве activity, когда состояние приложения упаковывается, а затем распаковывается. Важно протестировать оба.
Если у вас возникли вопросы или что-то осталось непонятным, пишите в комментариях. Я постараюсь ответить на всё и если надо поправить это в статье.