XCUITest — это нативный фреймворк Apple для UI тестирования iOS, встроенный прямо в Xcode. iOS занимает около 27% мирового рынка смартфонов (Statcounter 2024), что представляет огромную QA-ответственность для мобильных команд. Фреймворк XCTest Apple, включающий как юнит-тесты, так и UI-тесты через XCUITest, — это единственный фреймворк, способный работать на iOS Simulators и физических устройствах без стороннего middleware. Согласно документации Apple по тестированию, XCUITest использует Accessibility API для взаимодействия с UI элементами, что делает тесты изначально устойчивыми к изменениям макета — при условии правильной установки идентификаторов доступности.

“XCUITest — это самый быстрый путь к надёжной iOS UI автоматизации для iOS-only команды. Интеграция с отчётами Xcode, параллельные тесты на нескольких симуляторах и идентификация элементов через accessibility дают production-quality инструментарий из коробки. Appium хорош для кроссплатформы, но для iOS-native — XCUITest выигрывает по скорости и надёжности.” — Юрий Кан, Senior QA Lead

TL;DR — XCUITest — нативный iOS UI фреймворк Apple, встроенный в Xcode. Использует Accessibility API для идентификации элементов. Быстрее и надёжнее Appium для iOS-only проектов. Параллельные тесты на нескольких симуляторах. Нативная интеграция с Xcode Cloud.

Введение в XCUITest

XCUITest — это нативный фреймворк Apple для UI тестирования iOS приложений, предоставляющий мощные инструменты для автоматизации тестирования пользовательских интерфейсов. В отличие от юнит тестов, которые проверяют логику кода изолированно, UI тесты валидируют полный пользовательский опыт, симулируя реальные взаимодействия пользователя с приложением.

Для команд, строящих комплексные стратегии мобильного тестирования, XCUITest хорошо интегрируется с непрерывным тестированием в DevOps. В сочетании с оптимизацией CI/CD пайплайнов iOS UI тесты запускаются автоматически при каждом изменении кода.

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

Настройка XCUITest

Создание UI Test Target

  1. В Xcode выберите File → New → Target
  2. Выберите UI Testing Bundle
  3. Назовите его YourAppUITests

Базовая Структура Теста

import XCTest

class LoginTests: XCTestCase {
    var app: XCUIApplication!

    override func setUpWithError() throws {
        continueAfterFailure = false
        app = XCUIApplication()
        app.launchArguments = ["UI-Testing"]
        app.launch()
    }

    override func tearDownWithError() throws {
        app = nil
    }

    func testSuccessfulLogin() throws {
        // Реализация теста
    }
}

Стратегии Идентификации Элементов

Идентификаторы Доступности (Рекомендуется)

Наиболее надежный метод для идентификации элементов:

// В коде вашего приложения (UIViewController)
class LoginViewController: UIViewController {
    @IBOutlet weak var emailTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var loginButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        emailTextField.accessibilityIdentifier = "emailField"
        passwordTextField.accessibilityIdentifier = "passwordField"
        loginButton.accessibilityIdentifier = "loginButton"
    }
}

// В вашем UI тесте
let emailField = app.textFields["emailField"]
let passwordField = app.secureTextFields["passwordField"]
let loginButton = app.buttons["loginButton"]

SwiftUI Доступность

// SwiftUI View
struct LoginView: View {
    var body: some View {
        VStack {
            TextField("Email", text: $email)
                .accessibilityIdentifier("emailField")

            SecureField("Пароль", text: $password)
                .accessibilityIdentifier("passwordField")

            Button("Войти") {
                login()
            }
            .accessibilityIdentifier("loginButton")
        }
    }
}

Написание Эффективных UI Тестов

Тест Процесса Входа

func testLoginWithValidCredentials() throws {
    // Arrange
    let email = "test@example.com"
    let password = "Password123"

    // Act
    app.textFields["emailField"].tap()
    app.textFields["emailField"].typeText(email)

    app.secureTextFields["passwordField"].tap()
    app.secureTextFields["passwordField"].typeText(password)

    app.buttons["loginButton"].tap()

    // Assert
    let welcomeMessage = app.staticTexts["welcomeMessage"]
    XCTAssertTrue(welcomeMessage.waitForExistence(timeout: 5))
    XCTAssertTrue(welcomeMessage.label.contains("Добро пожаловать"))
}

func testLoginWithInvalidCredentials() throws {
    app.textFields["emailField"].tap()
    app.textFields["emailField"].typeText("invalid@example.com")

    app.secureTextFields["passwordField"].tap()
    app.secureTextFields["passwordField"].typeText("wrong")

    app.buttons["loginButton"].tap()

    let errorAlert = app.alerts["Ошибка"]
    XCTAssertTrue(errorAlert.waitForExistence(timeout: 3))
    XCTAssertTrue(errorAlert.staticTexts["Неверные учетные данные"].exists)

    errorAlert.buttons["OK"].tap()
}

Обработка Оповещений и Разрешений

func testLocationPermission() throws {
    app.buttons["enableLocation"].tap()

    // Обработка системного оповещения о разрешениях
    let springboard = XCUIApplication(
        bundleIdentifier: "com.apple.springboard"
    )

    let alert = springboard.alerts.element
    XCTAssertTrue(alert.waitForExistence(timeout: 3))

    alert.buttons["Разрешить При Использовании"].tap()

    // Проверка что приложение реагирует на разрешение
    let confirmationMessage = app.staticTexts["locationEnabled"]
    XCTAssertTrue(confirmationMessage.waitForExistence(timeout: 2))
}

Паттерн Page Object

Реализация

// PageObjects/LoginScreen.swift
class LoginScreen {
    private let app: XCUIApplication

    init(app: XCUIApplication) {
        self.app = app
    }

    // Элементы
    var emailField: XCUIElement {
        app.textFields["emailField"]
    }

    var passwordField: XCUIElement {
        app.secureTextFields["passwordField"]
    }

    var loginButton: XCUIElement {
        app.buttons["loginButton"]
    }

    // Действия
    @discardableResult
    func enterEmail(_ email: String) -> Self {
        emailField.tap()
        emailField.typeText(email)
        return self
    }

    @discardableResult
    func enterPassword(_ password: String) -> Self {
        passwordField.tap()
        passwordField.typeText(password)
        return self
    }

    @discardableResult
    func tapLogin() -> Self {
        loginButton.tap()
        return self
    }

    func login(email: String, password: String) {
        enterEmail(email)
            .enterPassword(password)
            .tapLogin()
    }
}

// Использование в тестах
class LoginTests: XCTestCase {
    var app: XCUIApplication!
    var loginScreen: LoginScreen!
    var homeScreen: HomeScreen!

    override func setUpWithError() throws {
        continueAfterFailure = false
        app = XCUIApplication()
        app.launch()

        loginScreen = LoginScreen(app: app)
        homeScreen = HomeScreen(app: app)
    }

    func testSuccessfulLogin() throws {
        loginScreen.login(
            email: "test@example.com",
            password: "Password123"
        )

        XCTAssertTrue(homeScreen.waitForScreen())
        XCTAssertTrue(homeScreen.verifyWelcomeMessage(
            contains: "Добро пожаловать"
        ))
    }
}

Продвинутые Техники

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

func testAppLaunchPerformance() throws {
    measure(metrics: [XCTApplicationLaunchMetric()]) {
        XCUIApplication().launch()
    }
}

func testScrollPerformance() throws {
    let tableView = app.tables["productList"]

    measure(metrics: [XCTOSSignpostMetric.scrollDecelerationMetric]) {
        tableView.swipeUp(velocity: .fast)
    }
}

Вспомогательные Функции и Расширения

extension XCUIElement {
    func waitForExistenceAndTap(timeout: TimeInterval = 5) -> Bool {
        guard waitForExistence(timeout: timeout) else {
            return false
        }
        tap()
        return true
    }

    func clearText() {
        guard let stringValue = value as? String else {
            return
        }

        tap()
        let deleteString = String(
            repeating: XCUIKeyboardKey.delete.rawValue,
            count: stringValue.count
        )
        typeText(deleteString)
    }
}

Непрерывная Интеграция

Интеграция с Fastlane

# Fastfile
lane :ui_tests do
  scan(
    scheme: "MyApp",
    devices: ["iPhone 15", "iPad Pro (12.9-inch)"],
    clean: true,
    output_directory: "./test_output",
    output_types: "html,junit",
    fail_build: true
  )
end

Лучшие Практики

  1. Используйте идентификаторы доступности для надежной идентификации
  2. Реализуйте паттерн Page Object для поддерживаемости
  3. Держите тесты независимыми: Каждый тест должен выполняться изолированно
  4. Следуйте паттерну AAA: Arrange, Act, Assert
  5. Интегрируйте в CI/CD: Автоматизируйте выполнение тестов

Заключение

XCUITest предоставляет надежное нативное решение для iOS UI тестирования с отличной интеграцией с Xcode. Комбинируя идентификацию на основе доступности, паттерн Page Object и интеграцию CI/CD, вы можете построить поддерживаемую и надежную suite UI тестов.

Ключевые Выводы:

  • Всегда используйте идентификаторы доступности
  • Применяйте паттерн Page Object для поддерживаемости
  • Используйте нативные механизмы ожидания XCTest
  • Интегрируйте UI тесты в CI/CD пайплайны
  • Держи тесты независимыми и детерминированными

Часто задаваемые вопросы

Что такое XCUITest и чем отличается от XCTest? XCTest — общий фреймворк Apple для iOS тестирования. XCUITest — UI-тестирующая часть, использующая XCUIApplication и XCUIElement. Все XCUITest-тесты — это XCTest-тесты, но не наоборот.

XCUITest лучше Appium для iOS? XCUITest быстрее и надёжнее для iOS-only проектов без дополнительной инфраструктуры. Appium лучше для кроссплатформы (iOS + Android) или тестов на JavaScript/Python.

Как идентифицировать UI элементы в XCUITest? Рекомендуется accessibilityIdentifier, установленный в коде приложения. Стабилен при редизайне, работает с UIKit и SwiftUI.

Как запустить XCUITest в CI/CD? Используй xcodebuild test или Fastlane с scan. Xcode Cloud — нативный CI. Для GitHub Actions используй macos runner с xcodebuild.

Источники: Документация Apple XCTest · Apple Testing with Xcode

Смотрите также

Официальные ресурсы