Зачем выделенное оборудование для тестирования?
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?
Да. Пробросьте физический iPhone или iPad со своего компьютера на Mac с помощью VirtualHere, и он появится как подключённое устройство в Xcode, xcodebuild и Appium, так что вы сможете запускать на нём тесты. Чтобы установить сборки для ручного тестирования, используйте TestFlight. Для больших матриц устройств запускайте наборы тестов XCUITest или Appium в облаке реальных устройств, например BrowserStack, AWS Device Farm или Sauce Labs. Обратите внимание: Xcode Cloud от Apple запускает тесты только на симуляторах.
Как тестировать на разных версиях iOS?
Вы можете установить несколько сред выполнения iOS Simulator на одном Mac. Используйте xcodebuild -downloadPlatform iOS для последней версии или загрузите более старые среды выполнения из Xcode Settings > Platforms. Затем укажите версию ОС в назначении теста: -destination 'platform=iOS Simulator,name=iPhone 16,OS=17.5'.