Руководство - Тестирование

Автоматическое UI-тестирование на Mac-серверах: XCTest, Appium и Selenium

Настройте выделенный Mac-сервер для автоматического UI-тестирования 24/7. Запускайте тесты XCTest, XCUITest, Appium и Selenium параллельно на нескольких симуляторах iOS со стабильными, воспроизводимыми результатами.

30 мин чтения Обновлено в январе 2025

Зачем выделенное оборудование для тестирования?

UI-тестирование на общих или локальных машинах приводит к нестабильности, несогласованности и узким местам. Выделенный Mac-сервер решает эти проблемы.

Стабильные результаты

Никакие другие процессы не конкурируют за CPU или RAM. Тесты выполняются в чистом, контролируемом окружении каждый раз, устраняя нестабильные провалы тестов из-за конкуренции за ресурсы.

Параллельное выполнение

Запускайте тесты на нескольких экземплярах iOS Simulator одновременно. 14 ядер M4 Pro обеспечивают работу 4-6 параллельных экземпляров Simulator без снижения производительности.

Доступность 24/7

Тесты запускаются в любое время — ночные регрессионные прогоны, проверка после merge или по запросу. Сервер всегда готов, ноутбук разработчика не нужен.

XCTest на выделенном Mac

XCTest — встроенный фреймворк тестирования Apple, входящий в Xcode. XCUITest расширяет его для UI-тестирования. Оба работают нативно и не требуют дополнительной настройки, кроме Xcode.

Запуск XCTest из командной строки

# Run all unit tests
xcodebuild test \
  -workspace MyApp.xcworkspace \
  -scheme MyApp \
  -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.2' \
  -resultBundlePath ./TestResults/UnitTests.xcresult

# Run only UI tests
xcodebuild test \
  -workspace MyApp.xcworkspace \
  -scheme MyAppUITests \
  -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.2' \
  -resultBundlePath ./TestResults/UITests.xcresult

# Run a specific test class
xcodebuild test \
  -workspace MyApp.xcworkspace \
  -scheme MyApp \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -only-testing:MyAppTests/LoginTests

# Run a specific test method
xcodebuild test \
  -workspace MyApp.xcworkspace \
  -scheme MyApp \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -only-testing:MyAppTests/LoginTests/testSuccessfulLogin

Тестирование на нескольких целевых устройствах

# Test on multiple iPhone models simultaneously
xcodebuild test \
  -workspace MyApp.xcworkspace \
  -scheme MyApp \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -destination 'platform=iOS Simulator,name=iPhone 16 Pro Max' \
  -destination 'platform=iOS Simulator,name=iPhone SE (3rd generation)' \
  -destination 'platform=iOS Simulator,name=iPad Pro 13-inch (M4)' \
  -resultBundlePath ./TestResults/MultiDevice.xcresult

# List all available destinations
xcodebuild -showdestinations \
  -workspace MyApp.xcworkspace \
  -scheme MyApp

Пакеты результатов и скриншоты

# Extract test results summary
xcrun xcresulttool get --path ./TestResults/UITests.xcresult \
  --format json

# Export test attachments (screenshots, videos)
xcrun xcresulttool export \
  --path ./TestResults/UITests.xcresult \
  --output-path ./TestArtifacts \
  --type attachments

# Get human-readable test summary
xcrun xcresulttool get --path ./TestResults/UITests.xcresult \
  --format json | python3 -m json.tool

Настройка Appium для iOS

Appium — это фреймворк автоматизации с открытым кодом, который позволяет писать тесты на любом языке (Python, JavaScript, Java и т.д.) и запускать их на iOS-приложениях в симуляторах или на реальных устройствах. Под капотом он использует драйвер XCUITest от Apple.

Установка Appium на ваш Mac-сервер

# Install Node.js via Homebrew
brew install node

# Install Appium 2.x globally
npm install -g appium

# Install the XCUITest driver for iOS
appium driver install xcuitest

# Verify installation
appium --version
appium driver list --installed

# Install appium-doctor to check dependencies
npm install -g appium-doctor
appium-doctor --ios

# Start Appium server
appium server --address 127.0.0.1 --port 4723

Настройка Desired Capabilities

# Example capabilities (JSON format for Appium 2.x)
{
    "platformName": "iOS",
    "appium:automationName": "XCUITest",
    "appium:deviceName": "iPhone 16",
    "appium:platformVersion": "18.2",
    "appium:app": "/path/to/MyApp.app",
    "appium:noReset": false,
    "appium:wdaStartupRetries": 3,
    "appium:wdaStartupRetryInterval": 20000,
    "appium:simulatorStartupTimeout": 120000
}

Пример теста Appium (Python)

# Install Appium Python client
# pip install Appium-Python-Client

from appium import webdriver
from appium.options.ios import XCUITestOptions
from appium.webdriver.common.appiumby import AppiumBy

# Configure options
options = XCUITestOptions()
options.platform_name = "iOS"
options.device_name = "iPhone 16"
options.platform_version = "18.2"
options.app = "/path/to/MyApp.app"

# Connect to Appium server
driver = webdriver.Remote(
    command_executor="http://127.0.0.1:4723",
    options=options
)

try:
    # Wait for app to load
    driver.implicitly_wait(10)

    # Find and tap login button
    login_button = driver.find_element(
        AppiumBy.ACCESSIBILITY_ID, "loginButton"
    )
    login_button.click()

    # Enter username
    username_field = driver.find_element(
        AppiumBy.ACCESSIBILITY_ID, "usernameField"
    )
    username_field.send_keys("testuser@example.com")

    # Enter password
    password_field = driver.find_element(
        AppiumBy.ACCESSIBILITY_ID, "passwordField"
    )
    password_field.send_keys("password123")

    # Submit login
    submit_button = driver.find_element(
        AppiumBy.ACCESSIBILITY_ID, "submitButton"
    )
    submit_button.click()

    # Verify welcome screen
    welcome_label = driver.find_element(
        AppiumBy.ACCESSIBILITY_ID, "welcomeLabel"
    )
    assert "Welcome" in welcome_label.text

    print("Test PASSED: Login successful")

finally:
    driver.quit()

Пример теста Appium (JavaScript)

// npm install webdriverio @wdio/cli

const { remote } = require('webdriverio');

async function runTest() {
    const driver = await remote({
        protocol: 'http',
        hostname: '127.0.0.1',
        port: 4723,
        path: '/',
        capabilities: {
            platformName: 'iOS',
            'appium:automationName': 'XCUITest',
            'appium:deviceName': 'iPhone 16',
            'appium:platformVersion': '18.2',
            'appium:app': '/path/to/MyApp.app'
        }
    });

    try {
        // Tap login button
        const loginBtn = await driver.$('~loginButton');
        await loginBtn.click();

        // Enter credentials
        const username = await driver.$('~usernameField');
        await username.setValue('testuser@example.com');

        const password = await driver.$('~passwordField');
        await password.setValue('password123');

        // Submit
        const submit = await driver.$('~submitButton');
        await submit.click();

        // Verify
        const welcome = await driver.$('~welcomeLabel');
        const text = await welcome.getText();
        console.assert(text.includes('Welcome'), 'Login test failed');

        console.log('Test PASSED: Login successful');
    } finally {
        await driver.deleteSession();
    }
}

runTest();

Selenium для Safari

macOS включает Safari и safaridriver из коробки. Не нужно загружать дополнительные драйверы браузера, в отличие от Chrome или Firefox на других платформах.

Включение safaridriver

# Enable the Safari WebDriver (one-time setup)
safaridriver --enable

# Verify safaridriver is working
safaridriver --version

# For headless-like automation, enable "Allow Remote Automation"
# in Safari > Settings > Advanced > Show Develop menu
# Then: Develop > Allow Remote Automation
# Or via command line:
defaults write com.apple.Safari AllowRemoteAutomation 1

Тест Selenium для Safari (Python)

# pip install selenium

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# Create Safari driver
driver = webdriver.Safari()

try:
    # Navigate to your web app
    driver.get("https://your-webapp.com")

    # Wait for page to load
    wait = WebDriverWait(driver, 10)

    # Find and click a button
    login_link = wait.until(
        EC.element_to_be_clickable((By.CSS_SELECTOR, "a.login-btn"))
    )
    login_link.click()

    # Fill in form
    email_input = wait.until(
        EC.presence_of_element_located((By.ID, "email"))
    )
    email_input.send_keys("test@example.com")

    password_input = driver.find_element(By.ID, "password")
    password_input.send_keys("testpassword")

    # Submit form
    submit_btn = driver.find_element(By.CSS_SELECTOR, "button[type='submit']")
    submit_btn.click()

    # Verify redirect to dashboard
    wait.until(EC.url_contains("/dashboard"))
    assert "/dashboard" in driver.current_url

    print("Test PASSED: Safari login flow works correctly")

finally:
    driver.quit()

Тест Selenium для Safari (JavaScript)

// npm install selenium-webdriver

const { Builder, By, until } = require('selenium-webdriver');

async function safariTest() {
    const driver = await new Builder()
        .forBrowser('safari')
        .build();

    try {
        await driver.get('https://your-webapp.com');

        // Click login
        const loginBtn = await driver.findElement(By.css('a.login-btn'));
        await loginBtn.click();

        // Fill form
        const email = await driver.findElement(By.id('email'));
        await email.sendKeys('test@example.com');

        const password = await driver.findElement(By.id('password'));
        await password.sendKeys('testpassword');

        // Submit
        const submit = await driver.findElement(
            By.css("button[type='submit']")
        );
        await submit.click();

        // Verify
        await driver.wait(until.urlContains('/dashboard'), 10000);
        const url = await driver.getCurrentUrl();
        console.assert(url.includes('/dashboard'));

        console.log('Test PASSED: Safari login flow works');
    } finally {
        await driver.quit();
    }
}

safariTest();

Параллельное выполнение тестов

Запуск тестов параллельно значительно сокращает общее время выполнения набора тестов. Mac Mini M4 Pro комфортно работает с 4-6 экземплярами Simulator одновременно.

Параллельное тестирование XCTest

# Enable parallel testing with xcodebuild
xcodebuild test \
  -workspace MyApp.xcworkspace \
  -scheme MyApp \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -parallel-testing-enabled YES \
  -parallel-testing-worker-count 4 \
  -resultBundlePath ./TestResults/Parallel.xcresult

# Parallel testing across multiple device types
xcodebuild test \
  -workspace MyApp.xcworkspace \
  -scheme MyApp \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -destination 'platform=iOS Simulator,name=iPhone SE (3rd generation)' \
  -destination 'platform=iOS Simulator,name=iPad Pro 13-inch (M4)' \
  -parallel-testing-enabled YES \
  -resultBundlePath ./TestResults/MultiDeviceParallel.xcresult

Управление экземплярами Simulator

# List all available simulators
xcrun simctl list devices available

# Create custom simulator instances for testing
xcrun simctl create "Test-iPhone-1" "iPhone 16" "iOS 18.2"
xcrun simctl create "Test-iPhone-2" "iPhone 16" "iOS 18.2"
xcrun simctl create "Test-iPhone-3" "iPhone 16" "iOS 18.2"
xcrun simctl create "Test-iPhone-4" "iPhone 16" "iOS 18.2"

# Boot multiple simulators
xcrun simctl boot "Test-iPhone-1"
xcrun simctl boot "Test-iPhone-2"
xcrun simctl boot "Test-iPhone-3"
xcrun simctl boot "Test-iPhone-4"

# Check booted simulators
xcrun simctl list devices booted

# Shut down all simulators
xcrun simctl shutdown all

# Delete all test simulators
xcrun simctl delete "Test-iPhone-1"
xcrun simctl delete "Test-iPhone-2"
xcrun simctl delete "Test-iPhone-3"
xcrun simctl delete "Test-iPhone-4"

Производительность параллельного тестирования

Конфигурация Длительность 200 UI-тестов Ускорение
1 Simulator (sequential) 45 minutes Baseline
2 Simulators (parallel) 24 minutes 1.9x faster
4 Simulators (parallel) 13 minutes 3.5x faster
6 Simulators (parallel) 10 minutes 4.5x faster

Примечание: Результаты измерены на Mac Mini M4 Pro (14 ядер, 24 ГБ RAM). Оптимальное количество параллельных симуляторов зависит от сложности ваших тестов и требований к памяти. Для большинства наборов UI-тестов 4 параллельных worker обеспечивают лучший баланс скорости и стабильности.

Интеграция с CI/CD

Интегрируйте ваши автоматические тесты в пайплайн CI/CD для автоматического выполнения тестов при каждом push, pull request или по расписанию.

Workflow GitHub Actions для автоматического тестирования

# .github/workflows/ios-tests.yml
name: iOS Automated Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
  schedule:
    # Nightly regression tests at 2 AM UTC
    - cron: '0 2 * * *'

jobs:
  unit-tests:
    name: Unit Tests
    runs-on: self-hosted
    timeout-minutes: 30

    steps:
      - uses: actions/checkout@v4

      - name: Select Xcode version
        run: sudo xcode-select -s /Applications/Xcode_16.2.app

      - name: Resolve dependencies
        run: |
          xcodebuild -resolvePackageDependencies \
            -workspace MyApp.xcworkspace \
            -scheme MyApp

      - name: Run unit tests
        run: |
          xcodebuild test \
            -workspace MyApp.xcworkspace \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.2' \
            -parallel-testing-enabled YES \
            -resultBundlePath $/UnitTests.xcresult

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: unit-test-results
          path: $/UnitTests.xcresult

  ui-tests:
    name: UI Tests
    runs-on: self-hosted
    timeout-minutes: 60
    needs: unit-tests

    steps:
      - uses: actions/checkout@v4

      - name: Select Xcode version
        run: sudo xcode-select -s /Applications/Xcode_16.2.app

      - name: Boot simulators for parallel testing
        run: |
          xcrun simctl boot "iPhone 16" || true
          xcrun simctl boot "iPhone SE (3rd generation)" || true

      - name: Run UI tests in parallel
        run: |
          xcodebuild test \
            -workspace MyApp.xcworkspace \
            -scheme MyAppUITests \
            -destination 'platform=iOS Simulator,name=iPhone 16' \
            -destination 'platform=iOS Simulator,name=iPhone SE (3rd generation)' \
            -parallel-testing-enabled YES \
            -parallel-testing-worker-count 4 \
            -resultBundlePath $/UITests.xcresult

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: ui-test-results
          path: $/UITests.xcresult

      - name: Cleanup simulators
        if: always()
        run: xcrun simctl shutdown all

Запуск тестов Appium в CI

# Add to your GitHub Actions workflow
      - name: Start Appium server
        run: |
          appium server --address 127.0.0.1 --port 4723 &
          sleep 5
          curl http://127.0.0.1:4723/status

      - name: Build app for testing
        run: |
          xcodebuild build-for-testing \
            -workspace MyApp.xcworkspace \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=iPhone 16'

      - name: Run Appium tests
        run: |
          cd tests/appium
          pip install -r requirements.txt
          pytest test_login.py test_checkout.py \
            --junitxml=results.xml -v

      - name: Stop Appium server
        if: always()
        run: pkill -f appium || true

Отчёты о тестировании

Генерируйте и обрабатывайте отчёты о тестах для отслеживания трендов качества и быстрого выявления падающих тестов.

Работа с пакетами xcresult

# Get test results summary as JSON
xcrun xcresulttool get \
  --path ./TestResults.xcresult \
  --format json

# Get specific test action results
xcrun xcresulttool get \
  --path ./TestResults.xcresult \
  --format json \
  --id "REF_ID"

# Export all attachments (screenshots, logs)
xcrun xcresulttool export \
  --path ./TestResults.xcresult \
  --output-path ./artifacts \
  --type attachments

# Merge multiple xcresult bundles
xcrun xcresulttool merge \
  ./UnitTests.xcresult \
  ./UITests.xcresult \
  --output-path ./MergedResults.xcresult

Конвертация в JUnit XML

JUnit XML — универсальный формат, поддерживаемый платформами CI/CD, интеграциями со Slack и дашбордами тестирования.

# Install xcresult-to-junit converter
brew install chargepoint/xcparse/xcparse

# Convert xcresult to JUnit XML
xcparse tests ./TestResults.xcresult ./junit-results/

# Or use trainer (a popular Ruby gem)
gem install trainer
trainer --path ./TestResults.xcresult --output_directory ./reports

# The generated JUnit XML works with:
# - GitHub Actions test summaries
# - Jenkins Test Result plugin
# - GitLab CI test reporting
# - Slack notifications via CI integrations

Скриншот при падении теста

// In your XCUITest, add screenshot capture on failure:
// XCTestCase+Screenshots.swift

import XCTest

extension XCTestCase {
    override func tearDown() {
        if testRun?.hasSucceeded == false {
            let screenshot = XCUIScreen.main.screenshot()
            let attachment = XCTAttachment(screenshot: screenshot)
            attachment.name = "Failure-\(name)"
            attachment.lifetime = .keepAlways
            add(attachment)
        }
        super.tearDown()
    }
}

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

Изоляция тестов

Каждый тест должен быть независимым и не полагаться на состояние предыдущих тестов. Сбрасывайте состояние приложения перед каждым тестом.

// In your XCUITest setUp() method:
override func setUp() {
    super.setUp()
    continueAfterFailure = false

    let app = XCUIApplication()
    app.launchArguments = ["--uitesting", "--reset-state"]
    app.launchEnvironment = [
        "DISABLE_ANIMATIONS": "1",
        "UI_TEST_MODE": "true"
    ]
    app.launch()
}

Управление симуляторами

Очищайте симуляторы между прогонами тестов для предотвращения утечек состояния и проблем с дисковым пространством.

#!/bin/bash
# cleanup-simulators.sh - Run before and after test suites

# Shutdown all running simulators
xcrun simctl shutdown all

# Erase all simulator content and settings
xcrun simctl erase all

# Delete unavailable simulators
xcrun simctl delete unavailable

# Clear DerivedData
rm -rf ~/Library/Developer/Xcode/DerivedData/*

# Clear simulator logs
rm -rf ~/Library/Logs/CoreSimulator/*

echo "Simulator cleanup complete"

Отключение анимаций для скорости

Отключайте анимации в приложении во время UI-тестов для сокращения времени выполнения и уменьшения нестабильности.

// In your AppDelegate or App struct:
#if DEBUG
if CommandLine.arguments.contains("--uitesting") {
    UIView.setAnimationsEnabled(false)
}
#endif

// Also disable Simulator animations via command line:
// Set Simulator > Debug > Slow Animations = OFF
defaults write com.apple.iphonesimulator SlowMotionAnimation -bool NO

Повторный запуск нестабильных тестов

Используйте встроенный механизм повторного запуска тестов Xcode для обработки периодически нестабильных UI-тестов.

# Retry failed tests up to 3 times
xcodebuild test \
  -workspace MyApp.xcworkspace \
  -scheme MyAppUITests \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -retry-tests-on-failure \
  -test-iterations 3 \
  -resultBundlePath ./TestResults.xcresult

Мониторинг дискового пространства

Симуляторы и артефакты тестирования потребляют значительное дисковое пространство. Настройте автоматическую очистку.

# Add to crontab for daily cleanup at midnight
# crontab -e
0 0 * * * /usr/local/bin/cleanup-test-artifacts.sh

# cleanup-test-artifacts.sh
#!/bin/bash
# Remove test results older than 7 days
find ~/TestResults -name "*.xcresult" -mtime +7 -delete

# Remove DerivedData older than 3 days
find ~/Library/Developer/Xcode/DerivedData \
  -maxdepth 1 -mtime +3 -exec rm -rf {} +

# Remove old simulator logs
find ~/Library/Logs/CoreSimulator -mtime +3 -delete

# Check remaining disk space
df -h / | tail -1

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

Сколько параллельных симуляторов выдержит Mac Mini M4 Pro?

С 14 ядрами CPU и 24 ГБ RAM M4 Pro комфортно работает с 4-6 параллельными экземплярами iOS Simulator для UI-тестирования. Для модульных тестов (без GUI Simulator) можно запустить ещё больше параллельных worker. Мы рекомендуем начать с 4 и увеличивать в зависимости от требований к памяти вашего набора тестов.

Можно ли запускать тесты в headless-режиме без VNC?

Да. Тесты XCTest и Appium полностью запускаются из командной строки через SSH. iOS Simulator работает в «headless»-режиме при запуске через xcodebuild без сессии дисплея. VNC не нужен во время выполнения тестов. VNC полезен только для визуальной отладки упавших тестов.

Как справляться с нестабильными UI-тестами?

Во-первых, обеспечьте изоляцию тестов (сбрасывайте состояние приложения перед каждым тестом). Отключите анимации. Используйте явные ожидания вместо sleep(). Используйте флаг -retry-tests-on-failure в Xcode для автоматического повторного запуска упавших тестов. На выделенном сервере нестабильность из-за конкуренции за ресурсы устранена, а это самая частая причина нестабильных тестов на общих CI-машинах.

Можно ли тестировать на реальных iOS-устройствах с удалённого Mac?

Подключить физические iOS-устройства к удалённому серверу невозможно (USB локален). Однако iOS Simulator покрывает подавляющее большинство сценариев UI-тестирования. Для тестирования на конкретных устройствах можно использовать ферму устройств Apple Xcode Cloud или распространять тестовые сборки через TestFlight для ручного тестирования на устройствах.

Как тестировать на разных версиях iOS?

Вы можете установить несколько сред выполнения iOS Simulator на одном Mac. Используйте xcodebuild -downloadPlatform iOS для последней версии или загрузите более старые среды выполнения из Xcode Settings > Platforms. Затем укажите версию ОС в назначении теста: -destination 'platform=iOS Simulator,name=iPhone 16,OS=17.5'.

Запускайте тесты 24/7

Получите выделенный Mac Mini M4 Pro для автоматического тестирования. Параллельное выполнение, стабильные результаты и постоянная доступность.

Связанные руководства