Créer un seuil de régression de couverture iOS avec xccov sur un Mac cloud

Créer un seuil de régression de couverture iOS avec xccov sur un Mac cloud

Après avoir migré une série de tests unitaires vers un Mac cloud, une équipe risque facilement de ne regarder qu’une seule chose : les tests sont-ils passés ? Pourtant, un commit peut supprimer les assertions d’une branche critique tout en laissant tous les tests au vert, alors que la couverture diminue silencieusement. Une approche plus fiable consiste à générer un xcresult distinct pour chaque build, à en extraire les données avec xccov, fourni avec Xcode, puis à contrôler à la fois la couverture globale du projet et celle des fichiers modifiés.

Figer les conditions d’entrée de la couverture

La couverture ne mérite d’être comparée que si les conditions d’exécution des tests restent stables. Commencez par figer la version de Xcode, le scheme, le plan de test, le modèle de simulateur et la version du système. Ne laissez pas chaque runner choisir lui-même sa destination. La commande de test doit également activer explicitement la couverture et créer un nouveau répertoire de résultats à chaque exécution.

set -euo pipefail

RESULT_DIR="$PWD/Artifacts/Coverage"
RESULT_BUNDLE="$RESULT_DIR/TestResults.xcresult"

rm -rf "$RESULT_BUNDLE"
mkdir -p "$RESULT_DIR"

xcodebuild test \
  -workspace ExampleApp.xcworkspace \
  -scheme ExampleApp-CI \
  -testPlan UnitTests \
  -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
  -enableCodeCoverage YES \
  -resultBundlePath "$RESULT_BUNDLE"

Le chemin indiqué par resultBundlePath ne doit pas exister avant l’exécution, faute de quoi d’anciens résultats peuvent empêcher l’écriture. Le script de CI doit aussi enregistrer xcodebuild -version, l’identifiant du commit courant et la destination complète, afin qu’un changement de chaîne d’outils ne soit pas interprété comme une régression du code.

Un seuil de couverture mesure l’évolution obtenue dans des conditions de test identiques ; il ne sert pas à classer des simulateurs ou des plans de test différents.

Extraire de xcresult des données auditables

Une fois les tests terminés, exportez les données au format JSON avec xccov au lieu d’analyser le texte mis en forme dans le terminal. Le JSON conserve plus fiablement les chemins de fichiers, les noms de cibles et la couverture des lignes, sans risque de décalage des colonnes lorsque la largeur d’affichage change.

xcrun xccov view \
  --report \
  --json \
  "$RESULT_BUNDLE" > "$RESULT_DIR/coverage.json"

test -s "$RESULT_DIR/coverage.json"

Le parseur doit d’abord vérifier que targets existe, puis regrouper les fichiers par cible. Si le rapport est vide, ne le traitez pas comme une couverture de 0% à comparer aux seuils : signalez directement un échec de collecte. Les causes fréquentes incluent des tests non activés dans le scheme, une cible exclue du calcul de couverture ou l’arrêt anormal du processus de test avant l’écriture du rapport.

Conserver les éléments de preuve bruts

Il est recommandé de conserver les éléments suivants comme artefacts d’une même tâche :

Artefact Utilité
TestResults.xcresult Vérifier les tests, les journaux et l’origine des données de couverture
coverage.json Fournir un format stable aux scripts d’analyse
coverage-summary.json Enregistrer les seuils, les valeurs réelles et les fichiers en échec
command.txt Reconstituer les paramètres d’exécution et la chaîne d’outils

Une capture d’écran affichant uniquement des pourcentages ne permet pas de savoir quels tests ont réellement été exécutés. Le bundle de résultats brut reste la référence pour le diagnostic.

Définir deux niveaux de seuil

La couverture globale du projet permet de détecter les fortes baisses. Dans une grande base de code, toutefois, l’ajout de quelques dizaines de lignes non testées peut ne faire varier le total que de quelques dixièmes de point. Il est donc recommandé d’appliquer deux règles :

  1. La couverture globale du projet ne doit pas descendre sous un minimum fixe.
  2. Les fichiers source exécutables concernés par les modifications doivent respecter un seuil plus élevé.

Par exemple, le seuil global peut être fixé à 72%, contre 85% pour les fichiers modifiés. Ces valeurs doivent partir du niveau de référence actuel de l’équipe, et non d’un objectif idéal impossible à appliquer immédiatement. L’extrait Python suivant montre comment lire la couverture de la cible ; dans un projet réel, files peut ensuite être développé afin d’évaluer chaque fichier séparément.

import json
import sys

with open("Artifacts/Coverage/coverage.json", encoding="utf-8") as f:
    report = json.load(f)

targets = report.get("targets", [])
if not targets:
    raise SystemExit("Coverage report has no targets")

tested = [t for t in targets if t.get("name") == "ExampleApp.app"]
if len(tested) != 1:
    raise SystemExit("Expected application target was not found")

coverage = float(tested[0]["lineCoverage"]) * 100
minimum = 72.0

print(f"application_line_coverage={coverage:.2f}")
sys.exit(0 if coverage >= minimum else 2)

Le script doit utiliser des codes de sortie différents pour distinguer une couverture inférieure au seuil d’un rapport impossible à analyser. Le premier cas exige d’ajouter des tests ou d’expliquer la modification ; le second impose de réparer la chaîne de collecte. Ces deux situations ne doivent pas être regroupées sous un même type d’échec.

Ne contrôler que les fichiers qui doivent réellement être testés

La liste des fichiers modifiés peut être obtenue avec git diff --name-only, puis croisée avec les chemins présents dans le rapport xccov. N’intégrez pas automatiquement tous les fichiers .swift au contrôle. Les éléments suivants doivent généralement être exclus explicitement :

Les règles d’exclusion doivent être versionnées dans le dépôt et soumises à la revue de code. Il ne faut pas ajouter un motif générique à la volée après un échec. Avant de comparer les chemins, normalisez-les également par rapport à la racine du dépôt, résolvez les liens symboliques et retirez les préfixes des répertoires temporaires de build. Sans cela, un même fichier peut ne pas correspondre simplement parce que ses chemins absolus diffèrent.

Pour les fichiers renommés, git diff --name-status -M est plus fiable qu’une simple liste. Lorsqu’un fichier a été déplacé de son ancien chemin vers un nouveau, recherchez l’entrée du rapport à partir du nouveau chemin au lieu de conclure à tort que le fichier n’existe pas dans le rapport.

Maîtriser les tests parallèles et les variations

Les variations occasionnelles de couverture ne proviennent généralement pas d’un comportement aléatoire de xccov, mais d’une exécution non déterministe des tests. Les tests asynchrones doivent attendre un état explicite au lieu de dormir pendant un nombre fixe de secondes. Les bases de données partagées, singletons et répertoires temporaires doivent être réinitialisés avant chaque test. Si des tests parallèles se disputent une même ressource, désactivez d’abord le parallélisme pour les cibles de test concernées, puis identifiez précisément le point partagé.

Le cycle de vie du simulateur doit lui aussi être traçable. Un runner persistant peut nettoyer les données de l’application avant chaque tâche, sans qu’il soit nécessaire de supprimer tous les runtimes à chaque exécution. L’objectif est de vérifier qu’un même plan de test produit des résultats cohérents de façon répétée avec une chaîne d’outils identique. Commencez par exécuter trois fois le même commit et consignez l’écart observé pour chaque fichier. Pour les fichiers dont les résultats continuent de fluctuer, corrigez d’abord l’isolation des tests avant de les soumettre à un seuil strict.

Enfin, rédigez le résumé de l’échec de manière directement exploitable : couverture globale réelle, valeur exigée, fichiers modifiés sous le seuil, nombre de lignes couvertes et exécutables dans chacun de ces fichiers, ainsi que l’emplacement d’archivage du xcresult brut. Le développeur pourra ainsi déterminer s’il doit ajouter des tests, corriger le script ou traiter l’instabilité de l’environnement de test, sans avoir à relancer toute la tâche.

Questions fréquentes

Faut-il contrôler uniquement la couverture globale du projet ?

Non. La valeur globale détecte une forte régression, mais réagit peu à une petite modification. Contrôlez aussi les fichiers modifiés et définissez explicitement les exclusions pour le code généré et les utilitaires de test.

Pourquoi la couverture varie-t-elle parfois pour un même commit ?

Les causes fréquentes sont une sélection de tests différente, des tâches asynchrones inachevées, un état partagé en parallèle, des données résiduelles du simulateur ou une version différente de Xcode. Fixez d’abord tous ces paramètres.

Quels fichiers conserver quand le contrôle échoue ?

Conservez le xcresult original, coverage.json, le résumé des seuils, la commande exacte et l’identifiant du commit. Ils permettent de distinguer un test absent, une erreur d’analyse et une baisse réelle.

Nœud physique dédié

Reproduisez vos workflows sur un Mac cloud que vous activez et arrêtez selon vos projets

Choisissez la ressource adaptée parmi trois configurations Apple Silicon et louez-la à la journée, à la semaine, au mois ou au trimestre. Chaque appareil est une machine physique dédiée, et non une machine virtuelle ; sa disponibilité réelle est indiquée en temps réel dans la console.

Choisir une configuration et commander