Pourquoi du matériel dédié pour les tests ?
Les tests UI sur des machines partagées ou locales introduisent de l'instabilité, de l'incohérence et des goulots d'étranglement. Un serveur Mac dédié résout ces problèmes.
Résultats cohérents
Aucun autre processus ne rivalise pour le CPU ou la RAM. Les tests s'exécutent dans un environnement propre et contrôlé à chaque fois, éliminant les échecs de tests instables causés par la contention de ressources.
Exécution parallèle
Exécutez des tests sur plusieurs instances iOS Simulator simultanément. Les 14 cœurs du M4 Pro gèrent 4 à 6 instances Simulator en parallèle sans dégradation de performance.
Disponibilité 24h/24
Les tests s'exécutent à tout moment -- suites de régression nocturnes, validation post-merge ou à la demande. Le serveur est toujours prêt, aucun ordinateur portable de développeur n'est nécessaire.
XCTest sur un Mac dédié
XCTest est le framework de test intégré d'Apple, inclus avec Xcode. XCUITest l'étend pour les tests UI. Les deux s'exécutent nativement et ne nécessitent aucune configuration supplémentaire au-delà de Xcode.
Exécuter XCTest en ligne de commande
# 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
Tests sur plusieurs destinations
# 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
Bundles de résultats et captures d'écran
# 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
Configuration d'Appium pour iOS
Appium est un framework d'automatisation open source qui vous permet d'écrire des tests dans n'importe quel langage (Python, JavaScript, Java, etc.) et de les exécuter sur des applications iOS sur simulateurs ou appareils. Il utilise le driver XCUITest d'Apple en interne.
Installer Appium sur votre serveur 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
Configurer les 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
}
Exemple de test 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()
Exemple de test 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 pour Safari
macOS inclut Safari et safaridriver nativement. Aucun téléchargement supplémentaire de driver de navigateur n'est nécessaire -- contrairement à Chrome ou Firefox sur d'autres plateformes.
Activer 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
Test 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()
Test 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();
Exécution parallèle des tests
L'exécution des tests en parallèle réduit considérablement le temps total d'exécution de votre suite de tests. Le Mac Mini M4 Pro peut confortablement exécuter 4 à 6 instances Simulator simultanément.
Tests parallèles 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
Gestion des instances 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"
Performance des tests parallèles
| Configuration | Durée de 200 tests UI | Amélioration de la vitesse |
|---|---|---|
| 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 |
Note : Résultats mesurés sur Mac Mini M4 Pro (14 cœurs, 24 Go RAM). Le nombre optimal de Simulators en parallèle dépend de la complexité de vos tests et des besoins en mémoire. Pour la plupart des suites de tests UI, 4 workers en parallèle offrent le meilleur équilibre entre vitesse et stabilité.
Intégration CI/CD
Intégrez vos tests automatisés dans votre pipeline CI/CD pour une exécution automatique des tests à chaque push, pull request ou intervalle programmé.
Workflow GitHub Actions pour les tests automatisés
# .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
Exécuter les tests Appium en 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
Rapports de tests
Générez et traitez les rapports de tests pour suivre les tendances de qualité et identifier rapidement les tests en échec.
Travailler avec les bundles 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
Convertir en JUnit XML
JUnit XML est le format universel supporté par les plateformes CI/CD, les intégrations Slack et les tableaux de bord de tests.
# 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
Capture d'écran en cas d'échec
// 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()
}
}
Bonnes pratiques
Isolation des tests
Chaque test doit être indépendant et ne pas dépendre de l'état des tests précédents. Réinitialisez l'état de l'application avant chaque test.
// 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()
}
Gestion des Simulators
Nettoyez les simulateurs entre les exécutions de tests pour éviter les fuites d'état et les problèmes d'espace disque.
#!/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"
Désactiver les animations pour la vitesse
Désactivez les animations dans votre application pendant les tests UI pour réduire le temps d'exécution et l'instabilité des tests.
// 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
Rejouer les tests instables
Utilisez le mécanisme de rejeu de tests intégré à Xcode pour gérer les tests UI occasionnellement instables.
# 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
Surveiller l'espace disque
Les simulateurs et les artefacts de tests consomment un espace disque significatif. Mettez en place un nettoyage automatisé.
# 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
Questions fréquentes
Combien de simulateurs en parallèle un Mac Mini M4 Pro peut-il gérer ?
Avec 14 cœurs CPU et 24 Go de RAM, le M4 Pro gère confortablement 4 à 6 instances iOS Simulator en parallèle pour les tests UI. Pour les tests unitaires uniquement (sans interface Simulator), vous pouvez exécuter encore plus de workers en parallèle. Nous recommandons de commencer avec 4 et d'augmenter selon les besoins en mémoire de votre suite de tests.
Puis-je exécuter des tests en mode headless sans VNC ?
Oui. Les tests XCTest et Appium s'exécutent entièrement depuis la ligne de commande via SSH. L'iOS Simulator fonctionne en mode "headless" lorsqu'il est lancé via xcodebuild sans session d'affichage. Vous n'avez pas besoin de VNC connecté pendant l'exécution des tests. VNC est utile uniquement pour déboguer visuellement les tests en échec.
Comment gérer les tests UI instables ?
Premièrement, assurez l'isolation des tests (réinitialisez l'état de l'application avant chaque test). Désactivez les animations. Utilisez des attentes explicites au lieu de sleep(). Utilisez le flag -retry-tests-on-failure de Xcode pour rejouer automatiquement les tests échoués. Sur un serveur dédié, l'instabilité liée à la contention de ressources est éliminée, ce qui est la cause la plus fréquente de tests instables sur des machines CI partagées.
Puis-je tester sur de vrais appareils iOS depuis un Mac distant ?
Oui. Redirigez un iPhone ou un iPad physique de votre ordinateur vers le Mac avec VirtualHere : il apparaît comme un appareil connecté pour Xcode, xcodebuild et Appium, et vous pouvez y exécuter vos tests. Pour installer des builds destinés aux tests manuels, utilisez TestFlight. Pour de grandes matrices d'appareils, exécutez vos suites XCUITest ou Appium sur un cloud d'appareils réels comme BrowserStack, AWS Device Farm ou Sauce Labs. À noter : Xcode Cloud d'Apple n'exécute les tests que sur des simulateurs.
Qu'en est-il des tests sur différentes versions d'iOS ?
Vous pouvez installer plusieurs runtimes iOS Simulator sur le même Mac. Utilisez xcodebuild -downloadPlatform iOS pour la dernière version, ou téléchargez les versions antérieures depuis Réglages Xcode > Plateformes. Ensuite, spécifiez la version de l'OS dans votre destination de test : -destination 'platform=iOS Simulator,name=iPhone 16,OS=17.5'.