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

С инструментами для написания и отладки тестов мы разобрались. Но прежде чем запускать тесты, нужно ответить ещё на один вопрос — где именно они будут выполняться.
Окружения выполнения тестов: симулятор и физическое устройство
Автотесты на 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 XCTest final 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 XCTest import EarlGrey final 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 # Запустить конкретный симулятор по его UDID xcrun 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).

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.

Файл .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 ) end lane :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: 2 parallelism: 4 outputDir: "./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‑автотестов
Процесс создания автотеста можно разбить на следующие шаги:
Собрать проект. Убедиться, что приложение успешно компилируется (например, через Xcode или
xcodebuild).Найти локаторы интересующих UI‑элементов. С помощью Accessibility Inspector убедиться, что все необходимые UI‑элементы имеют уникальные и статичные accessibility‑идентификаторы. Если их нет — поставить задачу разработчикам или добавить самостоятельно.
Написать автотест. Реализовать сценарий в коде на Swift с помощью фреймворка XCUITest: создать новый тестовый класс, использовать API XCUITest для взаимодействия с элементами и ассерты для проверок.
Проверить тест. Запустить его локально на симуляторе или реальном устройстве и убедиться, что тест корректно проходит и проверяет нужные состояния. При необходимости — провести отладку.
Интеграция в репозиторий. Сохранить изменения в системе контроля версий и отправить в удалённый репозиторий (GitHub, GitLab и др.).
Запуск в CI/CD. Организовать автоматический прогон тестов при пуше в репозиторий. После выполнения формируется отчёт (например, с помощью Allure) с результатами прогона.
Такой подход обеспечивает качество автотестов и их надёжную интеграцию в процесс разработки продукта.
Что дальше
В этой части мы разложили нативную автоматизацию iOS на основные «винтики»: поняли, какое место UI‑тесты занимают в пирамиде тестирования и почему их не должно быть много, познакомились с ключевыми инструментами, сравнили фреймворки, отделили их от тестовых раннеров, прошлись по консольным командам и артефактам сборки и, наконец, собрали всё в единый алгоритм.
Это была вводная часть, чтобы погружение в нативные автотесты проходило плавно и постепенно. В следующих частях перейдём непосредственно к практике:
как находить локаторы и как разработчики проставляют
accessibilityIdentifierв коде;пишем первый рабочий UI‑тест шаг за шагом;
ожидания, стабильность и борьба с flaky‑тестами;
паттерн Page Object и устойчивая архитектура тестов;
отчётность с Allure и запуск в CI/CD.
Больше пишу про мобильное тестирование и не только в своём тг-канале.
Если есть вопросы или темы, которые хочется разобрать подробнее, — пишите в комментариях. Отвёртки не убираем, продолжение следует.
