Введение в XCUITest

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

Это комплексное руководство охватывает продвинутые техники XCUITest (как обсуждается в Mobile Testing in 2025: iOS, Android and Beyond) (как обсуждается в [Appium 2.0: New Architecture and Cloud (как обсуждается в Cloud Testing Platforms: Complete Guide to BrowserStack, Sauce Labs, AWS Device Farm & More) Integration for Modern Mobile Testing](/blog/appium-2-architecture-cloud)), от базовой настройки до сложных сценариев автоматизации, тестирования на основе доступности и стратегий непрерывной интеграции.

Настройка 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 пайплайны
  • Держите тесты независимыми и детерминированными