Les mêmes tests XCUITest peuvent réussir systématiquement en local, puis échouer de manière intermittente sur un Mac cloud parce qu’un bouton reste introuvable, qu’une attente expire ou que l’application reste bloquée sur l’écran de démarrage. Le problème ne se résume généralement pas à une « machine lente ». Les causes les plus fréquentes sont une destination d’exécution instable, un état résiduel laissé par le test précédent, des attentes à durée fixe et l’absence de preuves suffisantes après un échec. La bonne méthode consiste à commencer par figer les entrées, puis à éliminer les dépendances implicites, avant seulement d’envisager les nouvelles tentatives.
Figer les entrées de chaque exécution
Spécifiez Xcode, le plan de test, la destination du simulateur et le répertoire de build directement dans la commande, sans dépendre des dernières sélections effectuées dans l’interface graphique. Un Mac cloud peut exécuter des tâches sur une longue durée, et plusieurs pipelines peuvent utiliser le même répertoire de travail. Chaque exécution doit donc disposer de ses propres répertoires DerivedData et de son propre paquet de résultats.
RUN_ID="${CI_RUN_ID:-local-$(date +%s)}"
ARTIFACTS="$PWD/artifacts/$RUN_ID"
DERIVED="$PWD/.derived/$RUN_ID"
mkdir -p "$ARTIFACTS" "$DERIVED"
xcodebuild test \
-workspace Sample.xcworkspace \
-scheme SampleUITests \
-testPlan Smoke \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
-derivedDataPath "$DERIVED" \
-resultBundlePath "$ARTIFACTS/Smoke.xcresult" \
-parallel-testing-enabled NO
Lors de la première phase de stabilisation, désactivez les tests en parallèle afin d’établir une référence en exécution séquentielle. Une fois cette base stable, réactivez progressivement le parallélisme, groupe par groupe. Sinon, les variations simultanées touchant les simulateurs, les données de test et les tâches en arrière-plan rendent difficile l’identification de l’origine du problème : le test lui-même ou l’ordonnancement.
Effectuer un contrôle minimal avant le démarrage
Avant l’exécution, consignez la sortie de xcodebuild -version et vérifiez avec xcrun simctl list devices available que la destination existe réellement. Ne spécifiez pas uniquement le nom de l’appareil en omettant la version du système, et ne laissez pas plusieurs runners partager le même répertoire DerivedData.
Remplacer les attentes fixes par des états observables
Thread.sleep paraît simple, mais revient en réalité à remplacer « la page est prête » par « quelques secondes se sont écoulées ». Lorsque la réponse réseau est rapide, cette attente fait perdre du temps ; lorsqu’une animation ou une opération disque prend un peu plus longtemps, elle se termine trop tôt.
let app = XCUIApplication()
app.launchArguments += ["--ui-testing", "--reset-state"]
app.launch()
let continueButton = app.buttons["onboarding.continue"]
let ready = continueButton.waitForExistence(timeout: 12)
XCTAssertTrue(ready, "Continue button did not appear")
XCTAssertTrue(continueButton.isHittable)
continueButton.tap()
Identifiez les éléments à l’aide d’un accessibility identifier stable plutôt qu’en vous appuyant sur un libellé localisé ou sur la hiérarchie des vues. Si un bouton existe mais n’est pas interactif, examinez les superpositions, les animations et la position de défilement au lieu de simplement augmenter le délai d’attente.
Le délai d’expiration définit une limite d’échec, pas une promesse de performance. Il doit absorber les variations normales tout en provoquant rapidement un échec, avec conservation du contexte, lorsqu’un blocage réel se produit.
Faire démarrer chaque test depuis un état connu
Les échecs intermittents proviennent souvent du test précédent : l’écran d’accueil a déjà été ignoré, des données de test subsistent, une boîte de dialogue ne s’affiche qu’une seule fois ou un processus en arrière-plan n’a pas été arrêté. La méthode la plus fiable consiste à faire exécuter à l’application une logique de réinitialisation dédiée lorsqu’elle reçoit les arguments de lancement des tests, et à générer des données uniques pour chaque identité de test.
Vous pouvez traiter les différentes limites d’état comme suit :
| Type d’état | Pratique recommandée | Pratique déconseillée |
|---|---|---|
| Préférences de l’application | Déclencher une réinitialisation réservée aux tests avec un argument de lancement | Supposer que l’état reste toujours vide après l’installation |
| Base de données locale | Importer des fixtures fixes ou recréer la base de test | Dépendre des données écrites par le test précédent |
| Données côté serveur | Utiliser un espace de noms unique pour l’exécution en cours | Partager des enregistrements fixes entre plusieurs tâches |
| Autorisations système | Les préparer et les vérifier de manière uniforme avant la suite | Les gérer ponctuellement dans un test quelconque |
| Processus de l’application | L’arrêter explicitement à la fin du test | Supposer qu’il récupérera automatiquement après un plantage |
Le nettoyage au niveau de la suite peut arrêter l’application. Ne la désinstallez que lorsqu’il est réellement nécessaire de vérifier le parcours de première installation. Effacer fréquemment l’intégralité du simulateur allonge les exécutions et peut aussi masquer un problème d’environnement derrière un test qui « réussit après nettoyage ».
xcrun simctl terminate booted "$APP_BUNDLE_ID" 2>/dev/null || true
Utiliser les nouvelles tentatives comme outil de classification
Une nouvelle tentative automatique ne doit pas remplacer les critères de réussite. Une stratégie raisonnable consiste à n’effectuer qu’une seule nouvelle tentative après le premier échec, tout en conservant séparément attempt-1.xcresult et attempt-2.xcresult. Si les deux tentatives échouent, traitez en priorité le problème comme un défaut reproductible. Si la première échoue et que la seconde réussit, classez le test comme instable et poursuivez l’analyse des conditions d’attente, de l’état partagé et de la contention des ressources.
Ne pas conserver uniquement le code de sortie final
Pour chaque échec, collectez au minimum les éléments suivants :
- Un paquet de résultats indépendant et les journaux de test ;
- Une capture d’écran et la hiérarchie de l’interface à l’étape de l’échec ;
- La version de Xcode, le simulateur ciblé et les arguments de lancement ;
- L’identifiant de l’exécution, le nom du test ainsi que ses heures de début et de fin ;
- L’état de sortie de l’application et les extraits pertinents des journaux système.
Le chemin du paquet de résultats doit inclure l’identifiant de l’exécution afin d’éviter qu’une tâche concurrente ne l’écrase. Les jetons, identifiants et données métier présents dans les journaux doivent être anonymisés avant leur téléversement.
Passer de la reproduction ciblée à une validation stable
Commencez par exécuter plusieurs fois le test ciblé avec -only-testing afin de réduire les variables sans rapport avec le problème. Exécutez ensuite la classe de tests à laquelle il appartient, puis revenez à la suite complète. Un échec qui ne se produit que dans la suite complète signale généralement une dépendance à l’ordre d’exécution, des données partagées ou une contention des ressources.
xcodebuild test \
-workspace Sample.xcworkspace \
-scheme SampleUITests \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
-only-testing:SampleUITests/CheckoutTests/testSubmit \
-parallel-testing-enabled NO
Lors de la validation, ne vous contentez pas d’une seule exécution réussie. Lancez plusieurs cycles consécutifs, vérifiez qu’aucune dépendance à un ordre fixe ne subsiste et assurez-vous que la durée totale n’augmente pas sensiblement à cause de délais d’attente maximaux trop élevés. Après avoir réactivé les tests en parallèle, vérifiez également que les différents fragments ne modifient ni le même enregistrement de test ni le même répertoire de sortie.
L’objectif final n’est pas de masquer le taux d’échec derrière des nouvelles tentatives, mais de donner à chaque exécution des entrées déterministes, des attentes explicites, un état isolé et des preuves complètes. Lorsque ces quatre conditions sont réunies, XCUITest sur un Mac cloud passe du statut de test qui « réussit parfois » à celui de signal d’ingénierie fiable pour décider d’une fusion ou d’une publication.
Questions fréquentes
Faut-il relancer automatiquement un test XCUITest en échec ?
Une seule relance peut aider à classer l’incident, mais elle ne doit pas masquer le premier échec. Conservez les deux bundles de résultats et suivez séparément les tests qui ne réussissent qu’au second passage.
Pourquoi remplacer les appels sleep dans les tests UI ?
Une durée fixe ne représente pas l’état réel de l’interface. Attendez plutôt une condition observable, comme l’existence ou l’activation d’un élément, avec un délai maximal explicite.
Choisissez un Mac dans le cloud pour votre prochain pipeline de développement
Comparez trois configurations fixes et quatre nœuds en Asie, puis choisissez la durée de location adaptée à votre charge de travail. Chaque location correspond à une machine physique dédiée, et non à une machine virtuelle.