Une fois les tests unitaires, les tests d’API et les tests d’interface répartis entre plusieurs plans de test Xcode, le problème le plus fréquent n’est pas l’échec des tests, mais le fait qu’ils ne s’exécutent pas comme prévu. Un développeur peut changer le plan par défaut dans son Scheme local, un autre valider un fichier après avoir ignoré temporairement un cas de test, tandis qu’une variable d’environnement peut être modifiée sans répercuter le changement dans la CI. Un Mac dans le cloud exécute fidèlement la configuration présente dans le dépôt. La première mesure à prendre n’est donc pas d’ajouter des relances, mais de traiter le plan de test lui-même comme du code à contrôler.
Définir un point d’entrée unique et traçable
Le Scheme doit être partagé. Son chemin est généralement App.xcodeproj/xcshareddata/xcschemes/App-CI.xcscheme. Il ne faut pas dépendre du répertoire utilisateur xcuserdata, car sa configuration n’est pas intégrée de façon fiable au contrôle de version.
Dans l’action Test du Scheme, référencez le fichier App-CI.xctestplan présent dans le dépôt, puis transmettez explicitement le nom du plan depuis la CI. Au démarrage du nœud de build, vérifiez d’abord que le plan est visible :
set -euo pipefail
xcodebuild \
-workspace App.xcworkspace \
-scheme App-CI \
-showTestPlans
xcodebuild \
-workspace App.xcworkspace \
-scheme App-CI \
-testPlan App-CI \
-destination 'platform=iOS Simulator,name=iPhone 16' \
test
L’option -testPlan ne doit pas être omise. Sinon, si la valeur par défaut du Scheme change, la commande peut continuer à réussir alors que l’ensemble des tests exécutés n’est plus le même.
Transformer le fichier de plan en contrat vérifiable
Un fichier .xctestplan est un fichier JSON qui se prête bien aux contrôles statiques. Le contrôle bloquant doit couvrir au minimum quatre catégories de champs :
| Élément contrôlé | Règle attendue | Signification d’un échec |
|---|---|---|
| configurations | Doit contenir la configuration CI | Le point d’entrée de la CI a été supprimé ou renommé |
| testTargets | Doit contenir les cibles convenues | Un groupe de tests n’a pas été exécuté |
| skippedTests | Ne peut contenir que des éléments de la liste autorisée | Des tests ont été ignorés sans justification |
| environmentVariableEntries | Les valeurs sensibles sont interdites et les clés essentielles doivent être présentes | L’environnement a dérivé ou des informations sensibles ont été ajoutées au dépôt |
Ne comparez pas directement l’ordre textuel de l’intégralité du JSON. Xcode peut réorganiser des tableaux ou ajouter des champs ; une comparaison caractère par caractère produirait des échecs sans intérêt. Il faut analyser la structure et ne vérifier que les contraintes dont l’équipe dépend réellement.
Le contrôle bloquant du plan de test ne vise pas à empêcher toute modification, mais à rendre visibles avant la fusion les tests qui ne seront plus exécutés et les changements apportés à l’environnement.
Détecter par script les cibles manquantes et les tests ignorés
Le script ci-dessous vérifie la configuration CI, les cibles de test obligatoires et les tests ignorés qui n’ont pas été enregistrés. La liste autorisée doit rester très courte, et chaque revue de code doit préciser les conditions permettant d’en retirer un élément.
#!/usr/bin/env python3
import json
import sys
from pathlib import Path
plan = json.loads(Path("App-CI.xctestplan").read_text())
required_targets = {"AppTests", "AppIntegrationTests"}
allowed_skips = {
"AppIntegrationTests/testTemporaryServerResponse"
}
config_names = {item["name"] for item in plan.get("configurations", [])}
if "CI" not in config_names:
sys.exit("Missing CI test configuration")
targets = plan.get("testTargets", [])
target_names = {
item.get("target", {}).get("name")
for item in targets
}
missing = required_targets - target_names
if missing:
sys.exit(f"Missing test targets: {sorted(missing)}")
actual_skips = {
test
for item in targets
for test in item.get("skippedTests", [])
}
unexpected = actual_skips - allowed_skips
if unexpected:
sys.exit(f"Unapproved skipped tests: {sorted(unexpected)}")
Exécutez ce script avant la commande de test. En cas d’échec, il ne doit pas réécrire automatiquement le fichier du plan, car une correction automatique pourrait masquer une modification de configuration volontaire.
Définir une condition de retrait pour chaque exception autorisée
La liste des exceptions autorisées ne doit pas devenir un dépotoir permanent. Chaque entrée doit au minimum être associée à un ticket, à un responsable et à une condition de suppression. Si l’équipe ne souhaite pas maintenir un second fichier structuré, elle peut rendre ces informations obligatoires dans le modèle de revue de code et exécuter régulièrement un script qui affiche la liste actuelle.
Séparer les variables stables des secrets de la CI
Le plan de test convient aux options qui ne changent pas d’un nœud à l’autre, comme UITEST_MODE=1, une langue fixe ou un mode de service simulé. Les jetons d’accès, clés privées et identifiants à usage unique ne doivent être enregistrés ni dans le fichier du plan ni comme arguments en clair du Scheme.
La CI exécutée sur un Mac dans le cloud peut injecter les secrets avant le lancement, puis laisser le processus de test les lire depuis les variables d’environnement. Le script d’audit vérifie uniquement que les clés sont présentes et que leurs valeurs appartiennent à un ensemble fixe autorisé. Le plan reste ainsi reproductible sans introduire d’informations sensibles dans le dépôt.
Il faut également surveiller la cible utilisée pour l’expansion des variables. Si le plan référence une Target qui a été renommée, le fichier peut encore s’ouvrir dans l’interface de Xcode alors que la résolution des variables à l’exécution ne correspond plus au comportement attendu. Lorsqu’un projet est renommé, le contrôle du plan doit être exécuté avec xcodebuild -list.
Conserver suffisamment d’éléments pour analyser les échecs
Une fois le contrôle bloquant validé, exécutez l’ensemble des tests et écrivez le bundle de résultats dans un répertoire fixe :
rm -rf artifacts/App-CI.xcresult
xcodebuild \
-workspace App.xcworkspace \
-scheme App-CI \
-testPlan App-CI \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-resultBundlePath artifacts/App-CI.xcresult \
test
En cas d’échec, conservez trois éléments : le fichier .xctestplan actuel, la commande réellement exécutée et le fichier .xcresult. La seule fin du journal de console ne permet pas de déterminer si l’échec vient de la logique du test, d’une cible qui n’a pas été chargée ou d’une modification du plan.
Lorsque des nœuds de test permanents sont utilisés sur ZoneMini, l’audit statique doit également être relancé au début de chaque tâche, au lieu de supposer qu’un environnement actif de longue date est nécessairement resté cohérent. Le Scheme partagé définit le point d’entrée, le plan de test décrit l’ensemble à exécuter, le script d’audit encadre les modifications et le bundle de résultats conserve les preuves. Ces quatre couches sont indispensables pour que la mention « tests réussis » garde une signification stable.
Questions fréquentes
Pourquoi une commande xcodebuild fixe ne suffit-elle pas dans la CI ?
Elle fixe le Scheme et le nom du plan, mais pas le contenu du fichier .xctestplan. Les cibles, exclusions et variables peuvent encore changer sans être visibles dans la commande.
Faut-il interdire tous les skippedTests ?
Non. Une exclusion temporaire peut figurer dans une liste avec son motif, son responsable et sa condition de retrait. Toute nouvelle exclusion non déclarée doit bloquer la fusion.
Comment imposer le même plan sur le poste local et le Mac cloud ?
Versionnez le Scheme dans xcshareddata/xcschemes et le fichier .xctestplan, puis fixez -testPlan dans la CI. Vérifiez aussi sa présence avec -showTestPlans avant les tests.
Choisissez un Mac dans le cloud dédié à vos tâches de build continues
Vérifiez le modèle M4, la mémoire, le stockage, le nœud et la période de facturation, puis intégrez cet environnement de build fixe à votre pipeline existant.