Последнее время я пробую разные подходы к автоматизации UI-тестов мобильных приложений: чистые XCUITest и Espresso/Compose, Appium, Maestro, page object и небольшие DSL, разные способы ожиданий и борьбы с флаки-тестами. Довольно быстро упёрся в скучную, но вполне реальную проблему: а на чём всё это гонять?
Опенсорсные приложения, которые мне попадались, в основном делились на две группы. Одни есть только под Android, а мне хотелось сравнивать подходы на обеих платформах. Другие специально сделаны простыми: экран логина, список, карточка. Чтобы научиться водить симулятор, они отлично подходят, но в них почти нет тех багов, которые на моей практике создавали проблемы в реальных проектах. Например:
-
убыток отображается как прибыль;
-
двойной тап отправляет перевод дважды;
-
1,000.50парсится как1.0005, потому что на устройстве немецкая локаль; -
запоздавший ответ сети затирает свежее состояние;
-
CVV напечатан прямо на лицевой стороне карты.
Мне показалось, что для таких багов хорошо подходит финтех: деньги, авторизация, живые данные, сетевые тайминги и безопасность сходятся на нескольких экранах. В итоге я сделал свои приложения, по одному на платформу, где баги — это фича. Они заложены специально, описаны, и каждый можно включить или выключить.
-
ChaosBank-iOS — SwiftUI (плюс сборка на UIKit): https://github.com/VadimToptunov/ChaosBank-iOS
-
ChaosBank-Android — Kotlin + Jetpack Compose (плюс сборка на XML): https://github.com/VadimToptunov/ChaosBank-Android
Оба проекта под MIT. По функциональности это небольшой необанк с брокером акций и крипты, примерно в духе Revolut и Robinhood. Сейчас в нём 131 именованный дефект, и к каждому есть короткое письменное упражнение.
Начинал я это как стенд для собственных экспериментов, но, возможно, он пригодится и как тренировочная площадка тем, кто только начинает в мобильной разработке или мобильном QA. Ниже — как всё устроено, как я бы стал этим пользоваться и пара вещей, в которых я пока не уверен.
Как это выглядит
Это рабочее приложение, а не макет. Сначала веб-логин (специально внутри WKWebView / WebView, об этом ниже), потом OTP, 6-значный код и Face ID. Дальше: мультивалютная главная, переводы, обмен валют, история операций с поиском и пагинацией, рынки с тикающими ценами, тикет заявки, портфель с живым P&L и управление картой.
Android-версия старается быть портом один в один: те же экраны, те же дефекты, те же имена локаторов.
Корректный код и баги — отдельно
На этом решении держится бо́льшая часть проекта.
Самый очевидный способ сделать глючное приложение — просто написать кривой код. Для моих целей это работало плохо: сложно понять, поймал ли тест баг или упал по какой-то посторонней причине, а «правильное» поведение существует только у автора в голове. При сравнении фреймворков мне хотелось точно знать, что именно должен был поймать каждый прогон.
Поэтому продуктовая логика написана корректно (деньги в Decimal, правильная цена заявки, правильные балансы), а каждый баг — это маленькое переопределение за одной проверкой:
if Defects.isActive(.doubleCharge) { // маленькое изолированное «сломанное» поведение} else { // эталонная корректная реализация}
if (Defects.isActive(DefectId.doubleCharge)) { // маленькое изолированное «сломанное» поведение}
Это даёт простой способ проверить, что тест действительно что-то сторожит:
-
Запускаешь тест на профиле
clean. Он должен пройти. -
Перезапускаешь с включённым дефектом. Он должен упасть.
-
Если не упал, скорее всего, тест проверяет не то, что ты думаешь.
Вот как это выглядит на экране. Сборка та же, экран тот же. Слева clean, справа включены два дефекта безопасности:
Этот менее очевиден. Посмотрите на TSLA:
Слева TSLA в минусе на 290.56**. Все элементы на месте, поэтому тест, который проверяет только их наличие, пройдёт. Чтобы это заметить, нужна проверка значения.
И ещё один: общий баланс справа — это сумма счетов?
Нет. Карточки счетов листаются горизонтально, и третья (GBP) просто за краем экрана. С homeTotalOmitsAccount итог молча её не учитывает: €16 626,11 вместо €17 379,28. Проверка вида «итог — положительное число» этого не поймает.
Локаторы остаются на месте
Второе правило: дефект меняет поведение или значения, но не accessibility-идентификаторы. Все ID лежат в одном файле (Core/A11y.swift на iOS, core/A11y.kt на Android), и строки одинаковые на обеих платформах: card.cvv — это card.cvv везде.
Идея в том, чтобы набор тестов, написанный против clean, продолжал находить свои элементы, когда дефекты включены, и падал на проверках.
Для моих экспериментов это и было нужно. Можно натравить XCUITest, Espresso/Compose, Appium или Maestro на одни и те же экраны и сравнивать, как они справляются с изменённым поведением, как ждут и насколько читаемыми получаются тесты, без того чтобы сравнение свелось к погоне за переехавшими локаторами.
Один и тот же баг, один и тот же ID, две платформы:
Как включать баги
Дефекты задаются конфигурацией, так что отдельная сборка на каждый не нужна. Есть три способа.
1. Аргументы запуска. Удобно для CI и быстрых прогонов:
# iOSxcrun simctl launch booted VadimToptunov.ChaosBank \ -ChaosBankDefects pnlSign -ChaosBankStartUnlocked 1 -ChaosBankTab portfolio# Androidadb shell am start -n com.vadimtoptunov.chaosbank_android/.MainActivity \ -e CHAOSBANK_DEFECTS pnlSign -e CHAOSBANK_START_UNLOCKED 1 -e CHAOSBANK_TAB portfolio
Можно включить один дефект, целый профиль (ui, validation, security, network, flaky…) или набор по сложности (beginner, middle, senior). Приоритет такой: явный список дефектов → профиль → seed → профиль, зашитый в сборку → clean. StartUnlocked пропускает авторизацию, чтобы сразу попасть на нужный экран. Он меняет только точку входа, а не поведение приложения.
2. Меню разработчика в приложении. Долгий тап по бейджу сборки в правом верхнем углу. Там можно переключать профили, включать отдельные дефекты, имитировать офлайн, медленную или нестабильную сеть, включать RTL и листать упражнения.
3. Готовые варианты сборки. Конфигурации Xcode (на Android — product flavors), например ChaosBank Flaky, ставятся рядом с чистым приложением под своим именем. Это может пригодиться тому, кто хочет заняться ручным исследовательским тестированием, не разбираясь с аргументами запуска.
Упражнения
У каждого дефекта есть запись в exercises.json:
{ "id": "IOS-MON-02", "title": "Loss displayed as gain", "difficulty": "junior", "category": "money", "defects": ["pnlSign"], "launchArgument": "-ChaosBankDefects pnlSign", "expectedClean": "Loss shows negative / loss color.", "expectedBuggy": "Loss shows positive / gain color.", "task": "Open Portfolio and assert TSLA's P&L renders as a loss (negative, red).", "keyLocators": ["portfolio.holding.TSLA.pnl", "portfolio.pnl"]}
Тексты упражнений пока только на английском.
Сейчас там 12 упражнений уровня junior, 97 middle и 22 senior в десяти категориях: деньги, состояние, безопасность, UI, валидация, локализация, сеть, доступность, конкурентность и производительность. Уровни сложности расставлял я сам, и местами они наверняка не совсем точные.
Junior — это, например, «разрешён перевод на ноль» или «номер карты не замаскирован». Senior — в основном про тайминги: двойная отправка, поздний ответ, перетирающий свежее состояние, анимации, которые так и не заканчиваются.
Если вы начинаете в мобильном QA
Примерно в таком порядке я бы пробовал. Это просто один из вариантов.
1. Сначала без кода. Поставить сборку beginner и потыкать. Список дефектов лучше заранее не читать. Записать найденное, потом сравнить с exercises.json. Довольно честный способ увидеть, что вы замечаете, а что пропускаете.
2. Первый автотест. Взять упражнение уровня junior и написать для него обычный XCUITest. Например:
func test_portfolio_lossIsRenderedAsLoss() { let app = XCUIApplication() app.launchArguments += ["-ChaosBankStartUnlocked", "1", "-ChaosBankTab", "portfolio"] app.launch() let pnl = app.descendants(matching: .any)["portfolio.holding.TSLA.pnl"] XCTAssertTrue(pnl.waitForExistence(timeout: 5)) XCTAssertFalse(pnl.label.contains("+"), "Убыток не должен отображаться как прибыль: \(pnl.label)")}
Или что-то похожее на Compose для Android:
@Testfun card_doesNotShowCvv() { val intent = Intent(ApplicationProvider.getApplicationContext(), MainActivity::class.java) .putExtra("CHAOSBANK_START_UNLOCKED", "1") .putExtra("CHAOSBANK_TAB", "card") ActivityScenario.launch<MainActivity>(intent) composeRule.onNodeWithTag("card.cvv").assertDoesNotExist()}
Это черновые наброски, а не готовый набор. Запускаете каждый на clean (зелёный), потом с -ChaosBankDefects pnlSign или CHAOSBANK_DEFECTS=cardCvvVisible (красный). Если тест остался зелёным, в этом стоит разобраться.
3. Тайминги и флаки. В профиле flaky гонки привязаны к seed. Неплохое место, чтобы увидеть, почему sleep(2) обычно выходит боком. Ещё один вариант — профиль network плюс медленная сеть в меню разработчика.
4. Гибридные экраны. Веб-логин специально сидит внутри WebView. По умолчанию он «непрозрачный»: Appium видит только NATIVE_APP, и до полей приходится добираться через нативное дерево доступности. С -ChaosBankInspectableWeb 1 (на Android — -e CHAOSBANK_INSPECTABLE_WEB 1) появляется контекст WEBVIEW_…, в который можно переключиться через switch_to.context(...). Мелочь, но в первый раз в реальном приложении она обычно сбивает с толку.
Для iOS-приложения есть и написанный руками UI-сьют: ChaosBank ⨯ KassiOS. Это ещё один мой эксперимент — небольшой DSL в духе Kaspresso поверх XCUITest, KassiOS. Он зелёный на clean и краснеет на каждом внедрённом дефекте. Возможно, полезнее смотреть его после того, как напишете несколько своих тестов.
Если вы начинаете в мобильной разработке
Приложения — это ещё и небольшая кодовая база без сторонних зависимостей, так что их может быть интересно почитать. Части, на которые я потратил больше всего времени:
-
Деньги.
Decimalвезде, кроме одного намеренного места: дефектroundingDriftпропускает одно вычисление черезDouble, чтобы было видно, к чему это приводит. -
Детерминизм. Симулированный поток цен и «подбрасывание монетки» в гонках берутся из seeded RNG (SplitMix64), так что упавший прогон можно воспроизвести. Живые котировки с Yahoo Finance есть, но включаются отдельно, потому что делают тесты недетерминированными.
-
Мок-бэкенд на акторе, с таймаутами, ретраями, офлайн-режимом и ответами не по порядку.
-
Тестируемые view model. Покрытие логического слоя — около 97% на iOS и 95% на Android, проверяется в CI. UI оставлен UI-тестам.
-
Два UI-стека. Сборки на UIKit (
UITableView) и XML (RecyclerView) есть потому, что некоторые баги по-настоящему проявляются только там, например переиспользованная ячейка, которая показывает сумму из предыдущей строки.
Одно упражнение: выбрать дефект, найти его проверку Defects.isActive и прочитать корректную ветку else рядом со сломанной. Другое: попробовать добавить свой дефект. Шаги есть в руководстве разработчика. Учтите, что CI сравнивает каталоги упражнений двух репозиториев, так что новый дефект должен появиться на обеих платформах.
Тестовые задания
Этого я не планировал, но, похоже, подходит: тестовые задания при найме или онбординг в команду. Раз набор багов — это просто конфигурация, можно собрать вариант под то, что важно вашей команде:
-
Для роли с упором на безопасность — сборка
security: незамаскированные данные карты, принятый просроченный OTP, диплинк в обход авторизации, токен вUserDefaults. -
Для команды, которая борется с флаки-тестами —
flakyилиsenior, а задание — набор тестов, стабильный, скажем, десять прогонов подряд. -
Для джуниорской позиции —
beginnerи сначала письменный баг-репорт, а уже потом код. -
Для чего-то более узкого — свой список через
-ChaosBankDefects/CHAOSBANK_DEFECTS.
Сборки профилей ставятся отдельным приложением со своим именем (ChaosBank Security, ChaosBank Flaky…), так что кандидату можно отдать один APK или сборку для симулятора. Сборка clean даёт проверяющему точку отсчёта, на которой можно прогнать присланные тесты.
Одна оговорка: каталог публичный, и кандидат может подсмотреть список дефектов. Я бы, наверное, относился к этому как к экзамену с открытой книгой и смотрел на то, как написаны тесты (ожидания, проверки значений, объяснение найденного), а не на количество найденных багов.
Что я понял по дороге
-
«Локаторы остаются на месте» оказалось сложнее, чем я ожидал. Некоторые реалистичные баги, вроде переиспользованной ячейки со старым accessibility ID, как раз про локаторы. В итоге я сделал их отдельными, явно помеченными дефектами, а не исключениями из правила.
-
Случайность лучше делать опциональной. Живые котировки — это весело, но проверка цены или P&L мало что значит, когда число меняется каждую секунду. Поэтому по умолчанию работает симуляция с seed.
-
Синхронизация двух кодовых баз требует автоматизации. Вручную они расходятся. Теперь CI-скрипт сравнивает каталоги упражнений двух репозиториев.
-
Чистая сборка должна быть действительно чистой. Если в
cleanесть случайный баг, вся идея «зелёный на clean, красный на дефекте» разваливается. Во многом для этого и нужен эталонный набор тестов.
Ограничения
-
Для iOS нужен Mac с Xcode 26+ и симулятор iOS 17+. Для Android — JDK 21 и API 29+.
-
Эталонный UI-сьют есть для iOS, но пока нет для Android. Наборы на Espresso/Compose, Appium и Maestro — в роадмапе.
-
Интерфейс и документация на английском.
-
Это песочница. «Банк» живёт в памяти, номер карты тестовый.
-
Я один поддерживаю два приложения, так что какие-то дефекты наверняка получились менее реалистичными, чем другие.
Ссылки
-
iOS-приложение: https://github.com/VadimToptunov/ChaosBank-iOS
-
Android-приложение: https://github.com/VadimToptunov/ChaosBank-Android
-
Руководство для QA/SDET: docs/USER_GUIDE.md
-
Эталонный UI-сьют: https://github.com/VadimToptunov/ChaosBank-KassiOS-Tests
Дальше в планах — сьюты на Espresso/Compose, Appium и Maestro против тех же экранов, чтобы посмотреть, как подходы сравниваются на практике. Если попробуете приложения, буду рад услышать, какие дефекты было сложно поймать, какие кажутся нереалистичными и каких багов из вашей работы не хватает. Комментарии, issues и PR — всё приветствуется.
ссылка на оригинал статьи https://habr.com/ru/articles/1098668/