Как ломаются мобильные приложения

Почему формально рабочие части системы ломаются вместе — и как тестировать то, чего нет в документации.

Обложка: Как ломаются мобильные приложения

В мобильной разработке баг часто появляется не в одном экране, а на стыке нескольких частей системы. Веб создаёт данные, бэкенд их обрабатывает, мобильное приложение показывает результат пользователю, а релиз зависит от App Store, Google Play, устройства и версии ОС. Когда требования к каждой из этих частей пишутся отдельно — и никто не проверяет их вместе — появляются баги.

В Centicore Group разобрали несколько кейсов из практики мобильного тестирования: промокоды, которые нельзя активировать, рейтинг, который считал лишнюю звезду, пуши на iPad, UI, который ломал рабочий сценарий, и WebSocket, который оставался висеть после выхода из чата.

Промокод, который нельзя ввести

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

Проблема была в том, что требования к двум частям фичи никто не сравнивал между собой. Поле для активации в мобилке покрыто валидацией: максимум 20 символов, только латиница, никаких пробелов и спецсимволов. А форма создания в вебе — без каких-либо ограничений. Врач или администратор клиники мог написать в префиксе что угодно: кириллицу, кавычки, восклицательный знак, теоретически SQL-инъекцию (на боевом сервере не проверяли). Промокоды выпускались пачками и уходили пациентам.

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

Фиксить нужно мобилку, потому что генерация уже работает. А мобилка на тот момент полностью нативная, без динамических обновлений, значит полный цикл хотфикса: собрать сборку, отправить на ревью в App Store и Google Play, дождаться одобрения, выпустить. Многие крупные игроки к тому времени уже работали с динамическими обновлениями, это приложение нет.

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

Samsung, одна звезда и RatingBar

В финтех-приложении стандартная функция обратной связи — Voice of Client: пользователь оценивает консультацию в чате звёздами. Тестировщик нажимает на первую звезду — выбираются две. Нажимает на вторую — выбирается третья.

Первая гипотеза — кривой экран. Проверили на других устройствах: у коллег всё работает нормально. Версия ОС у всех Android 15, тестовый пользователь один и тот же, сборка идентичная. Добавить тестовое устройство в изолированную сеть банковского приложения непросто, но сделали. После нескольких итераций проверок выяснили, что баг воспроизводится только на живом железе Samsung — на эмуляторах и других производителях чисто.

Причина нашлась в устройстве компонента: Android RatingBar глубоко в иерархии наследует ProgressBar, а звёзды в нём скорее декоративный слой. Когда пользователь касается звезды, компонент вычисляет рейтинг через округление на основе параметра stepSize. На Samsung это округление стабильно уходило в большую сторону: касание по краю четвёртой звезды давало рейтинг 5. Фикс простой — stepSize выставляется в 0.01, а итоговое округление до целого числа делается вручную в листенере. После этого поведение стало одинаковым на всём железе.

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

iPadOS, которого не существовало

На iOS тестировать одно удовольствие: Apple настолько плотно контролирует железо, что ситуация, когда баг есть на iPhone 13, но нет на iPhone 15, практически невозможна. Граница, на которой что-то ломается в яблочной экосистеме, проходит между iPhone и iPad.

В финтех-приложении перестали работать пуш-уведомления на iPad. Сборка идентичная с iPhone, версия та же, настройки одинаковые. На iPhone регистрация в пуш-сервисе проходила штатно: приложение отправляло запрос на регистрацию устройства и получало подтверждение. На iPad в ответ прилетал код 400 с ошибкой «Provider UUID not found».

Первое подозрение было на заголовки запроса, но заголовки оказались чистыми. Причина нашлась в теле JSON-запроса: iPad и iPhone используют разные названия операционной системы: на телефоне это iOS, на планшете — iPadOS. Приложение передавало это название через атрибут OSName как есть, без нормализации. Бэкенд знал только «iOS» — при получении «iPadOS» возвращал ошибку, потому что такого значения в его словаре не существовало. Одна строка в JSON оставила целую категорию устройств без уведомлений.

Пофиксили на бэке: при получении значения «iPadOS» оно автоматически заменялось на «iOS» перед любой дальнейшей обработкой. Решение в лоб, зато надёжное. Могло быть значительно сложнее: Huawei без Google-сервисов с пуш-уведомлениями это отдельный класс проблем, где просто заменить одну строку — не получится.

Когда никто не проверял крайние сценарии

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

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

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

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

Где прячутся баги, которых нет в документации

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

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

Рекомендуем