mobilegame-qaapp-updatesmigrationsreleaseqa

Тестирование обновлений — баги, которые видят только юзеры со старой версией

Есть особый сорт релизных багов: «после обновления слетел прогресс», «после обновления разлогинило», «обновился — крашится на старте». Их объединяет одно — на тестовом стенде их не существует. Стенд у нас чистый: свежая установка, свежая база, свежий логин. А юзер получает релиз не чистой установкой — он получает его обновлением поверх версии, которой пользовался полгода, со всеми накопленными данными. И тестировали мы, получается, не то, что приедет к людям.

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

Апдейт — это новый код, читающий старые данные

Чистая установка и обновление отличаются одним: при обновлении новому коду достаётся всё хозяйство старой версии. Локальная база (SQLite/Room/Realm/Core Data), SharedPreferences/UserDefaults, сериализованные объекты, кэшированные файлы, токены в Keychain/Keystore. Всё это писала старая версия — в старом формате.

Если схема базы поменялась, нужна миграция. Если поменялся формат сериализации — старый blob должен либо читаться, либо осознанно выбрасываться. Если переименовали ключ в настройках — старое значение должно переехать, а не потеряться. Каждое «поменяли формат хранения» в release notes разработчиков — это красный флаг для QA: здесь живёт потенциальный «слетел прогресс».

Матрица версий: юзеры не обновляются с N-1

Удобно думать, что юзер обновляется с прошлой версии на новую. На деле у стора длинный хвост: кто-то сидит на версии полугодовой давности с отключённым автообновлением — и прыгает сразу через пять релизов. Для него выполняется не одна миграция, а цепочка: все промежуточные, по порядку. И если миграция из версии 3 в 4 предполагала, что перед ней уже отработала 2→3 — а у юзера кастомный путь — получаем краш, который воспроизводится только у него.

Поэтому минимальный набор апдейт-тестов у меня такой: предыдущая версия → новая (основная масса юзеров), самая старая поддерживаемая → новая (худший случай цепочки миграций), и хотя бы одна «из середины». Из этого следует организационное требование, о котором забывают: архив старых билдов должен существовать. Если у команды нет возможности поставить версию трёхмесячной давности — тестировать обновления ей нечем.

Как это выглядит руками

Сценарий скучный, но именно он ловит целый класс багов: поставить старый билд, пожить в нём — залогиниться, накопить прогресс, поменять настройки, положить что-то в корзину, — и накатить новую версию поверх (adb install -r на Android, установка поверх через TestFlight/Xcode на iOS). После апдейта проверяю по списку: логин жив (не разлогинило), прогресс/данные на месте, настройки не сбросились, приложение вообще стартует. Плюс один прогон основных сценариев — не на чистой базе, а на этой, «пожившей».

Отдельная ловушка: установка через IDE или TestFlight не всегда идентична стор-обновлению (подписи, split APK, downloadable-контент), поэтому финальную проверку критичного релиза стоит делать максимально близким к стору способом — internal testing track в Google Play / TestFlight-билд той же конфигурации.

Обновление посреди жизни

Стор обновляет приложение в фоне — юзер об этом не просил. Значит, апдейт может случиться между сессиями с несохранённым состоянием: игра свёрнута посреди боя, в приложении заполнена половина формы, идёт загрузка файла. Возврат после обновления — что юзер видит? Восстановленное состояние, вменяемый рестарт сценария или потерю всего?

Идеального ответа тут не бывает, но поведение должно быть осознанным, а не случайным. Тест простой: свернуть приложение в «интересном» состоянии, обновить, вернуться.

Force update: экран, который тестируют реже всего

Рано или поздно появляется форс-апдейт: «эта версия больше не поддерживается, обновитесь». На Android для этого есть in-app updates API с двумя режимами — flexible (обновление качается в фоне, юзер продолжает работать) и immediate (блокирующий полноэкранный флоу). Что я проверяю в самом механизме: старая версия при включённом форсе действительно блокируется, кнопка «обновить» ведёт в стор на страницу приложения (а не в никуда), диалог нельзя обойти — свернуть/убить/повернуть экран/выключить сеть и продолжить пользоваться, и отдельно — что происходит, если стор недоступен или обновление ещё не раскатилось на страну юзера: вечный тупик или понятное сообщение?

И обратный кейс, который больно бьёт: форс-апдейт включили серверным конфигом раньше, чем новая версия реально доступна в сторе юзера. Приложение требует обновиться — стор говорит «у вас последняя версия». Юзер заперт. Этот сценарий стоит проверять на каждом включении форса.

Staged rollout: две версии живут одновременно

Релизы раскатываются постепенно — staged rollout в Google Play, phased release в App Store. Для QA из этого следуют две вещи. Первая — раскатку можно остановить, и мониторинг крашей/метрик по новой версии в первые часы — часть релизного процесса, а не опция. Вторая, менее очевидная: пока раскатка идёт, старая и новая версии живут в проде параллельно — днями, а с длинным хвостом необновившихся — месяцами. Значит, сервер и API обязаны держать обе: новый бэкенд не может молча поменять контракт, который читает старый клиент. Каждое серверное изменение проверяется на паре «старый клиент + новый сервер», не только на свежей связке.

Downgrade: официально нельзя, но случается

Откат на старую версию сторы юзерам не дают, но он всё равно случается: тестировщики и сайдлоад на Android, восстановление старого девайса из бэкапа, откат сборки во внутренних треках. И тогда старый код читает новые данные — миграций «вниз» никто не писал, и это почти гарантированный краш-луп: приложение падает на старте бесконечно, лечится только сносом с потерей данных. Минимум, который стоит знать про свой продукт: что именно происходит при даунгрейде — и если ответ «краш-луп», пусть это будет хотя бы задокументированное известное поведение, а тестовые инструкции команды прямо запрещают откат поверх данных.

Первый запуск после апдейта — это не первый запуск

Класс багов на стыке с онбордингом: после обновления приложение решает, что юзер новый — и показывает FTUE заново, сбрасывает флаг «туториал пройден», повторно просит разрешения или показывает выбор языка. Причина обычно та же: флаг хранился в месте, которое апдейт задел (или в новой версии переименовали ключ). Проверка входит в базовый прогон: обновиться и убедиться, что приложение помнит — мы тут не в первый раз. Ну и what’s new экран, если он есть: показывается один раз, закрывается, не появляется на каждом старте.

Антипаттерны

Тестировать только чистую установку. Главный. Чистая установка — это меньшинство реального трафика релиза; основная масса — обновления поверх.

Миграции без тестов на реальных старых данных. Юнит-тест миграции на синтетической базе — хорошо, но реальная база юзера со старой версии (объём! мусор! коллизии из ещё более старых версий!) регулярно находит то, что синтетика не находит.

Не хранить старые билды. Без архива билдов и старых данных апдейт-тестирование физически невозможно. Заведите полку со старыми версиями — и снапшоты «поживших» данных к ним.

Чек-лист тестирования обновления

  • Обновление с предыдущей версии: логин, данные, настройки, прогресс на месте
  • Обновление с самой старой поддерживаемой версии (цепочка миграций)
  • Прогон основных сценариев на «пожившей» базе, не на чистой
  • Апдейт при несохранённом состоянии: поведение осознанное
  • Force update: не обходится, ведёт в стор, вменяем при недоступном сторе
  • Форс не включается раньше, чем версия реально доступна юзерам
  • Старый клиент + новый сервер: контракт API держит обе версии
  • Мониторинг крашей новой версии в первые часы раскатки, план halt
  • Downgrade: поведение известно и задокументировано
  • После апдейта нет повторного FTUE/разрешений, флаги пережили обновление
  • Архив старых билдов и снапшоты данных к ним существуют

Релиз — это не «новая версия работает». Релиз — это «путь каждого юзера со своей старой версии на новую работает». Чистая установка проверяет первое. Тестирование обновлений — второе, и именно там живут баги, которые видят все, кроме нас.