Automatizzare i test UI iOS con XCUITest su un Mac dedicato
Per automatizzare XCUITest, esegui xcodebuild test con una destinazione simulatore e un -resultBundlePath, su un Mac con una sessione utente attiva. Crea un simulatore dedicato con xcrun simctl. Aggiungi -parallel-testing-enabled YES per dividere i test tra cloni del simulatore, e -retry-tests-on-failure per ripetere i test instabili. Abbiamo eseguito ogni comando di questa pagina su un Mac con Xcode 26.6 e il simulatore iOS 26.5.
Cosa ti serve prima
- Un Mac con Xcode e almeno un runtime del simulatore iOS.
- Un target di test UI nel progetto e uno scheme condiviso che lo testa.
- Una sessione desktop con l’utente connesso. I test UI controllano l’app Simulator, quindi ciò che li esegue deve vivere in quella sessione. Significa un agente CI avviato come LaunchAgent, non come LaunchDaemon. Il template plist di Buildkite dice lo stesso: la modalità GUI permette i test UI di Xcode ma richiede un login.
1. Controlla Xcode e i runtime
xcodebuild -version sudo xcodebuild -runFirstLaunch xcrun simctl list runtimes xcrun simctl list devicetypes | grep iPhone
-runFirstLaunch installa i pacchetti e accetta la licenza. Eseguilo dopo ogni installazione o aggiornamento di Xcode.
2. Crea un simulatore solo per la CI
Un dispositivo dedicato tiene la CI separata da tutto ciò che una persona ha aperto a mano. bootstatus -b lo avvia e aspetta che sia pronto.
UDID=$(xcrun simctl create "CI iPhone 17" "iPhone 17" com.apple.CoreSimulator.SimRuntime.iOS-26-5) xcrun simctl bootstatus "$UDID" -b echo "$UDID"
Usa l’UDID nella destinazione: -destination "platform=iOS Simulator,id=$UDID". Funziona anche un nome, come name=iPhone 17,OS=26.5, ma due dispositivi possono avere lo stesso nome.
3. Esegui i test UI con un result bundle
rm -rf build/TestResults.xcresult xcodebuild test \ -project MyApp.xcodeproj \ -scheme MyApp \ -destination "platform=iOS Simulator,id=$UDID" \ -derivedDataPath build/DerivedData \ -resultBundlePath build/TestResults.xcresult
Il result bundle contiene ogni risultato dei test, log, screenshot e errore. Leggi un riepilogo da riga di comando:
xcrun xcresulttool get test-results summary --path build/TestResults.xcresult
Stampa un JSON con il numero di test passati, falliti e saltati per dispositivo. Allega il bundle all’esecuzione della CI come artifact, compresso con ditto -c -k --keepParent.
4. Compila una volta, testa molte volte
Separa la build dall’esecuzione dei test. Così puoi rieseguire un sottoinsieme senza ricompilare.
xcodebuild build-for-testing -project MyApp.xcodeproj -scheme MyApp \ -destination "platform=iOS Simulator,id=$UDID" -derivedDataPath build/DerivedData xcodebuild test-without-building -project MyApp.xcodeproj -scheme MyApp \ -destination "platform=iOS Simulator,id=$UDID" -derivedDataPath build/DerivedData \ -only-testing:MyAppUITests/LoginTests/testSignIn
-only-testing accetta Target, Target/Class o Target/Class/method. -skip-testing funziona allo stesso modo, al contrario.
5. Esegui i test in parallelo
xcodebuild test -project MyApp.xcodeproj -scheme MyApp \ -destination "platform=iOS Simulator,id=$UDID" \ -parallel-testing-enabled YES \ -parallel-testing-worker-count 2 \ -resultBundlePath build/TestResults.xcresult
Xcode clona il simulatore e distribuisce le classi di test tra i cloni. Il nostro log mostrava test in esecuzione su Clone 1 of iPhone 17. -parallel-testing-enabled sovrascrive l’impostazione dello scheme. Su un Mac da 16 GB inizia con 2 worker e misura prima di aumentarli. Ogni clone è un simulatore completo in memoria. Per testare su più tipi di dispositivo insieme, elenca più destinazioni e imposta -maximum-concurrent-test-simulator-destinations.
6. Ripeti i test instabili
xcodebuild test -project MyApp.xcodeproj -scheme MyApp \ -destination "platform=iOS Simulator,id=$UDID" \ -retry-tests-on-failure \ -test-iterations 3 \ -resultBundlePath build/TestResults.xcresult
xcodebuild -help dice che un test fallito viene rieseguito fino al numero di iterazioni. Senza -test-iterations, il massimo è 3. Lo abbiamo verificato con un test che fallisce una volta e poi passa. L’esecuzione è finita con TEST SUCCEEDED. Il riepilogo indicava 3 esecuzioni per 2 test.
Quindi un retry rende verde un test instabile, e l’instabilità resta nascosta. Leggi il result bundle a ogni esecuzione, e tieni traccia dei test che hanno avuto bisogno di un retry. Aggiungi -test-repetition-relaunch-enabled YES per rieseguire ogni tentativo in un processo nuovo. Per scovare un test instabile, usa -run-tests-until-failure. Non si può combinare con -retry-tests-on-failure.
7. Metti dei limiti ai test bloccati
-test-timeouts-enabled YES \ -default-test-execution-time-allowance 120 \ -maximum-test-execution-time-allowance 300
Aggiungi questi flag al comando di test. Un test UI che aspetta per sempre un elemento fallisce dopo il tempo concesso, e non tiene occupata la macchina fino al timeout della CI.
8. Tieni puliti i simulatori tra un’esecuzione e l’altra
xcrun simctl shutdown "$UDID" xcrun simctl erase "$UDID" xcrun simctl delete unavailable
Erase azzera contenuti e impostazioni del simulatore. Delete unavailable rimuove i dispositivi che la versione attuale di Xcode non supporta più. Eseguilo dopo ogni aggiornamento di Xcode.
Come sopravvive ai riavvii
I simulatori che crei restano dopo un riavvio. A dover tornare è l’agente CI che esegue i test. Avvialo come LaunchAgent, attiva il login automatico e avvia il simulatore della CI all’inizio di ogni job con bootstatus -b. Quel comando è sicuro anche su un dispositivo già avviato.
Errori che abbiamo incontrato e come risolverli
Unable to find a device matching the provided destination specifier. Il nome o la versione del sistema non esistono. Controllaxcrun simctl list devicese l’elenco dei runtime.xcodebuild: error: Existing file at -resultBundlePath. Cancella il vecchio bundle prima di ogni esecuzione.Unable to erase contents and settings in current state: Booted. Spegni il simulatore prima di cancellarlo.
Perché un Mac dedicato aiuta
I test UI passano molto tempo prima del primo tap. Aspettano la build, l’avvio di un simulatore e l’installazione dell’app. Una macchina sempre accesa tiene pronti la DerivedData e un simulatore avviato. Nel nostro benchmark abbiamo compilato l’app iOS di Wikipedia con Xcode 26.6. Un job dopo una piccola modifica ha richiesto 27 secondi su un M6 caldo. Ne ha richiesti 269 su un runner macos-26 nuovo ospitato da GitHub. Il nostro M6 ha 12 core CPU, quindi due cloni in parallelo lasciano ancora spazio alla build.
Quando basta la CI in hosting
Una piccola suite eseguita su poche pull request al giorno sta bene con i minuti a consumo. Con la tariffa di GitHub di $0.062 per minuto macOS (verificata a settembre 2026), un Mac a $139 si ripaga dopo circa 2,242 minuti al mese. Sotto quella soglia, resta sull’hosting. Se devi testare su molti iPhone fisici, ti serve un device cloud. Un Mac mini esegue simulatori. La tua suite in parallelo ha bisogno di più di 16 GB di memoria? I nostri tagli M5 Pro hanno 48 o 64 GB, ma sono in preordine, quindi metti in conto circa una settimana di attesa.
La guida alla pipeline CI/CD iOS mostra dove si inseriscono i test UI in una pipeline completa. Il calcolatore fa i conti con i tuoi numeri.
Domande frequenti
xcodebuild ha un flag per ripetere i test falliti?
+
Sì. -retry-tests-on-failure riesegue un test fallito, fino a -test-iterations volte o 3 per impostazione predefinita. Non si può combinare con -run-tests-until-failure.
Come eseguo i test XCUITest in parallelo da riga di comando?
+
Aggiungi -parallel-testing-enabled YES, e se vuoi -parallel-testing-worker-count. Xcode clona il simulatore e divide le classi di test tra i cloni.
XCUITest può girare su un Mac headless?
+
Ha bisogno di una sessione con l’utente connesso, perché i test UI controllano l’app Simulator. Esegui l’agente CI come LaunchAgent e attiva il login automatico.
Come leggo i risultati dei test di xcodebuild senza aprire Xcode?
+
Passa -resultBundlePath, poi esegui xcrun xcresulttool get test-results summary --path sul bundle. Stampa i conteggi in JSON.