E2E тестирование Swift-приложений: от XCUITest до реальных кейсов

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему E2E-тесты — это «режим бога» для iOS-разработчика?

Представьте: вы написали идеальный Swift-код, всё компилируется, юнит-тесты зелёные. Выпускаете релиз — и через час летят баги: кнопка «Купить» не нажимается на iPhone 14 Pro, а на iPad Mini экран входа выглядит как абстрактная живопись. Знакомо? Именно здесь на сцену выходит E2E тестирование Swift — единственный способ быть уверенным, что ваше приложение работает как часы в руках реального пользователя.

End-to-End (E2E) тесты имитируют полный сценарий пользователя: от запуска приложения до завершения целевого действия (покупка, регистрация, отправка формы). В этой статье мы разберём, как настроить XCUITest Swift так, чтобы ваши тесты не падали по пустякам, а CI/CD-пайплайн не превращался в ад.

Что вы узнаете:

  • Как писать стабильные UI-тесты с XCUITest
  • Page Object Model для Swift — реальный пример
  • Обработка асинхронных операций и ожиданий
  • Интеграция E2E-тестов в CI/CD (GitHub Actions + Fastlane)
  • Топ-3 ошибки новичков и как их избежать

Инструментарий: что нам понадобится

Для автоматизации тестирования Swift мы используем три кита:

  1. XCUITest — фреймворк от Apple, встроенный в XCTest. Позволяет эмулировать жесты, проверять состояния UI-элементов.
  2. Xcode Test Plan — для конфигурации тестовых схем (разные языки, ориентации, данные).
  3. Fastlane + GitHub Actions — для прогона тестов на CI.

Базовый синтаксис XCUITest прост: вы получаете доступ к приложению через XCUIApplication(), ищете элементы через buttons["Login"] или textFields["email"] и эмулируете действия.

Пишем первый E2E-тест: сценарий логина

Допустим, у нас есть экран входа с полями Email, Password и кнопкой «Sign In». Цель: проверить, что при вводе корректных данных пользователь переходит на главный экран.

import XCTest

class LoginE2ETests: XCTestCase {
    
    let app = XCUIApplication()
    
    override func setUpWithError() throws {
        continueAfterFailure = false
        app.launch()
    }
    
    func testSuccessfulLogin() throws {
        // 1. Находим элементы
        let emailField = app.textFields["emailTextField"]
        let passwordField = app.secureTextFields["passwordTextField"]
        let loginButton = app.buttons["signInButton"]
        
        // 2. Действия пользователя
        emailField.tap()
        emailField.typeText("user@example.com")
        
        passwordField.tap()
        passwordField.typeText("ValidPassword123")
        
        loginButton.tap()
        
        // 3. Проверяем результат
        let dashboard = app.staticTexts["dashboardTitle"]
        XCTAssertTrue(dashboard.waitForExistence(timeout: 5))
    }
}

Важный момент: используйте waitForExistence(timeout:) — без этого тест может упасть, если анимация или сетевой запрос задерживаются. Это база E2E тестирования Swift.

Почему этот код может упасть?

  • Поле ввода может не быть первым responder — добавьте .tap() перед typeText.
  • Кнопка может быть перекрыта клавиатурой — используйте app.keyboards.buttons["Return"].tap().
  • Текстовые метки могут отличаться в зависимости от локализации — всегда используйте Accessibility Identifier.

Page Object Model (POM) для Swift: делаем тесты чистыми

Когда приложение разрастается, тесты превращаются в кашу. Решение — Page Object Model. Мы создаём отдельные классы для каждого экрана, которые инкапсулируют логику поиска элементов и действий.

Пример для экрана логина:

// LoginPage.swift
import XCTest

class LoginPage {
    let app: XCUIApplication
    
    init(app: XCUIApplication) {
        self.app = app
    }
    
    var emailField: XCUIElement {
        app.textFields["emailTextField"]
    }
    
    var passwordField: XCUIElement {
        app.secureTextFields["passwordTextField"]
    }
    
    var loginButton: XCUIElement {
        app.buttons["signInButton"]
    }
    
    var errorLabel: XCUIElement {
        app.staticTexts["errorMessage"]
    }
    
    @discardableResult
    func login(email: String, password: String) -> Self {
        emailField.tap()
        emailField.typeText(email)
        passwordField.tap()
        passwordField.typeText(password)
        loginButton.tap()
        return self
    }
    
    func isErrorDisplayed() -> Bool {
        return errorLabel.waitForExistence(timeout: 2)
    }
}

А теперь тест с использованием POM:

func testInvalidLoginShowsError() {
    let loginPage = LoginPage(app: app)
    loginPage.login(email: "wrong@email", password: "short")
    
    XCTAssertTrue(loginPage.isErrorDisplayed(), 
                  "Ошибка должна отображаться при неверных данных")
}

Преимущества: при изменении UI вы правите один класс, а не 20 тестов. Это must-have для UI тестов iOS Swift в продакшене.

Работа с асинхронностью: ожидания и коллбеки

Сетевое взаимодействие, анимации, переходы между экранами — главные враги стабильных тестов. В Swift XCTest UI тестах есть несколько способов справиться:

1. Ожидание существования элемента

let element = app.buttons["submitButton"]
let exists = element.waitForExistence(timeout: 10)
XCTAssertTrue(exists)

2. Ожидание исчезновения (например, лоадера)

let spinner = app.activityIndicators["loadingSpinner"]
let spinnerHides = expectation(for: NSPredicate(format: "exists == false"), 
                                evaluatedWith: spinner, handler: nil)
wait(for: [spinnerHides], timeout: 10)

3. Ожидание появления текста

let welcomeText = app.staticTexts["Welcome, User!"]
let textAppears = expectation(for: NSPredicate(format: "exists == true"), 
                               evaluatedWith: welcomeText, handler: nil)
wait(for: [textAppears], timeout: 5)

Никогда не используйте sleep(3) — это убивает производительность и надёжность. Всегда используйте ожидания на основе предикатов.

Интеграция E2E-тестов в CI/CD: GitHub Actions + Fastlane

Чтобы автоматизация тестирования Swift работала на каждый PR, настраиваем пайплайн. Пример конфигурации Fastlane:

# Fastfile
lane :e2e_t                

Блоги

Рекомендаций по книгам