Когда ваше приложение умирает – жизненный цикл activity в Android для QA |
17.09.2024 00:00 |
Я Арман, Senior QA, тестирую android уже 4 года. Если вам интересно узнавать больше полезного и интересного, не только про мобильное тестирование, то держите мой канал t.me/LilBugHunters, здесь я делюсь короткими полезными заметками для QA-специалистов. Эта статья будет посвящена разбору жизненного цикла activity в android для ручного тестировщика. Начну сразу с самого главного – зачем вам, тестировщику, вообще вникать в то, что такое жизненный цикл activity. После уже рассмотрим как activity работает, какие бывают состояния и вообще какие кейсы можно отловить, зная всё это и какие инструменты для этого нужно использовать. Некоторые моменты здесь могут показаться сложными. Чтобы упростить понимание, я дописал программу для практики работы с логами. Теперь она отображает (и логирует) вызовы всех основных методов жизненного цикла activity. В ней вы можете на практике увидеть, что происходит с приложением в различных тестовых кейсах, которые придумайте сами. Скачать можно здесь: AndroidTestLogsGenerator Что это и зачем это тестировщику?Activity – это основной компонент android приложения. Если очень просто, то activity – это экран приложения. При запуске приложения запускается его главное activity. Приложение может иметь несколько activity и переключаться между ними, открывая разные экраны. Каждое activity имеет свой жизненный цикл – набор состояний, через которое оно проходит, в зависимости от действий пользователя или системы. Давайте взглянем одним глазком на него (пока одним глазком дальше будет подробнее) и попытаемся понять, что он из себя представляет. Мы видим, что каждое состояние приложения сопровождается callback-функцией, которая вызывается при изменении состояния приложения автоматически. Каждая функция в жизненном цикле activity (onCreate(), onStart() и т.д.) содержит код, который выполняется при наступлении определенных событий. Этот код, как и код любой другой функциональности, нуждается в тестировании. Что-то из этого вы и так тестируете, но не понимая как оно работает, например, поведение при повороте экрана. Некоторые аспекты трудно догадаться как протестировать без понимания жизненного цикла activity. Или можно посчитать некоторые проверки бесполезными. Как пример, тестирование с включенной функцией “Don't keep activities”. В официальной документации android говорится, что это нужно для экономии батареи, это очень смешно, учитывая, что это настройка для разработчиков и нужна для тестирования, о чём как раз расскажу дальше. Также знание жизненного цикла позволит вам строить правильные гипотезы при поиске шагов для воспроизведения бага. Например, когда пользователи жалуются, что у них пропадает вся введённая информация после сворачивания приложения, но у вас это не воспроизводится. Все эти моменты и кейсы мы рассмотрим и разберём далее. Жизненный цикл 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 в частности. Основные сценарии вызываемые пользователем достаточно очевидны:
Однако, есть еще один интересный сценарий, который также приводит к остановке приложения, но затем восстанавливает его из сохранённого состояния:
Видим, что в отличие от смены конфигурации, в этом случае приложение останавливается сразу как только пользователь отбирает разрешение. Данный кейс интересен тем, что с помощью него можно проверить как приложение восстановит работу, если в момент отбора разрешения непосредственно работало с сущностями использующими эти разрешения (например, была открыта камера или галерея). В идеале необходимо заново запросить их при восстановлении. Убийство процесса системойНу и вишенкой на торте у нас убийство процесса системой. О сколько боли и страданий было, когда ты играешь в свою любимую игрушку на слабеньком fly (если помните такое), а она вылетает в самой середине миссии и приходиться начинать всё с начала. Это я описал один из сценариев, когда приложение убивается системой из-за нехватки ресурсов, в момент активности. Однако, чаще всего приложение или activity убивается системой для высвобождения ресурсов после того, как оно скрылось с экрана (например, после сворачивания или перехода к другому activity). Таким образом, мы имеем два варианта:
В первом случае, к сожалению, при открытии приложения оно запустится с нуля, поскольку не было возможности сохранить состояние activity. Второй случай более интересен, так как activity успевает вызвать onSaveInstanceState() и сохранить свое состояние перед тем, как будет убито системой. Это означает, что при повторном открытии приложения оно должно восстановить свое состояние из сохранённых данных. Вы можете спросить: чем же это отличается от смены конфигурации? Недостаточно ли просто проверить эти сценарии, ведь и в том, и в другом случае мы перезапускаем приложение и восстанавливаем его из сохранённого состояния? Однако, здесь есть важное отличие, которое делает обязательной проверку этого сценария отдельно от смены конфигурации.
Я постарался объяснить простыми словами, но если хотите подробностей, то здесь есть разбор на английском данной ситуации. Для подтверждения моих слов, я настроил в приложении подсчёт хэш суммы bundle – пакета с сохранённым состоянием приложения, при вызове callback-функций его использующий, давайте сравним эти два сценария: Как мы видим, при смене конфигурации, hash у bundle не изменяется, а после убийства системой, когда приложение было свёрнуто, hash во время сохранения и восстановления разный. Как это тестировать?Важно отметить, что это довольно распространенный сценарий, когда приложение убивается в фоне и при открытии оно должно правильно восстановить состояния activity. Его тестирование не менее важно, чем проверка поведения приложения при поворотах экрана и других изменениях конфигурации. Возникает вопрос: как это тестировать? Неужели нужно каждый раз сворачивать приложение и ждать, пока система его убьет?
UPD. В комментариях возникли вопросы, отмечу, что DKA позволяет проверить сценарии корректного сохранения и восстановления именно activity, он убивает каждое отдельное activity как только вы с него уходите, но не весь процесс. Убийство процесса собственно наоборот убивает весь процесс, а следовательно и все activity в нём запущенные (разные activity могут быть запущенны в разных процессах, но по умолчанию запускаются в основном). Хоть и тот и другой способ позволяют проверить корректность восстановления activity из сохранённого состояния, эту разницу нужно держать в голове. Для тестирования честного сценария убийства системой, стоит воспользоваться способом через убийство процесса. DKA позволяет покрыть такие сценарии, как навигация внутри приложения между activity, а также сценарии утечки памяти, при не правильном освобождении ресурсов при вызове onDestroy(). Убийство же процесса наоборот освобождает все ресурсы с ним связанные полностью, и onDestroy() уже не вызывается. ЗаключениеНадеюсь, я помог разобраться с тем, что такое жц activity, зачем его знать, и зачем нужен этот don’t keep activities. Подведем итоги:
Если у вас возникли вопросы или что-то осталось непонятным, пишите в комментариях. Я постараюсь ответить на всё и если надо поправить это в статье. |