Guide - Tests

Tests UI automatises sur serveurs Mac : XCTest, Appium et Selenium

Configurez un serveur Mac dedie pour les tests UI automatises 24h/24. Executez des tests XCTest, XCUITest, Appium et Selenium en parallele sur plusieurs iOS Simulators avec des resultats coherents et reproductibles.

30 min de lecture Mis a jour en janvier 2025

Pourquoi du materiel dedie pour les tests ?

Les tests UI sur des machines partagees ou locales introduisent de l'instabilite, de l'incoherence et des goulots d'etranglement. Un serveur Mac dedie resout ces problemes.

Resultats coherents

Aucun autre processus ne rivalise pour le CPU ou la RAM. Les tests s'executent dans un environnement propre et controle a chaque fois, eliminant les echecs de tests instables causes par la contention de ressources.

Execution parallele

Executez des tests sur plusieurs instances iOS Simulator simultanement. Les 14 coeurs du M4 Pro gerent 4 a 6 instances Simulator en parallele sans degradation de performance.

Disponibilite 24h/24

Les tests s'executent a tout moment -- suites de regression nocturnes, validation post-merge ou a la demande. Le serveur est toujours pret, aucun ordinateur portable de developpeur n'est necessaire.

XCTest sur un Mac dedie

XCTest est le framework de test integre d'Apple, inclus avec Xcode. XCUITest l'etend pour les tests UI. Les deux s'executent nativement et ne necessitent aucune configuration supplementaire au-dela de Xcode.

Executer 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 resultats et captures d'ecran

# 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'ecrire des tests dans n'importe quel langage (Python, JavaScript, Java, etc.) et de les executer 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 telechargement supplementaire de driver de navigateur n'est necessaire -- contrairement a 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();

Execution parallele des tests

L'execution des tests en parallele reduit considerablement le temps total d'execution de votre suite de tests. Le Mac Mini M4 Pro peut confortablement executer 4 a 6 instances Simulator simultanement.

Tests paralleles 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 paralleles

Configuration Duree de 200 tests UI Amelioration 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 : Resultats mesures sur Mac Mini M4 Pro (14 coeurs, 24 Go RAM). Le nombre optimal de Simulators en parallele depend de la complexite de vos tests et des besoins en memoire. Pour la plupart des suites de tests UI, 4 workers en parallele offrent le meilleur equilibre entre vitesse et stabilite.

Integration CI/CD

Integrez vos tests automatises dans votre pipeline CI/CD pour une execution automatique des tests a chaque push, pull request ou intervalle programme.

Workflow GitHub Actions pour les tests automatises

# .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

Executer 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

Generez et traitez les rapports de tests pour suivre les tendances de qualite et identifier rapidement les tests en echec.

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 supporte par les plateformes CI/CD, les integrations 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'ecran en cas d'echec

// 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 etre independant et ne pas dependre de l'etat des tests precedents. Reinitialisez l'etat 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 executions de tests pour eviter les fuites d'etat et les problemes 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"

Desactiver les animations pour la vitesse

Desactivez les animations dans votre application pendant les tests UI pour reduire le temps d'execution et l'instabilite 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 mecanisme de rejeu de tests integre a Xcode pour gerer 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 automatise.

# 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 frequentes

Combien de simulateurs en parallele un Mac Mini M4 Pro peut-il gerer ?

Avec 14 coeurs CPU et 24 Go de RAM, le M4 Pro gere confortablement 4 a 6 instances iOS Simulator en parallele pour les tests UI. Pour les tests unitaires uniquement (sans interface Simulator), vous pouvez executer encore plus de workers en parallele. Nous recommandons de commencer avec 4 et d'augmenter selon les besoins en memoire de votre suite de tests.

Puis-je executer des tests en mode headless sans VNC ?

Oui. Les tests XCTest et Appium s'executent entierement depuis la ligne de commande via SSH. L'iOS Simulator fonctionne en mode "headless" lorsqu'il est lance via xcodebuild sans session d'affichage. Vous n'avez pas besoin de VNC connecte pendant l'execution des tests. VNC est utile uniquement pour deboguer visuellement les tests en echec.

Comment gerer les tests UI instables ?

Premierement, assurez l'isolation des tests (reinitialisez l'etat de l'application avant chaque test). Desactivez les animations. Utilisez des attentes explicites au lieu de sleep(). Utilisez le flag -retry-tests-on-failure de Xcode pour rejouer automatiquement les tests echoues. Sur un serveur dedie, l'instabilite liee a la contention de ressources est eliminee, ce qui est la cause la plus frequente de tests instables sur des machines CI partagees.

Puis-je tester sur de vrais appareils iOS depuis un Mac distant ?

Vous ne pouvez pas connecter d'appareils iOS physiques a un serveur distant (l'USB est local). Cependant, l'iOS Simulator couvre la grande majorite des scenarios de tests UI. Pour les tests specifiques aux appareils, vous pouvez utiliser la ferme d'appareils "Xcode Cloud" d'Apple ou distribuer des builds de test via TestFlight pour les tests manuels sur appareils.

Qu'en est-il des tests sur differentes versions d'iOS ?

Vous pouvez installer plusieurs runtimes iOS Simulator sur le meme Mac. Utilisez xcodebuild -downloadPlatform iOS pour la derniere version, ou telechargez les versions anterieures depuis Reglages Xcode > Plateformes. Ensuite, specifiez la version de l'OS dans votre destination de test : -destination 'platform=iOS Simulator,name=iPhone 16,OS=17.5'.

Executez votre suite de tests 24h/24

Obtenez un Mac Mini M4 Pro dedie pour les tests automatises. Execution parallele, resultats coherents et disponibilite permanente.

Guides associes