XCUI, Tests & Robots. Разбираем нативную автоматизацию iOS на винтики. Часть 1

от автора

Привет, Хабр! На связи снова Максим из ATI.SU.

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

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

Этой статьёй мы открываем цикл о UI‑тестировании iOS приложений. Начнём с основ: разберёмся, какое место UI‑тесты занимают в пирамиде тестирования, познакомимся с инструментами и постепенно перейдём к построению устойчивых тестов с применением проверенных паттернов.

Отвёртки на изготовку — будем разбирать нативную автоматизацию на винтики!

Пирамида тестирования: почему UI-тестов не должно быть слишком много

Прежде чем писать первые UI-тесты, важно понять, что это такое и какое место они занимают в стратегии тестирования приложения. В мире разработки издавна есть концепция пирамиды тестирования. Она показывает, как распределять разные типы тестов на проекте, чтобы получать быструю и надёжную обратную связь о качестве приложения.

Концепцию пирамиды тестирования, её назначение и виды подробно разбирали множество раз. Чтобы не повторяться, оставлю ссылки на годные статьи по этой теме:

Если совсем коротко, то суть пирамиды сводится к следующему:

  • чем ближе к основанию — тем тесты быстрее, дешевле и стабильнее;

  • чем ближе к верхушке — тем тесты медленнее, дороже и сложнее в поддержке.

Есть много разных интерпретаций пирамиды тестирования, которые отличаются:

  • по вложенности — от 3 уровней и больше;

  • по видам тестирования. Где‑то E2E и UI‑тесты ставят в один ряд, где‑то — это разные виды тестирования;

  • по форме — стандартная пирамида или перевёрнутая (в виде рожка мороженого).

При этом верхние уровни ближе всего к реальному пользовательскому сценарию. Рассмотрим стандартную пирамиду, которая разделена на три основных уровня.

Unit‑тесты (основание пирамиды)

Unit‑тесты точечно проверяют отдельные части логики приложения в изоляции: функции, классы, методы.

Например:

  • корректно ли рассчитывается стоимость заказа;

  • правильно ли работает сортировка;

  • что происходит при пустом ответе от сервера.

Пример unit‑теста:

func testSum() {    // Проверяем, что функция sum корректно складывает два числа    XCTAssertEqual(sum(2, 3), 5)}

Специфика unit‑тестов:

  • выполняются очень быстро;

  • не требуют запуска приложения;

  • легко поддерживаются;

  • помогают быстро находить ошибки в бизнес‑логике.

Именно unit‑тестов в проекте обычно больше всего, и пишут их, как правило, сами разработчики в процессе написания кода.

Интеграционные тесты

Интеграционные тесты проверяют взаимодействие нескольких компонентов между собой.

Например:

  • корректно ли экран получает данные из сети;

  • сохраняется ли объект в базу данных;

  • правильно ли отрабатывает цепочка: запрос к серверу → обработка ответа → отображение на экране.

Здесь уже проверяется не один конкретный элемент (свойство, класс, метод и др.), а работа нескольких слоёв приложения вместе.

Пример интеграционного теста:

func testSaveUser() {    // UserRepository хранит данные через Database — два компонента в связке    let repository = UserRepository(database: Database())    // Сохраняем пользователя    repository.save(User(name: "Maxim"))    // Читаем его обратно    let loaded = repository.find(name: "Maxim")    // Проверяем, что связка «репозиторий + база» отработала верно    XCTAssertEqual(loaded.name, "Maxim")}

Такие тесты медленнее unit‑тестов, потому что затрагивают больше частей системы: сеть, базу данных, файловую систему и асинхронные операции.

И unit‑, и интеграционные тесты выше написаны на XCTest — базовом фреймворке Apple. На WWDC24 Apple представила более современный фреймворк — Swift Testing (вышел в Xcode 16). Он не заменяет XCTest, а дополняет его: оба спокойно живут в одном тестовом таргете. Его главное отличие — более простой и читаемый синтаксис, что видно по примеру:

import Testing@Test func sum() {    #expect(sum(2, 3) == 5)}

Важная оговорка: Swift Testing умеет только в unit‑ и интеграционные тесты. UI‑тесты и performance‑тесты остаются за XCTest / XCUITest.

UI-тесты (вершина пирамиды)

UI-тесты работают через интерфейс приложения. Они буквально воспроизводят действия реального пользователя при работе с приложением:

  • запускают приложение;

  • нажимают кнопки;

  • вводят текст;

  • проверяют отображение элементов на экране.

Пример UI-теста:

func testLogin() {    let app = XCUIApplication()    app.launch()                                   // Запускаем приложение    app.textFields["Login"].tap()                  // Ставим курсор в поле логина    app.textFields["Login"].typeText("admin")      // Вводим логин    app.secureTextFields["Password"].tap()         // Ставим курсор в поле пароля    app.secureTextFields["Password"].typeText("1234") // Вводим пароль    app.buttons["Sign In"].tap()                   // Нажимаем кнопку входа    // Проверяем, что после входа появился экран приветствия    XCTAssertTrue(app.staticTexts["Welcome"].exists)}

Такие тесты максимально приближены к реальному использованию приложения, но за это приходится платить, поскольку UI‑тесты:

  • работают значительно медленнее, чем unit‑ и интеграционные тесты;

  • чаще ломаются;

  • сильно зависят от состояния интерфейса;

  • требуют больше времени на поддержку.

Именно по этой причине UI‑тестов обычно меньше всего на проекте. У новичков часто возникает идея:

«А давайте просто автоматизируем все пользовательские сценарии через UI — ведь это максимально близко к реальности».

Заманчивая мысль, но на практике такой подход быстро превращается в проблему.

Представьте:

  • приложение запускается 15–20 секунд;

  • один UI‑тест может идти от нескольких секунд до нескольких минут (в зависимости от реализации);

  • в проекте уже более 100 тестов.

В итоге полный прогон может занимать часы. Кроме того, UI‑тесты нестабильны по своей природе:

  • анимация не успела завершиться;

  • элемент ещё не появился;

  • всплыл системный алерт;

  • сеть ответила медленнее обычного.

В результате тест может упасть не из‑за бага, а из‑за окружения. Такие тесты называют flaky‑тестами. Поэтому UI‑тесты обычно используют только для проверки:

  • критически важных пользовательских сценариев;

  • основных бизнес‑флоу;

  • интеграции всех частей приложения вместе.

Например:

  • авторизация в приложении;

  • оформление заказа;

  • регистрация нового пользователя;

  • оплата;

  • создание сущности;

  • ключевые пользовательские переходы.

Упрощённо распределение тестов часто выглядит примерно так:

  • 70–80% — unit‑тесты;

  • 15–20% — интеграционные;

  • 5–10% — UI‑тесты.

Это не строгие правила, а скорее ориентир. Главная идея пирамиды — не в точных процентах, а в поддержке баланса:

  • дешёвые проверки должны находить большинство проблем;

  • дорогие UI-тесты должны покрывать только самое важное.

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

Основные инструменты для автотестов на iOS

Xcode

Первый и основной инструмент, с которым мы будем взаимодействовать при написании UI-автотестов, — это, конечно же, Xcode. Это IDE от Apple, которая служит основной платформой для создания, отладки и тестирования iOS-приложений. В контексте автоматизации Xcode используется для непосредственного написания кода тестов, их запуска и анализа результатов. Основным языком для написания тестов является Swift, разработанный Apple в 2014 году. Реже встречается Objective-C, который использовался ранее и до сих пор может лежать в основе старых проектов с legacy-кодом. Xcode распространяется бесплатно, и его можно скачать из App Store.

Интерфейс Xcode

Интерфейс Xcode

Accessibility Inspector

При написании практически любого UI-теста нам потребуется находить и идентифицировать элементы на экране, чтобы потом с ними взаимодействовать. Способов найти идентификатор элемента несколько, и о них мы поговорим в следующих статьях. Но один из самых удобных — воспользоваться Accessibility Inspector. Это стандартная утилита в macOS, которая предназначена для проверки свойств доступности (accessibility) UI-элементов в любом запущенном приложении. UI-тесты находят элементы на экране, используя их accessibility-идентификаторы. Accessibility Inspector позволяет увидеть иерархию элементов и их свойства (например, identifier, label, value), чтобы использовать их в коде теста для взаимодействия с интерфейсом. Это основной инструмент для поиска локаторов. Accessibility Inspector входит в состав Xcode Developer Tools (т.е. достаточно установить Xcode).

Identifier кнопки "Найти грузы" в Accessibility Inspector

Identifier кнопки «Найти грузы» в Accessibility Inspector

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

Окружения выполнения тестов: симулятор и физическое устройство

Автотесты на iOS можно запускать на разных окружениях:

  • на физическом устройстве

  • на симуляторе.

Выбор окружения зависит от целей тестирования, типа приложения и того, какие именно сценарии необходимо проверить.

Физическое устройство

Физическое устройство — это настоящий iPhone или iPad, на котором приложение запускается так же, как у конечного пользователя. Устройство подключается к Mac по кабелю или по Wi‑Fi, после чего Xcode устанавливает приложение и запускает тесты напрямую на устройстве.

Такой способ тестирования даёт наиболее достоверный результат, поскольку приложение работает:

  • на реальном процессоре;

  • с настоящими ограничениями памяти;

  • с реальными датчиками и сетями;

  • в условиях настоящего энергопотребления и нагрева.

Когда необходимо использовать физическое устройство

Физические устройства обязательны для проверки функциональности, связанной с аппаратной частью устройств:

  • push‑уведомления;

  • работа камеры и микрофона;

  • геолокация и GPS;

  • Bluetooth, NFC, Wi‑Fi и сотовая сеть;

  • Touch ID и Face ID;

  • работа с внешними устройствами;

  • встроенные покупки (in‑app purchases);

  • производительность приложения;

  • энергопотребление и троттлинг.

Также только на реальном устройстве можно достоверно проверить:

  • плавность анимаций;

  • скорость запуска приложения;

  • утечки памяти;

  • поведение приложения при нехватке ресурсов;

  • проблемы, возникающие только на конкретных моделях устройств.

Преимущества физических устройств

  • Максимально приближенные условия к пользовательским.

  • Высокая достоверность результатов.

  • Возможность тестирования аппаратных возможностей.

  • Проверка реальной производительности и стабильности.

Недостатки

  • Требуются реальные устройства разных моделей.

  • Дороже в поддержке и масштабировании.

  • Медленнее запуск тестов.

  • Сложнее организовать параллельный прогон большого количества тестов.

Симулятор

Симулятор — это встроенный инструмент Xcode, который запускает виртуальное iOS‑устройство прямо на macOS.

Важно понимать отличие симулятора от эмулятора:

  • эмулятор воспроизводит аппаратное обеспечение устройства (процессор, память, сетевые модули, графический ускоритель и некоторые датчики). По сути, он создаёт на компьютере виртуальную среду, где разработчики и тестировщики могут запускать приложение так, будто оно установлено на реальном устройстве;

  • симулятор воспроизводит только программное окружение iOS. Иными словами, симулятор не пытается воссоздать реальное аппаратное обеспечение — например, процессор или память. Он лишь воспроизводит поведение операционной системы. Приложение выполняется напрямую на процессоре Mac, а симулятор лишь предоставляет интерфейс и окружение iOS. Благодаря этому симуляторы работают очень быстро и идеально подходят для ежедневного запуска автотестов.

Что удобно тестировать на симуляторе

Симулятор особенно эффективен для:

  • проверки UI и вёрстки;

  • тестирования навигации;

  • проверки бизнес‑логики;

  • запуска smoke‑ и regression‑тестов;

  • проверки адаптивности интерфейса;

  • проверки поведения приложения при смене ориентации экрана;

  • имитации GPS‑координат;

  • базовой проверки системных прерываний;

  • запуска большого количества UI‑тестов в CI/CD.

Всё это доступно без единого реального устройства: симулятор позволяет за пару кликов переключаться между разными моделями, размерами экранов и версиями iOS.

Ограничения симулятора

Несмотря на удобство, симулятор имеет ряд важных ограничений.

  • Отсутствие полноценного доступа к аппаратной части. Симулятор не способен корректно воспроизводить работу:

    • камеры;

    • Bluetooth;

    • NFC;

    • сотовой сети;

    • барометра;

    • некоторых сенсоров;

    • реальной биометрии.

Некоторые из этих функций можно имитировать, но это не заменяет полноценное тестирование на настоящем устройстве.

  • Недостоверная производительность. Приложение в симуляторе использует ресурсы Mac, который значительно мощнее большинства iPhone. Из‑за этого:

    • анимации могут выглядеть плавнее;

    • загрузка экранов — быстрее;

    • проблемы с производительностью могут быть скрыты.

Тест, успешно прошедший на симуляторе, не гарантирует отсутствие лагов на реальном устройстве.

  • Специфические баги. Некоторые ошибки проявляются только на симуляторе либо только на реальном устройстве. Особенно это касается:

    • работы памяти;

    • графического рендеринга;

    • многопоточности;

    • системных API.

Что обычно выбирают на практике

На практике оба окружения используют совместно, разделяя их по этапам работы. Симуляторы закрывают повседневную нагрузку: быстрые локальные прогоны во время разработки и автоматические запуски в CI/CD, где важнее всего скорость и где гоняется основная масса тестов. Физические устройства подключают на финальных и особых проверках — перед релизом, для end‑to‑end сценариев и всего, что упирается в реальное железо: производительности, push‑уведомлений, сетевых сценариев и багов конкретных моделей.

Соберём все различия в одну таблицу:

Критерий

Симулятор

Физическое устройство

Скорость запуска

Высокая

Низкая

Стоимость поддержки

Низкая

Высокая

Масштабирование тестов

Простое

Сложное

UI и вёрстка

Отлично подходит

Подходит

Бизнес-логика

Отлично подходит

Подходит

Тестирование разных версий iOS

Отлично подходит

Ограничено набором устройств

GPS и геолокация

Имитация

Реальные условия

Камера / NFC / Bluetooth

Ограниченно

Полная поддержка

Face ID / Touch ID

Частичная имитация

Реальная работа

Производительность

Недостоверна

Достоверна

Энергопотребление

Нельзя проверить

Можно проверить

Троттлинг и нагрев

Нельзя проверить

Можно проверить

Работа с внешними устройствами

Ограниченно

Полная поддержка

Надёжность результата

Средняя

Высокая

Подходит для CI/CD

Отлично подходит

Ограниченно

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

Итак, мы знаем, в какой среде запускать тесты. Но главного инструмента мы ещё не коснулись — того, чем эти тесты, собственно, пишутся. За это отвечает тестовый фреймворк: именно он превращает наш Swift‑код в реальные действия с интерфейсом.

Фреймворки для UI‑тестирования

Фреймворк для автоматизации тестирования — это набор инструментов и библиотек, предназначенных для разработки и выполнения тестов. Он предоставляет готовую структуру и механизмы для написания, запуска и проверки тестов, позволяя разработчику или тестировщику сосредоточиться на логике тестирования, не реализуя базовые функции с нуля.

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

Простая аналогия для понимания сути. Представь, что твоя задача — собрать модель машины из LEGO (написать тесты).

Без фреймворка. У тебя на старте есть только идея, как машина должна выглядеть в итоге. И прежде чем приступить к сборке, всё приходится продумывать самому: какой формы и размера нужны детали, как они крепятся друг к другу, какие сочетаются между собой, и в каком порядке всё это собирать. По сути, ты сам себе и придумываешь детали, и пишешь инструкцию. Только после этой подготовки можно приступить к самой модели. Это долго, и легко ошибиться.

С фреймворком. Ты открываешь готовый набор LEGO. В коробке уже есть:

  • стандартные детали, которые точно совместимы друг с другом (инструменты и библиотеки);

  • инструкция сборки (структура проекта);

  • понятные шаги, как собирать модель (правила и подход).

Тебе не нужно изобретать, как детали соединяются, — ты просто следуешь инструкции и собираешь нужную модель.

Тестовый фреймворк также иногда называют тестовым движком.

XCUITest

XCUITest — нативный тестовый движок от Apple, глубоко интегрированный в Xcode. Это расширение фреймворка XCTest, специально разработанное для UI‑тестирования iOS‑ и macOS‑приложений.

Сильные стороны. Главный козырь XCUITest — нативность, и из неё вырастает почти всё остальное. Тесты выполняются быстро за счёт прямой интеграции с Xcode и экосистемой Apple, из коробки понимают все элементы iOS‑интерфейса и сами дожидаются готовности элемента, прежде чем с ним взаимодействовать, — а это заметно снижает нестабильность. В дополнение есть встроенный UI‑рекордер, который генерирует код теста прямо по вашим действиям на экране и помогает быстро стартовать. В сумме получаются надёжные и предсказуемые тесты.

Слабое место. Обратная сторона нативности — единственный, но существенный минус: XCUITest живёт только в экосистеме Apple. Для Android (а значит, и для кроссплатформенного проекта) понадобится другой инструмент.

Принцип работы. Тесты выполняются как отдельный процесс, который отправляет команды приложению через API операционной системы. Это обеспечивает надёжное и быстрое взаимодействие.

Пример простого теста на XCUITest:

import XCTestfinal class MyAppUITests: XCTestCase {  func testLogin() {    let app = XCUIApplication()    app.launch()                                   // Запускаем приложение    app.textFields["Login"].tap()                  // Ставим курсор в поле логина    app.textFields["Login"].typeText("admin")      // Вводим логин    app.secureTextFields["Password"].tap()         // Ставим курсор в поле пароля    app.secureTextFields["Password"].typeText("1234") // Вводим пароль    app.buttons["Sign In"].tap()                   // Нажимаем кнопку входа    // Проверяем, что после входа появился экран приветствия    XCTAssertTrue(app.staticTexts["Welcome"].exists)  }}

Не стоит путать XCTest и XCUITest — это уровни одного тестового фреймворка XCTest, но они решают разные задачи.

XCTest — базовый фреймворк для тестирования кода. С его помощью проверяют логику приложения: функции, классы, вычисления, работу API. Эти тесты не взаимодействуют с интерфейсом — они работают с кодом напрямую.

XCUITest — надстройка над XCTest, которая нужна для UI‑тестов. Она позволяет запускать приложение и взаимодействовать с его интерфейсом: нажимать кнопки, вводить текст, переходить по экранам.

Проще говоря: XCTest проверяет код приложения, а XCUITest — то, что видит и делает пользователь на экране.

Appium

Appium — кроссплатформенный open‑source тестовый движок на основе протокола WebDriver. Взаимодействие с устройством происходит через HTTP‑запросы к установленному серверному приложению, что делает его универсальным решением для различных платформ.

Сильные стороны. Главное достоинство Appium — универсальность. Один и тот же API позволяет автоматизировать и iOS, и Android, а сами тесты можно писать почти на любом популярном языке: Java, Python, JavaScript, Ruby, Swift и других. При этом Appium не привязан ни к конкретной IDE, ни к операционной системе разработчика и оставляет свободу в выборе вспомогательных инструментов и фреймворков.

Слабое место. За универсальность приходится платить скоростью и стабильностью. Между тестом и приложением встаёт дополнительный слой — Appium‑сервер, — из‑за которого прогон идёт медленнее и оказывается чуть менее предсказуемым, чем у нативного XCUITest.

Принцип работы. Appium использует протокол WebDriver. Тестовый скрипт отправляет HTTP‑запросы на Appium‑сервер, который транслирует их в команды для XCUITest (на iOS) или UIAutomator2/Espresso (на Android). На iOS под капотом Appium всё равно использует XCUITest — поэтому быстрее чистого XCUITest он не может быть по определению.

Пример простого теста на Appium:

import io.appium.java_client.ios.IOSDriver;import io.appium.java_client.ios.options.XCUITestOptions;import io.appium.java_client.AppiumBy;import org.openqa.selenium.WebElement;import org.testng.Assert;import org.testng.annotations.Test;import java.net.URL;public class MyIosTest {  @Test  public void testLogin() throws Exception {    // Описываем, на каком устройстве и каком приложении запускаемся    XCUITestOptions options = new XCUITestOptions()        .setDeviceName("iPhone 15")        .setPlatformVersion("17.0")        .setApp("/path/to/MyApp.app")        .setAutomationName("XCUITest");    // Подключаемся к запущенному Appium-серверу    IOSDriver driver =        new IOSDriver(new URL("http://127.0.0.1:4723"), options);    // Находим поле логина и вводим логин    WebElement loginField = driver.findElement(AppiumBy.accessibilityId("Login"));    loginField.click();    loginField.sendKeys("admin");    // Находим поле пароля и вводим пароль    WebElement passwordField = driver.findElement(AppiumBy.accessibilityId("Password"));    passwordField.click();    passwordField.sendKeys("1234");    // Нажимаем кнопку входа    driver.findElement(AppiumBy.accessibilityId("Sign In")).click();    // Проверяем, что после входа появился экран приветствия    WebElement welcomeLabel = driver.findElement(AppiumBy.accessibilityId("Welcome"));    Assert.assertTrue(welcomeLabel.isDisplayed());    driver.quit();  }}

Обратите внимание: тот же сценарий авторизации на Appium занимает заметно больше кода, чем на XCUITest, — это плата за кроссплатформенность и слой Appium‑сервера.

EarlGrey 2

EarlGrey 2 — нативный фреймворк UI‑тестирования от Google, построенный поверх XCUITest.

Сильные стороны. Главный козырь EarlGrey 2 — продвинутая автоматическая синхронизация. UI‑тесты часто падают не из‑за багов, а из‑за спешки: тест пытается нажать на кнопку раньше, чем она появилась, или проверить данные, которые ещё не подгрузились. EarlGrey 2 сам отслеживает анимации, сетевые запросы и фоновые очереди и дожидается, пока приложение перейдёт в нужное состояние, прежде чем сделать следующий шаг. На практике это заметно снижает нестабильность — одну из главных болей UI‑тестов. Вдобавок, в отличие от чистого XCUITest, EarlGrey 2 умеет работать в режиме white‑box: заглядывать внутрь приложения и читать его состояние прямо из кода, а не только видеть экран. Фреймворк нативно дружит с Xcode и запускается через xcodebuild, а тем, кто работал с Android и Espresso, его модель покажется знакомой.

Слабое место. За продвинутую синхронизацию приходится платить порогом входа: настройка сложнее, чем у чистого XCUITest. Как и XCUITest, EarlGrey 2 живёт только в экосистеме Apple — Android ему недоступен. А сообщество и динамика развития заметно скромнее, чем у XCUITest и Appium.

Принцип работы. EarlGrey 2 надстроен над XCUITest, поэтому сами действия выполняются нативными средствами Apple. Поверх этого работает движок синхронизации: перед каждым шагом он дожидается, пока завершатся анимации и затихнут сетевые запросы и фоновые очереди. White‑box‑возможности реализованы через специальный механизм eDO: тест из отдельного процесса дотягивается до объектов внутри приложения и читает его внутреннее состояние.

Пример простого теста на EarlGrey 2:

import XCTestimport EarlGreyfinal class MyAppUITests: XCTestCase {  func testLogin() {    let app = XCUIApplication()    app.launch()    // Находим поле логина и вводим логин    EarlGrey.selectElement(with: grey_accessibilityID("Login"))      .perform(grey_tap())    EarlGrey.selectElement(with: grey_accessibilityID("Login"))      .perform(grey_typeText("admin"))    // Находим поле пароля и вводим пароль    EarlGrey.selectElement(with: grey_accessibilityID("Password"))      .perform(grey_tap())    EarlGrey.selectElement(with: grey_accessibilityID("Password"))      .perform(grey_typeText("1234"))    // Нажимаем кнопку входа    EarlGrey.selectElement(with: grey_accessibilityID("Sign In"))      .perform(grey_tap())    // Проверяем, что после входа появился экран приветствия    EarlGrey.selectElement(with: grey_accessibilityID("Welcome"))      .assert(grey_sufficientlyVisible())  }}

Maestro

Maestro — кроссплатформенный фреймворк (iOS, Android, web), который за последние пару лет быстро набрал популярность.

Сильные стороны. Тесты пишутся декларативно на YAML — это простой текстовый формат, где ты описываешь что должно произойти («нажми кнопку», «введи текст»), а не программируешь шаги кодом. Из‑за этого порог входа ниже, чем у других фреймворков. Maestro анализирует то, что отрисовано на экране, и не зависит от технологии приложения — одинаково работает с UIKit, SwiftUI, Flutter и React Native. Он умеет взаимодействовать с системными диалогами (геолокация, камера), биометрией и диплинками. А встроенная устойчивость к задержкам и автоожидание элементов снижают нестабильность тестов. Всё это делает Maestro удобным для быстрых e2e‑ и smoke‑сценариев, кроссплатформенных проектов и команд без глубокой экспертизы в Swift.

Слабое место. Как и Appium, Maestro — это black-box: он видит приложение только снаружи, как обычный пользователь. На iOS под капотом всё тот же XCUITest, поэтому быстрее его Maestro быть не может. А декларативный YAML оказывается менее гибким, чем полноценный код, когда логика теста становится сложной.

Принцип работы. Maestro не заглядывает внутрь приложения — он работает только с тем, что видно на экране, и потому одинаково дружит с любым UI-стеком. На iOS команды в итоге транслируются в XCUITest — то есть в основе лежит тот же нативный движок Apple.

Пример простого теста на Maestro:

appId: com.example.app---- launchApp                  # Запускаем приложение- tapOn: "Login"             # Ставим курсор в поле логина- inputText: "admin"         # Вводим логин- tapOn: "Password"          # Ставим курсор в поле пароля- inputText: "1234"          # Вводим пароль- tapOn: "Sign In"           # Нажимаем кнопку входа- assertVisible: "Welcome"   # Проверяем, что появился экран приветствия

Обратите внимание: тот же сценарий входа на Maestro умещается в несколько строк YAML — никаких классов, импортов и прочей служебной обвязки, но и контроля меньше, чем в полноценном коде.

Если будете искать материалы по теме, наверняка наткнётесь на ещё два названия — KIF и Calabash. Сегодня это уже скорее история: KIF (Objective-C, работает внутри процесса приложения) почти не развивается, а BDD-фреймворк Calabash, в своё время популяризировавший человекочитаемые сценарии, давно неактуален. Знать о них полезно ровно для того, чтобы не принять старый туториал за актуальный.

Ограничения UI-тестирования

И XCUITest, и Appium работают по принципу «чёрного ящика» (black‑box): приложение они видят только снаружи, через интерфейс. Всё, что им доступно, — найти видимый элемент, тапнуть по нему, ввести текст и проверить, что на экране отображается нужное. Проще говоря, тест видит ровно то же, что и пользователь, и ничего не знает о внутреннем устройстве приложения.

Этому противопоставляют «белый ящик» (white‑box): здесь у теста есть доступ к коду приложения — он может напрямую вызывать методы, читать состояние, подменять зависимости.

Вся разница между этими принципами упирается в одну деталь: в каком процессе выполняется тест. Тестирование по принципу «белого ящика» возможно только тогда, когда тест работает в одном процессе с кодом приложения и потому может дёргать его методы напрямую. На Android так устроены Espresso и Kaspresso — они выполняются внутри того же процесса, что и приложение. XCUITest же запускается как отдельный процесс и физически не имеет доступа к коду — только к accessibility‑дереву на экране и поэтому относится к «black‑box». Частичное исключение — EarlGrey 2: он надстроен над XCUITest и через механизм eDO всё же даёт ограниченный white‑box доступ.

Вывод простой: проверки, которым нужно заглянуть внутрь приложения, имеет смысл спускать ниже по пирамиде — на unit‑ и интеграционный уровни. А UI‑тестам оставлять только то, что действительно проверяется через интерфейс.

Итак, мы рассмотрели разные фреймворки для написания UI‑тестов. Когда тесты будут написаны, запускать их каждый раз вручную из Xcode неудобно — особенно когда прогон должен идти автоматически, без участия человека (например, на сервере при каждом коммите). Здесь на сцену выходят консольные инструменты.

Консольные инструменты

Для автоматизации сборки и запуска тестов, особенно в CI/CD, используются консольные команды.

xcodebuild — инструмент командной строки для управления проектом Xcode. С его помощью можно собирать приложение, запускать тесты и выполнять другие действия. Пример запуска UI‑тестов в симуляторе:

# Сборка проектаxcodebuild -project TestApp.xcodeproj -scheme TestApp build# Запуск тестовxcodebuild test -project TestApp.xcodeproj -scheme TestAppTests \  -destination 'platform=iOS Simulator,name=iPhone 17'# Сборка для тестирования (без запуска тестов)xcodebuild build-for-testing -project TestApp.xcodeproj -scheme TestApp

xcrun simctl — утилита для управления симуляторами iOS из консоли. Позволяет создавать, перезагружать и удалять симуляторы, устанавливать в них приложения и пр. Примеры команд:

# Список доступных симуляторовxcrun simctl list devices# Запустить конкретный симулятор по его UDIDxcrun simctl boot <UDID># Установить приложение в запущенный симуляторxcrun simctl install booted TestApp.app# Выключить запущенный симуляторxcrun simctl shutdown booted# Стереть содержимое и настройки симулятораxcrun simctl erase <UDID>

Эти команды особенно полезны при написании CI/CD‑скриптов: они позволяют настраивать среду и запускать тесты без открытия Xcode.

Запуская сборку и тесты через консоль, Xcode попутно создаёт ряд служебных файлов. На первый взгляд они скрыты от глаз, но именно от них зависит скорость сборки и сама возможность запустить тесты вне IDE. Разберёмся, что это за артефакты.

Артефакты сборки и запуска

DerivedData

DerivedData — это папка, в которой Xcode хранит кеш и промежуточные результаты сборки проекта: скомпилированный код, индексы, логи и собранное приложение (.app).

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

Иногда при возникновении необъяснимых ошибок в Xcode очистка этой папки помогает решить проблему (например, через Xcode → Settings → Locations).

Быстрый доступ к папке DerivedData через Xcode

Быстрый доступ к папке DerivedData через Xcode

XCTestRun

.xctestrun — конфигурационный файл в формате .plist, содержащий всю необходимую информацию для запуска тестов вне Xcode:

  • список тестов, которые будут выполнены;

  • информация о целевых устройствах и симуляторах;

  • пути к приложению и тестовым бандлам;

  • переменные окружения для тестов;

  • параметры запуска и аргументы командной строки.

Пример содержания файла:

<key>TestAppUITests</key>           <!-- имя тестового таргета --><dict>    <key>TestBundlePath</key>        <!-- где лежит бандл с тестами -->    <string>TestAppUITests.xctest</string>    <key>TestHostPath</key>          <!-- какое приложение запускать -->    <string>TestApp.app</string>    <key>TestingEnvironmentVariables</key>  <!-- переменные окружения для тестов -->    <dict>        <key>APP_ENV</key>        <string>testing</string>    </dict></dict>

При запуске тестов (через консоль, Fastlane или другие инструменты) сначала генерируется файл .xctestrun, а затем выполняются тесты согласно его настройкам. Без этого файла выполнить UI‑тесты невозможно: он является основным описанием набора тестов для тест‑раннера.

Файл .xctestrun лежит в DerivedData проекта, в папке Build/Products/; в CI путь можно задать вручную флагом -derivedDataPath.

XCResult

Когда прогон завершён, его результаты Xcode складывает в бандл .xcresult. Это отчёт с итогами: статусы тестов (pass / fail), логи, скриншоты и видео падений, замеры производительности и прочие вложения. Открыть его можно прямо в Xcode или распарсить из консоли утилитой xcresulttool. А чтобы превратить «сырой» прогон в наглядный отчёт, его данные выгружают в системы отчётности вроде Allure — например, утилитой xcresults, которая конвертирует .xcresult в формат Allure.

Пример отчета .xcresult в Xcode

Пример отчета .xcresult в Xcode

Файл .xctestrun мы упомянули не случайно: именно его читает тест‑раннер. Самое время разобраться, что это за компонент.

Тестовые раннеры

Тестовый раннер (test runner) — это компонент тестовой системы, который обнаруживает тесты, запускает их в нужной среде, управляет выполнением и собирает результаты. Он может быть как отдельным инструментом, так и частью фреймворка.

Часто понятия тестового раннера и фреймворка смешиваются, так как многие инструменты поставляются «всё в одном»:

  • Фреймворк (например, XCTest, PyTest, Appium) — это библиотека, которую вы подключаете в проект. Она даёт синтаксис и правила для написания тестов и проверок.

  • Раннер — это утилита (часто консольная), которая находит и запускает тесты, а затем показывает результат их выполнения.

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

  • Находит и отбирает нужные тесты для запуска. Раннер сканирует проект, находит все методы, помеченные как тесты (например, аннотацией @Test или префиксом test...), и может фильтровать их по тегам, группам или названиям.

  • Решает, в каком порядке и с какими параметрами их запускать. Определяет последовательность выполнения тестов, может менять порядок для оптимизации или передавать специфичные параметры конфигурации.

  • Управляет устройствами и симуляторами, на которых идут тесты. Раннер может запускать и останавливать симуляторы, запускать физические девайсы, устанавливать на них приложение и очищать окружение между запусками.

  • Может запускать тесты параллельно на нескольких девайсах. Поддерживает параллелизацию и шардинг — разделение тестов на части и распределение их по нескольким исполнителям (воркерам) — отдельным процессам или симуляторам, которые работают одновременно, что значительно ускоряет прогон.

  • Повторно запускает упавшие тесты (retry). Автоматически перезапускает нестабильные тесты, чтобы отличить реальные баги от случайных падений.

  • Собирает и формирует детальные отчёты. После выполнения раннер собирает логи, скриншоты, видео и статус каждого теста, формируя итоговый отчёт в удобном формате — консольный вывод, HTML, XML или JSON.

  • Передаёт результаты в CI/CD и другие внешние системы. Интегрируется с системами непрерывной интеграции (Jenkins, GitLab CI, GitHub Actions), отправляет метрики в мониторинговые платформы и уведомления в мессенджеры или другие каналы.

Можно выделить следующие популярные тестовые раннеры в мобильной разработке на iOS.

xcodebuild

Прежде чем тянуть в проект сторонний инструмент, стоит знать: базовый раннер у вас уже есть. Это сам xcodebuild — начиная с Xcode 10 он умеет в параллельное тестирование «из коробки». Достаточно передать флаги, и Xcode сам клонирует симуляторы и распределит тесты по процессам‑воркерам:

xcodebuild test -project TestApp.xcodeproj -scheme TestAppTests \  -destination 'platform=iOS Simulator,name=iPhone 15' \  -parallel-testing-enabled YES \  -parallel-testing-worker-count 4

Для небольших и средних проектов этого часто достаточно. Сторонние раннеры нужны, когда хочется большего: распределить прогон по нескольким машинам, гибко управлять retry и шардингом, собирать единый отчёт по всему парку устройств.

Fastlane

Fastlane — наиболее популярное решение, написанное на Ruby. Это не просто раннер, а набор инструментов для автоматизации всего цикла мобильной разработки:

  • автоматизация сборки, подписания и публикации приложений;

  • управление скриншотами и метаданными App Store;

  • интеграция с множеством сервисов (Slack, TestFlight, Firebase);

  • гибкость благодаря Ruby-скриптингу.

Помимо сборки и деплоя, Fastlane умеет запускать тесты (команда run_tests или scan). В Fastfile можно описывать сложные сценарии сборки и тестирования. Пример конвейера для UI-тестов:

lane :test do  run_tests(      scheme: "TestAppTests",      devices: ["iPhone 15", "iPad Pro"],      output_directory: "./test_output",      parallel_testrun_count: 4     )endlane :build_and_test do  build_app(scheme: "TestApp")  run_tests  slack(message: "Tests completed successfully!")end

Небольшой нюанс про актуальность: после передачи проекта от Google в Mobile Native Foundation Fastlane какое-то время выглядел почти заброшенным, но в конце 2025 года в проект вернулись мейнтейнеры и возобновили активность, релизы продолжают выходить. Так что инструмент жив, хотя и пережил период затишья.

Marathon Labs

Marathon Labs — тестовый раннер от Marathon Labs, который управляет тестами через конфигурационный YAML‑файл. В Marathonfile обычно указывают:

  • путь к .xctestrun файлу (определяет набор тестов);

  • список устройств (симуляторы или реальные девайсы);

  • число retries (количество повторов при падении теста) и другие параметры:

name: "iOS Tests"tests:  - testrun: "TestAppTests.xctestrun"devices:  - "iPhone 15"  - "iPad Pro (12.9-inch)"retryCount: 2parallelism: 4outputDir: "./marathon-results"

Marathon Labs оптимизирован для облачного выполнения тестов и обеспечивает эффективное распределение нагрузки.

Emcee

Emcee — тестовый раннер от команды Avito для распределённого запуска тестов. Позволяет параллельно прогонять тесты на нескольких машинах или симуляторах, интегрируется в CI/CD, автоматически балансирует нагрузку и собирает результаты. Особенно подходит для крупных проектов с тысячами тестов.

Bluepill

Bluepill — тестовый раннер от LinkedIn для параллельного запуска iOS‑тестов. Он стартует несколько симуляторов сразу и распределяет тесты между ними, что сокращает общее время прогона большого набора UI‑тестов.

Последний релиз в репозитории Bluepill на GitHub был выпущен 30 января 2024 года. Возможно, проект заморожен.

Flank

Flank — массово‑параллельный раннер для iOS и Android, заточенный под Firebase Test Lab. Конфигурируется через YAML, совместимый с gcloud CLI, умеет в шардинг и параллельный прогон на большом парке облачных устройств. Удобен тем, кто уже использует экосистему Firebase / Google Cloud: позволяет масштабировать прогон, не поднимая собственную ферму устройств.

Xcode Cloud

Xcode Cloud — нативный CI/CD‑сервис от Apple, встроенный прямо в Xcode и App Store Connect. Это не раннер в чистом виде, а целая платформа: она собирает проект, прогоняет тесты на облачных симуляторах и устройствах Apple и доставляет сборки в TestFlight. Главные плюсы — нулевая настройка инфраструктуры и максимально нативная интеграция; главный минус — это платный сервис, привязанный к экосистеме Apple.

Есть и другие раннеры в мире iOS разработки. Главное понять принцип их работы и выбрать инструмент под свои задачи. Мы собрали полный арсенал: разобрались, зачем нужны UI‑тесты, чем их писать, где запускать, что происходит «под капотом» при сборке и что помогает гонять тесты автоматически. Теперь сложим всё вместе и пройдём путь создания автотеста от начала до конца.

Алгоритм создания UI‑автотестов

Процесс создания автотеста можно разбить на следующие шаги:

  1. Собрать проект. Убедиться, что приложение успешно компилируется (например, через Xcode или xcodebuild).

  2. Найти локаторы интересующих UI‑элементов. С помощью Accessibility Inspector убедиться, что все необходимые UI‑элементы имеют уникальные и статичные accessibility‑идентификаторы. Если их нет — поставить задачу разработчикам или добавить самостоятельно.

  3. Написать автотест. Реализовать сценарий в коде на Swift с помощью фреймворка XCUITest: создать новый тестовый класс, использовать API XCUITest для взаимодействия с элементами и ассерты для проверок.

  4. Проверить тест. Запустить его локально на симуляторе или реальном устройстве и убедиться, что тест корректно проходит и проверяет нужные состояния. При необходимости — провести отладку.

  5. Интеграция в репозиторий. Сохранить изменения в системе контроля версий и отправить в удалённый репозиторий (GitHub, GitLab и др.).

  6. Запуск в CI/CD. Организовать автоматический прогон тестов при пуше в репозиторий. После выполнения формируется отчёт (например, с помощью Allure) с результатами прогона.

Такой подход обеспечивает качество автотестов и их надёжную интеграцию в процесс разработки продукта.

Что дальше

В этой части мы разложили нативную автоматизацию iOS на основные «винтики»: поняли, какое место UI‑тесты занимают в пирамиде тестирования и почему их не должно быть много, познакомились с ключевыми инструментами, сравнили фреймворки, отделили их от тестовых раннеров, прошлись по консольным командам и артефактам сборки и, наконец, собрали всё в единый алгоритм.

Это была вводная часть, чтобы погружение в нативные автотесты проходило плавно и постепенно. В следующих частях перейдём непосредственно к практике:

  • как находить локаторы и как разработчики проставляют accessibilityIdentifier в коде;

  • пишем первый рабочий UI‑тест шаг за шагом;

  • ожидания, стабильность и борьба с flaky‑тестами;

  • паттерн Page Object и устойчивая архитектура тестов;

  • отчётность с Allure и запуск в CI/CD.

Больше пишу про мобильное тестирование и не только в своём тг-канале.

Если есть вопросы или темы, которые хочется разобрать подробнее, — пишите в комментариях. Отвёртки не убираем, продолжение следует.

ссылка на оригинал статьи https://habr.com/ru/articles/1053724/