Привет, Хабр! На связи снова Максим из 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 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).
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 )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‑автотестов
Процесс создания автотеста можно разбить на следующие шаги:
-
Собрать проект. Убедиться, что приложение успешно компилируется (например, через 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.
Больше пишу про мобильное тестирование и не только в своём тг-канале.
Если есть вопросы или темы, которые хочется разобрать подробнее, — пишите в комментариях. Отвёртки не убираем, продолжение следует.
ссылка на оригинал статьи https://habr.com/ru/articles/1053724/