Автоматизация тестирования мобильных приложений: что автоматизировать, инструменты и устройства

Автоматизация тестирования мобильных приложений: что автоматизировать, инструменты и устройства Полезное

Автоматизация тестирования мобильных приложений — это проверка Android- и iOS-приложений программами-тестами, которые сами запускают сценарии, сравнивают результат с ожидаемым и формируют отчет. На практике в компании это три разные задачи: анализ (какие проверки автоматизировать и на каких устройствах), генерация тестов и конфигураций прогона и запуск в CI на эмуляторах, реальных устройствах или ферме.

Ниже — отбор кандидатов с расчетом, сравнение Espresso, UI Automator, XCUITest и Appium, эмулятор или устройство, генерация матрицы устройств и причины нестабильных тестов.

Мини-словарь

Термин Что это Где работает
UI-тест (end-to-end) Сценарий «как пользователь»: нажатия, ввод, переходы Эмулятор, симулятор, устройство
Эмулятор Виртуальное Android-устройство (Android Emulator) Любая ОС с Android SDK
Симулятор Среда iOS в Xcode, запускает сборку под архитектуру Mac, а не образ iPhone Только macOS

Эмулятор и симулятор — не синонимы: Android-эмулятор воспроизводит устройство целиком, а iOS-симулятор запускает приложение в среде macOS, поэтому часть iOS-проблем (камера, производительность, некоторые жесты) видна только на реальном iPhone. Push-уведомления с Xcode 14 приходят и в симулятор (macOS 13+, Mac на Apple silicon или с T2), но только через тестовый sandbox-сервер APNs.

Что автоматизировать в первую очередь

Автоматизация окупается там, где сценарий часто повторяется, стабилен и критичен для бизнеса. Плохие кандидаты — экраны, которые еще перерисовывают, и проверки, где нужен человеческий взгляд: удобство, восприятие анимаций. Кандидатов удобно сравнивать по окупаемости — сколько минут ручной работы в месяц сэкономит тест и сколько стоит его написать:

# Какие мобильные тест-кейсы автоматизировать первыми: грубая оценка окупаемости
CASES = [
    # название, прогонов в месяц, минут вручную, правки UI (0 - редко, 2 - постоянно), критичный, нужен глаз человека
    ("Вход по паролю", 20, 4, 0, True, False),
    ("Оплата картой в тестовом шлюзе", 20, 8, 1, True, False),
    ("Онбординг из пяти экранов", 4, 3, 2, False, False),
    ("Смена языка интерфейса", 4, 5, 0, False, False),
    ("Удобство жестов на планшете", 2, 15, 1, False, True),
]
AUTOMATION_COST_MIN = 240  # оценка: написать и отладить один UI-тест, минут


def decide(runs, manual_min, volatility, critical, needs_human):
    if needs_human:
        return "вручную (оценка человеком)", None
    saved = runs * manual_min                 # минут ручной работы в месяц
    if saved <= 0:
        return "не автоматизировать: не прогоняется", None
    payback = AUTOMATION_COST_MIN / saved     # месяцев до окупаемости
    if volatility == 2:
        return "позже: экран еще меняется", payback
    if critical or payback <= 3:
        return "автоматизировать", payback
    return "пока вручную", payback


for name, *attrs in CASES:
    verdict, payback = decide(*attrs)
    months = "-" if payback is None else f"{payback:.1f} мес."
    print(f"{name:32} окупаемость {months:>9}  -> {verdict}")

Результат в Python 3.14:

Вход по паролю                   окупаемость  3.0 мес.  -> автоматизировать
Оплата картой в тестовом шлюзе   окупаемость  1.5 мес.  -> автоматизировать
Онбординг из пяти экранов        окупаемость 20.0 мес.  -> позже: экран еще меняется
Смена языка интерфейса           окупаемость 12.0 мес.  -> пока вручную
Удобство жестов на планшете      окупаемость         -  -> вручную (оценка человеком)

Модель упрощенная: в ней нет стоимости поддержки теста и цены пропущенного бага. Для критичных сценариев второе обычно перевешивает, поэтому флаг critical отправляет их в автоматизацию независимо от окупаемости. Минуты берите из тест-менеджмента, а не на глаз.

Уровни тестов: что где запускается

Большинство проверок дешевле делать ниже UI. Типичная раскладка Android-проекта на Gradle:

Уровень Каталог в модуле Инструменты Скорость
Локальные unit-тесты app/src/test/ JUnit, Mockito или MockK Секунды, без устройства
Инструментальные app/src/androidTest/ AndroidX Test, Espresso Минуты, нужен эмулятор
UI end-to-end отдельный проект или androidTest Espresso, UI Automator, Appium Самые медленные

Бизнес-логику (расчет скидки, валидацию формы) выносите из Activity и проверяйте локальными тестами на JVM, а на UI оставляйте навигацию, разрешения и системные диалоги.

Инструменты: чем отличаются Espresso, UI Automator, XCUITest и Appium

Инструмент Платформа Язык тестов Как видит приложение Когда брать
Espresso Android Kotlin, Java Внутри процесса приложения, сам ждет простоя UI-потока Быстрые UI-тесты своего приложения
UI Automator Android Kotlin, Java Снаружи, через систему: видит другие приложения и системные окна Разрешения, уведомления, переходы между приложениями
XCUITest iOS Swift, Objective-C Отдельный процесс-раннер через дерево доступности Нативные UI-тесты iOS в Xcode
Appium Android, iOS Python, Java, JS и др. Снаружи по протоколу WebDriver через драйверы (UiAutomator2, XCUITest) Один стек тестов для двух платформ, команда QA без мобильной разработки
Detox React Native JavaScript «Серый ящик»: синхронизируется с состоянием приложения Проекты на React Native
Flutter integration_test Flutter Dart Внутри Flutter-приложения Проекты на Flutter

Espresso синхронизируется с главным потоком сам, поэтому тесты быстрее и стабильнее, но за пределы своего приложения не выходит. Appium работает поверх нативных фреймворков (UiAutomator2, XCUITest), поэтому медленнее, зато один тест-код на обе платформы.

Пример UI-теста на Espresso

@RunWith(AndroidJUnit4::class)
class LoginTest {
    @get:Rule
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)

    @Test
    fun validLoginOpensHome() {
        onView(withId(R.id.login_email)).perform(typeText("qa@example.com"), closeSoftKeyboard())
        onView(withId(R.id.login_password)).perform(typeText("test-password"), closeSoftKeyboard())
        onView(withId(R.id.login_submit)).perform(click())
        onView(withId(R.id.home_greeting)).check(matches(isDisplayed()))
    }
}

Тест лежит в app/src/androidTest/, запускается командой ./gradlew connectedAndroidTest на подключенном эмуляторе или устройстве. R.id.* — идентификаторы из разметки вашего приложения; явных ожиданий нет, потому что Espresso сам ждет, пока UI-поток освободится.

Тот же сценарий на Appium (Python)

# Appium 3 и Appium-Python-Client 6: вход в Android-приложение (нужны сервер Appium и эмулятор)
from appium import webdriver
from appium.options.android import UiAutomator2Options
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

options = UiAutomator2Options()
options.platform_version = "14"
options.device_name = "emulator-5554"
options.app = "/builds/app-debug.apk"   # пример пути к сборке

driver = webdriver.Remote("http://127.0.0.1:4723", options=options)
try:
    wait = WebDriverWait(driver, 15)
    wait.until(EC.element_to_be_clickable((AppiumBy.ACCESSIBILITY_ID, "login_email"))).send_keys("qa@example.com")
    driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_password").send_keys("test-password")
    driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_submit").click()
    greeting = wait.until(EC.visibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, "home_greeting")))
    assert "Привет" in greeting.text, greeting.text
finally:
    driver.quit()

Ожидания здесь явные (WebDriverWait): Appium не знает о внутреннем состоянии приложения. ACCESSIBILITY_ID устойчивее XPath, если разработчики проставили стабильные метки (Android — contentDescription, iOS — accessibilityIdentifier). Без запущенного сервера клиент падает на создании сессии:

urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='127.0.0.1', port=4723): Max retries exceeded with url: /session (Caused by NewConnectionError("HTTPConnection(host='127.0.0.1', port=4723): Failed to establish a new connection: [Errno 111] Connection refused"))

Исправление: запустить сервер командой appium и один раз поставить драйвер appium driver install uiautomator2 — начиная с Appium 2 драйверы ставятся отдельно от сервера. Актуальная ветка на сентябрь 2026 — Appium 3 (нужен Node.js 20.19+), при обновлении с 2.x драйверы обновляют вместе с сервером.

Эмуляторы, реальные устройства и фермы

Критерий Эмулятор / симулятор Реальное устройство Облачная ферма
Стоимость Бесплатно, нужен ресурс CI Покупка и обслуживание парка Оплата минут или устройств
Скорость старта Быстро, легко сбросить состояние Требует сброса данных между тестами Очередь на популярные модели
Точность Нет реальной камеры, сенсоров, прошивок производителей Реальные железо, сеть, оболочка вендора Реальные устройства, но удаленно
Для чего Каждый коммит, регресс UI-логики Перед релизом: камера, производительность, батарея, push в боевом окружении Широкое покрытие моделей без своего парка

Рабочая схема: на каждый merge request — эмуляторы и симуляторы (iOS — только на macOS-агентах), перед релизом — короткий набор на реальных устройствах. Для реального iPhone сборку подписывают сертификатом разработчика и профилем с этим устройством. Фермы бывают облачные (Firebase Test Lab, AWS Device Farm) и свои на базе Appium и Selenium Grid.

Генерация матрицы устройств попарным методом

Все сочетания версий ОС, экранов, языков и сетей — это 3 x 3 x 2 x 2 = 36 конфигураций. Попарный (pairwise) метод берет меньше строк так, чтобы каждая пара значений встретилась хотя бы раз: по исследованиям NIST, большинство конфигурационных дефектов вызывается одним параметром или сочетанием двух.

# Матрица устройств: каждая пара значений параметров встречается хотя бы в одном прогоне
from itertools import combinations, product

PARAMS = {
    "ОС": ["Android 13", "Android 15", "Android 17"],
    "экран": ["компактный", "обычный", "планшет"],
    "язык": ["ru", "en"],
    "сеть": ["Wi-Fi", "LTE"],
}
names = list(PARAMS)


def pairs_of(row):
    return {((names[i], row[i]), (names[j], row[j]))
            for i, j in combinations(range(len(row)), 2)}


all_rows = list(product(*PARAMS.values()))
uncovered = set().union(*(pairs_of(r) for r in all_rows))
total_pairs = len(uncovered)
plan = []
while uncovered:
    best = max(all_rows, key=lambda r: len(pairs_of(r) & uncovered))
    plan.append(best)
    uncovered -= pairs_of(best)

# проверка критерия: все пары покрыты
covered = set().union(*(pairs_of(r) for r in plan))
assert len(covered) == total_pairs, "не все пары покрыты"

print(f"Полный перебор: {len(all_rows)} конфигураций, пар значений: {total_pairs}")
print(f"Попарный план: {len(plan)} конфигураций")
for n, row in enumerate(plan, 1):
    print(n, " | ".join(row))

Вывод в Python 3.14:

Полный перебор: 36 конфигураций, пар значений: 37
Попарный план: 9 конфигураций
1 Android 13 | компактный | ru | Wi-Fi
2 Android 13 | обычный | en | LTE
3 Android 15 | планшет | ru | LTE
4 Android 17 | планшет | en | Wi-Fi
5 Android 15 | компактный | en | Wi-Fi
6 Android 17 | компактный | ru | LTE
7 Android 15 | обычный | ru | Wi-Fi
8 Android 13 | планшет | ru | Wi-Fi
9 Android 17 | обычный | ru | Wi-Fi

Жадный алгоритм не гарантирует минимальный план (для этих параметров теоретический минимум — 9 строк, здесь совпало), а попарное покрытие не ловит ошибки, которым нужно сочетание трех параметров. Критичные связки (например, «Android 17 + планшет + en» для ключевого рынка) добавляйте вручную.

Почему мобильные автотесты «моргают» и что делать

Нестабильный (flaky) тест то проходит, то падает без изменений кода. Типовые симптомы и причины:

Симптом Частая причина Что сделать
Элемент «не найден», хотя он есть на экране Тест не дождался загрузки В Espresso — IdlingResource для фоновой работы, в Appium — явные ожидания вместо sleep
Падает только в CI Анимации, медленный эмулятор Отключить системные анимации на тестовом устройстве, дать эмулятору больше ресурсов
Тест зависит от порядка запуска Общее состояние: логин, база, кэш Сбрасывать данные между тестами (Android Test Orchestrator с clearPackageData)
Ломается после каждой правки верстки Локаторы по тексту или XPath Стабильные id и accessibility-метки, паттерн Page Object (Screen Object)
Разные результаты на разных устройствах Вендорские оболочки, разрешения Разнести проверки по матрице, системные диалоги закрывать через UI Automator

Запись действий (record and playback) быстро дает первый тест, но хрупкий. Для долгоживущего набора нужен Page Object: класс на экран с методами «ввести логин», «нажать войти», и локаторы меняются в одном месте.

Дорожная карта: первый автотест в CI

  1. Отберите 5-10 критичных стабильных сценариев по таблице окупаемости.
  2. Договоритесь с разработчиками о стабильных id и accessibility-метках на этих экранах.
  3. Напишите тесты: Espresso или XCUITest, если тесты пишут мобильные разработчики; Appium, если нужен общий стек QA на две платформы.
  4. Запустите на эмуляторе в CI на каждый merge request, соберите отчет JUnit XML.
  5. Сгенерируйте матрицу устройств и прогоняйте ее перед релизом на ферме или своем парке.

Результат этапа — зеленый пайплайн с отчетом, а не «покрытие в процентах». Пока доля ложных падений заметна, чините причины по таблице выше, а не наращивайте число тестов.

Выводы

  • Автоматизация мобильного тестирования в компании — это анализ (что и где проверять), генерация тестов и конфигураций и запуск в CI, а не только выбор фреймворка.
  • Первыми автоматизируют частые, стабильные и критичные сценарии; удобство и визуальную оценку оставляют людям.
  • Espresso и XCUITest быстрее и стабильнее внутри своей платформы, Appium дает один стек на Android и iOS ценой скорости и явных ожиданий.
  • Эмуляторы и симуляторы — для каждого коммита, реальные устройства и фермы — для камеры, производительности и перед релизом.
  • Попарная матрица сокращает число конфигураций (36 -> 9 в примере), но не заменяет ручного добавления критичных сочетаний.

Где применяется / связь с практикой

Освойте тему на практике

Мобильные автотесты пишут QA-инженеры по автоматизации и мобильные разработчики, в небольших командах — тестировщик, начинавший с ручных проверок (подробнее — кто такой тестировщик). Системно освоить тест-дизайн, баг-репорты и основы автоматизации можно на курсе «Инженер по тестированию». Попробовать формат и задать вопросы практикам можно на открытых уроках Otus.

FAQ

Можно ли тестировать iOS-приложение без Mac?
Симулятор iOS и сборка XCUITest-тестов требуют macOS с Xcode. Без своего Mac используют облачные macOS-агенты CI или фермы реальных iPhone.

Нужно ли уметь программировать, чтобы автоматизировать мобильные тесты?
Для записи сценариев без кода — нет, но поддерживать такой набор тяжело. Для Espresso, XCUITest и Appium нужен базовый уровень Kotlin, Swift, Python или Java.

Сколько UI-тестов должно быть в проекте?
Универсального числа нет. Ориентир — UI-тестов заметно меньше, чем unit-тестов, и каждый закрывает сценарий, который нельзя проверить ниже UI.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)