Введение в 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
- В Xcode выберите File → New → Target
- Выберите UI Testing Bundle
- Назовите его
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
Лучшие Практики
- Используйте идентификаторы доступности для надежной идентификации
- Реализуйте паттерн Page Object для поддерживаемости
- Держите тесты независимыми: Каждый тест должен выполняться изолированно
- Следуйте паттерну AAA: Arrange, Act, Assert
- Интегрируйте в CI/CD: Автоматизируйте выполнение тестов
Заключение
XCUITest предоставляет надежное нативное решение для iOS UI тестирования с отличной интеграцией с Xcode. Комбинируя идентификацию на основе доступности, паттерн Page Object и интеграцию CI/CD, вы можете построить поддерживаемую и надежную suite UI тестов.
Ключевые Выводы:
- Всегда используйте идентификаторы доступности
- Применяйте паттерн Page Object для поддерживаемости
- Используйте нативные механизмы ожидания XCTest
- Интегрируйте UI тесты в CI/CD пайплайны
- Держите тесты независимыми и детерминированными