Разработка UI-тестов для iOS-приложения (XCUITest)
XCUITest — это инструментальное тестирование: тест запускается на реальном симуляторе или устройстве, управляет приложением через Accessibility API, нажимает кнопки и проверяет, что на экране появился правильный текст. Медленнее юнит-тестов в разы, но покрывает то, что юнит-тестами не проверить — навигацию, отображение данных, реакцию на ввод.
Основные паттерны и частые ошибки
Самая распространённая проблема XCUITest — хрупкость. Тест ищет элемент по label, дизайнер меняет текст кнопки — тест падает. Правильное решение: accessibilityIdentifier.
// В коде приложения
button.accessibilityIdentifier = "loginButton"
// В тесте
let loginButton = app.buttons["loginButton"]
XCTAssertTrue(loginButton.exists)
loginButton.tap()
accessibilityIdentifier не отображается пользователю и не меняется при локализации. Это единственный надёжный способ адресовать элементы.
Второй антипаттерн: sleep(3) вместо ожидания элемента. Тест с хардкодными паузами нестабилен и медленен.
// Плохо
sleep(3)
XCTAssertTrue(app.staticTexts["Welcome"].exists)
// Правильно
let welcomeText = app.staticTexts["Welcome"]
XCTAssertTrue(welcomeText.waitForExistence(timeout: 5))
waitForExistence(timeout:) блокирует поток до появления элемента или истечения таймаута. Тест завершается быстрее при успехе и не зависит от скорости CI-машины.
Page Object паттерн
При наличии 20+ тест-сценариев дублирование локаторов становится проблемой. Page Object изолирует UI-взаимодействия:
struct LoginScreen {
private let app: XCUIApplication
var emailField: XCUIElement { app.textFields["emailInput"] }
var passwordField: XCUIElement { app.secureTextFields["passwordInput"] }
var loginButton: XCUIElement { app.buttons["loginButton"] }
var errorLabel: XCUIElement { app.staticTexts["errorMessage"] }
func login(email: String, password: String) {
emailField.tap()
emailField.typeText(email)
passwordField.tap()
passwordField.typeText(password)
loginButton.tap()
}
}
// Тест читается как сценарий, а не как набор UI-инструкций
func testLoginWithInvalidCredentials() {
let loginScreen = LoginScreen(app: app)
loginScreen.login(email: "[email protected]", password: "badpass")
XCTAssertTrue(loginScreen.errorLabel.waitForExistence(timeout: 3))
}
Мокирование бэкенда
UI-тесты не должны зависеть от реального сервера. Два подхода:
Launch arguments — приложение в тест-режиме загружает mock-данные:
// setUp
app.launchArguments = ["--uitesting", "--mock-auth-success"]
// В AppDelegate / SceneDelegate
if ProcessInfo.processInfo.arguments.contains("--uitesting") {
setupMockDependencies()
}
Локальный HTTP mock — Swifter или GCDWebServer поднимает локальный сервер в тест-хосте. Более реалистично, но сложнее в настройке.
Скриншот-тесты
XCTAttachment позволяет сохранять скриншоты в момент теста для дальнейшего анализа. Для снапшот-тестирования UI (детект визуальных регрессий) используем SnapshotTesting от Point-Free — сравнивает PNG-снапшоты с эталонами.
Запуск в CI
- name: Run UI Tests
run: |
xcodebuild test \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 15 Pro,OS=17.2' \
-resultBundlePath TestResults.xcresult \
-testPlan UITests
Параллельный запуск через -parallel-testing-enabled YES ускоряет большой suite. На Firebase Test Lab — матрица физических устройств для финального прогона перед релизом.
Что покрываем UI-тестами
- Critical user flows: регистрация, логин, онбординг, оплата
- Edge cases навигации: deep link, push notification tap, force close и возврат
- Доступность: VoiceOver через
XCUIApplication().activate()в accessibility mode
Срок: 3–5 дней на создание базового suite для critical user flows с Page Object структурой и CI-интеграцией.







