Розробка 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"]
// In 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 — матриця фізичних пристроїв для фінального прогону перед релізом.
Що покриваємо
- Critical user flows: реєстрація, логін, онбординг, оплата
- Edge cases навігації: deep link, натиск на push notification, force close та повернення
- Доступність: VoiceOver через
XCUIApplication().activate()в accessibility mode
Срок: 3–5 днів на створення базового suite для critical flows зі структурою Page Object та CI-інтеграцією.







