Les tests parallèles Swift Testing instables ne doivent pas être corrigés en désactivant toute la parallélisation : commencez par isoler les états partagés, les fichiers, les ports, le trousseau et les simulateurs, puis appliquez .serialized seulement à la portée qui reste dangereuse. Cette méthode convient lorsque les tests passent localement et isolément, mais échouent de façon aléatoire dans une CI sur Mac distant.
Qui devrait lire cet article ?
Vous migrez des tests XCTest vers Swift Testing et constatez que l’ordre d’exécution révèle des échecs intermittents.
Vous administrez une chaîne CI, la collecte des résultats ou l’isolement des ressources d’un Mac distant.
Vous devez choisir entre refactoriser les tests, sérialiser une suite ou ajouter un nœud indépendant.
Dernière mise à jour : 6 septembre 2026. Les comportements décrits ont été vérifiés à partir de la documentation Apple sur Swift Testing, la parallélisation, les résultats Xcode, la migration XCTest et l’implémentation officielle de ParallelizationTrait.
Le premier diagnostic appartient à toute l’équipe
Un test qui échoue uniquement dans la CI peut donner une fausse impression de problème de framework. Swift Testing exécute par défaut les tests en parallèle, et Apple décrit ce modèle dans sa documentation officielle sur la parallélisation de Swift Testing. Cela signifie surtout que la concurrence expose les hypothèses cachées de votre code.
Avant de modifier une suite, fixez quatre éléments :
- le commit testé ;
- la version de Xcode et du SDK ;
- le Scheme, le Test Plan et le point d’entrée CI ;
- l’identité du nœud et son état au moment de l’échec.
Conservez le résultat de test produit par Xcode, le nom du test, l’ordre observé, les journaux de la tâche et les processus actifs. La documentation Apple sur l’exécution des tests et l’interprétation des résultats explique quelles informations le résultat de test peut fournir et comment les exploiter dans le diagnostic.
Votre première séparation doit distinguer quatre concurrences souvent confondues :
- la parallélisation interne de Swift Testing ;
- la parallélisation propre à XCTest ;
- plusieurs tâches CI exécutées simultanément ;
- plusieurs Runner ou projets partageant le même Mac distant.
Une suite peut donc passer avec .serialized tout en échouant parce qu’une autre tâche utilise le même dossier, le même port ou le même trousseau. Inversement, un problème peut venir exclusivement de l’état global d’un test, sans rapport avec le nombre de tâches CI.
Pour établir une base reproductible, lancez le même point d’entrée dans trois conditions : exécution parallèle, exécution du test fautif seul et exécution de la portée avec sérialisation locale. Ne changez pas simultanément le code, le cache et le nœud. Sinon, vous ne saurez pas quelle modification a supprimé le signal.
02Tests auteurs : supprimer l’état partagé avant de déplacer le problème
Le rôle de l’auteur de test est de rendre chaque test indépendant. Cherchez d’abord les variables globales, les singletons, les propriétés static, les caches persistants et les objets réutilisés entre méthodes. Un test qui modifie une configuration sans la restaurer laisse un environnement différent au test suivant.
Les remplacements de setUp et tearDown pendant une migration méritent une vérification ciblée. Le fonctionnement de Swift Testing ne doit pas être supposé identique à celui d’XCTest : contrôlez la création des fixtures, le nettoyage et la durée de vie des tâches asynchrones dans la version de votre outil. La documentation Apple de migration d’XCTest vers Swift Testing fournit le cadre officiel pour faire coexister les deux approches.
Pour chaque fixture, posez quatre questions :
- Est-elle créée par le test ou récupérée depuis un singleton ?
- Peut-elle être modifiée par une autre instance simultanément ?
- Le nettoyage est-il terminé avant la fin du test ?
- Un échec intermédiaire laisse-t-il un état persistant ?
Une tâche asynchrone lancée mais non attendue est un cas classique. Le test paraît terminé, puis la tâche écrit dans une base, publie une notification ou modifie un cache pendant le test suivant. La suite semble alors dépendre de l’ordre, alors que la cause réelle est une fin de vie mal définie.
Après la refactorisation, utilisez un ordre aléatoire lorsque votre outil le permet, répétez la portée concernée et exécutez séparément les anciens tests fautifs. Si l’échec disparaît uniquement lorsqu’une suite est sérialisée, gardez la preuve : vous avez établi une corrélation, pas encore identifié la ressource responsable.
Les tests paramétrés nécessitent la même prudence. Une donnée de paramètre ne doit pas réutiliser implicitement une ressource globale créée par une autre itération. Consultez la documentation Apple sur les tests paramétrés avant de déduire qu’un ordre d’itération explique à lui seul l’échec.
03Première étape : cartographier les ressources de l’application
L’ingénieur applicatif doit ensuite examiner ce qui dépasse la mémoire propre du processus de test. Les collisions les plus coûteuses ne sont pas toujours visibles dans le code de l’assertion.
Fichiers et dossiers
Un chemin temporaire fixe, un fichier de base de données commun ou un dossier de sortie partagé permet à deux tests de se supprimer ou de se remplacer mutuellement. Générez un espace de travail par test ou par instance de fixture. Le nom doit être suffisamment spécifique pour éviter une collision avec une autre tâche CI exécutée sur le même hôte.
Séparez aussi DerivedData, les artefacts de compilation et les répertoires temporaires entre tâches. La propreté de l’espace de travail ne remplace pas l’isolation applicative, mais elle permet de distinguer un résidu de build d’un défaut reproductible.
Ports, notifications et persistance système
Un serveur local lancé sur un port fixe devient une ressource globale. Demandez un port disponible par test, ou rendez le port injectable. La même discipline s’applique aux écouteurs de notification : chaque test doit supprimer ses abonnements, y compris lorsqu’une assertion échoue.
UserDefaults et Keychain demandent une attention particulière. Supprimer toutes les données du trousseau avant chaque tâche peut masquer une dépendance et perturber d’autres processus. Préférez un espace de noms dédié, une identité de test distincte ou une restauration contrôlée. Si une suppression globale est nécessaire pour une expérience de diagnostic, documentez son impact et prévoyez une procédure de retour.
Attention :
.serializedprotège une portée de tests contre certaines exécutions concurrentes ; il ne crée pas automatiquement un nouveau dossier, ne nettoie pas Keychain et ne sépare pas deux Runner partageant le même Mac.
Simulateur et interface utilisateur
Un test d’interface, un processus de simulateur et un test Swift Testing exécuté dans le processus principal ne sont pas une seule forme de concurrence. Un simulateur partagé peut conserver des données, une session ou une application lancée. Une sérialisation dans une suite ne garantit donc pas l’exclusivité d’un simulateur utilisé par une autre tâche CI.
Réservez les tests d’interface à un appareil ou un simulateur identifié. Exécutez les tests de logique séparément lorsque leur parallélisation est sûre. Cette séparation donne un signal plus utile qu’un interrupteur global qui transforme toute la suite en file d’attente.
04Migration XCTest : rendre les frontières visibles
Le responsable de migration doit établir une carte des cibles. Notez quels tests sont encore exécutés par XCTest, lesquels utilisent Swift Testing, et quelles cibles sont appelées par chaque Scheme. Apple confirme dans la présentation officielle de Swift Testing que Swift Testing et XCTest peuvent coexister, mais cette coexistence ne signifie pas que toutes les règles d’exécution sont identiques.
Vérifiez ensuite le Test Plan et les paramètres de la commande CI. Une exécution locale avec un Scheme peut activer une configuration différente de celle du Runner. Comparez les diagnostics, les destinations, les options de parallélisation et les répertoires de sortie. Les recommandations Apple sur l’organisation des tests dans un projet Xcode et sur les Test Plans constituent la référence pour cette vérification.
Le trait .serialized doit être choisi selon la frontière de partage :
- sur une suite, si plusieurs tests utilisent la même ressource non isolable ;
- sur une portée paramétrée, si les variantes se disputent une ressource précise ;
- jamais comme décoration automatique de tous les tests migrés.
La documentation Apple décrit la sémantique de ParallelizationTrait et de .serialized. L’implémentation officielle de ParallelizationTrait permet également de vérifier le comportement exposé par le paquet Swift Testing. Utilisez ces sources pour confirmer la syntaxe et la portée, mais ne déduisez pas qu’une sérialisation corrigera une collision externe au processus.
FAQ : interpréter les échecs intermittents sans fausse conclusion
Pourquoi un test Swift Testing réussit-il en local mais échoue-t-il dans la CI ?
La CI ajoute souvent une concurrence absente de votre poste : plusieurs tests, tâches, simulateurs ou Runner peuvent partager des ressources. Commencez par comparer le même commit avec un espace de travail propre, puis exécutez le test seul. Si l’échec persiste, examinez l’environnement du nœud ; s’il disparaît, recherchez un état partagé ou une collision de ressource avant d’accuser le framework.
Comment désactiver seulement une partie des tests parallèles ?
Ajoutez .serialized à la suite ou à la portée qui utilise une ressource non isolable, conformément à la documentation de votre version de Swift Testing. Ne désactivez pas toute la parallélisation du projet par réflexe. Notez la cause, la date de la mesure, les tests concernés et la condition de retrait. Une sérialisation sans échéance devient rapidement une dette de pipeline.
Faut-il placer .serialized sur le test ou sur la suite ?
Placez-le au niveau le plus étroit qui protège réellement la ressource. Si plusieurs tests partagent un même état, la suite est généralement la frontière lisible. Si seul un groupe de paramètres entre en collision, limitez la portée à cette exécution paramétrée. Une portée trop large ralentit des tests indépendants ; une portée trop étroite laisse la collision intacte.
XCTest et Swift Testing peuvent-ils perturber la parallélisation ?
Oui, mais il faut préciser le niveau concerné. Le mélange des frameworks peut faire croire que tous les tests suivent la même planification, alors que les cibles, les Schemes et les Test Plans peuvent les organiser différemment. Inspectez le résultat produit, les commandes CI et les destinations. Séparez aussi la concurrence entre jobs de celle qui existe à l’intérieur d’une suite.
Comment reproduire la concurrence sur un Mac distant ?
Utilisez un nœud dédié ou au minimum un espace de travail isolé, puis lancez plusieurs répétitions avec le même commit et le même point d’entrée. Archivez chaque résultat, l’ordre des tests, les journaux système utiles, les processus et l’espace disponible. Comparez ensuite trois modes : parallèle, test fautif seul et portée locale sérialisée. Cette matrice évite de confondre cause et simple corrélation.
06CI et Mac distant : vérifier l’hygiène du nœud
L’ingénieur CI doit prouver que le nœud ne fournit pas un environnement contaminé. Vérifiez les tâches simultanées, les processus laissés par une exécution précédente, l’espace disque, la mémoire disponible, les simulateurs actifs et les dossiers de sortie. N’effacez pas indistinctement tous les caches avant d’avoir capturé leur état : vous risqueriez de supprimer l’indice qui explique l’échec.
Un test de contrôle doit utiliser :
- un clone propre du dépôt ;
- un
DerivedDatapropre et propre à la tâche ; - un répertoire temporaire propre ;
- un résultat de test archivé ;
- un journal identifiant le nœud et l’exécution ;
- une séparation explicite des ports et des identités de test.
Cette expérience ne mesure pas une performance. Elle répond à une question plus importante : le défaut suit-il le code, la concurrence ou le nœud ?
Pour une chaîne qui doit conserver les résultats d’une exécution à l’autre, définissez aussi une politique d’archivage. Les formats de résultats Xcode doivent être associés au commit, au Scheme et à l’identité du Runner. Une simple capture de la dernière sortie console ne permet pas de relier un échec intermittent à l’ordre d’exécution ou à l’état des ressources.
07Plateforme : choisir entre refactorisation, sérialisation et nœud dédié
Le responsable de plateforme doit faire dépendre la décision de preuves concrètes.
| Signal observé | Action prioritaire | Périmètre | Condition de sortie |
|---|---|---|---|
| État global ou fixture réutilisée | Refactoriser la fixture | Tests concernés | Chaque test crée et nettoie son état |
| Fichier, port ou persistance partagée | Isoler la ressource | Suite ou job concerné | Noms et identités séparés |
| Ressource externe impossible à rendre concurrente immédiatement | Ajouter .serialized |
Suite minimale | Cause documentée et date de réexamen |
| Plusieurs jobs contaminent le même espace | Isoler le workspace ou le nœud | Tâche CI ou Runner | Aucun partage implicite |
| Échec seulement sur un hôte chargé | Tester un nœud dédié | Flux concerné | Échec reproductible ou écart environnemental expliqué |
Ne considérez pas la désactivation globale de la parallélisation comme une solution finale. Elle peut servir de retour temporaire pour débloquer une livraison, mais vous devez conserver une porte de réévaluation. La vision de conception officielle de Swift Testing présente la parallélisation comme un élément central du modèle, tandis que .serialized répond à des besoins ciblés.
Un nœud indépendant est pertinent lorsque le problème vient d’un partage entre pipelines, d’un simulateur persistant ou d’un environnement difficile à nettoyer. Il ne dispense pas de corriger une fixture qui modifie un singleton. Pour une chaîne macOS durable, comparez également la capacité d’un nœud réservé dans votre offre de Mac mini distant avec une machine locale ou un poste partagé.
08Matrice de décision avant la remise en parallèle
Utilisez cette matrice avant de retirer une sérialisation. Elle empêche de déclarer la suite « réparée » après une seule exécution réussie.
| Vérification | Refactorisation | .serialized local |
Nœud isolé |
|---|---|---|---|
| État global supprimé | Obligatoire | Non | Non |
| Fichiers et ports séparés | Obligatoire | Non garanti | À configurer |
| Compatibilité avec plusieurs jobs | Élevée | Faible à moyenne | Élevée si le nœud est dédié |
| Retour rapide à la CI | Moyenne | Élevée | Moyenne |
| Dette technique créée | Faible | Moyenne | Variable |
| Réexécution après redémarrage | Obligatoire | Obligatoire | Obligatoire |
| Retrait possible de la mesure | Oui | Oui, avec preuve | Oui, après comparaison |
La validation doit couvrir un environnement propre, plusieurs répétitions, un redémarrage du nœud et une nouvelle exécution avec le même commit. Le résultat doit permettre de retrouver le test, la phase, le nœud, le job et l’état de la ressource. Si vous ne pouvez pas répondre à ces questions, la cause n’est pas suffisamment établie.
Un Mac distant exploité comme nœud de test doit aussi être traité comme une infrastructure : espace de travail par tâche, accès SSH contrôlé, journaux conservés et séparation des utilisateurs ou des jobs. Vous pouvez examiner les différentes options de commande d’un Mac mini distant lorsque la reproduction exige une machine séparée plutôt qu’un poste de développement partagé.
09La stratégie recommandée pour cette semaine
Le premier jour, capturez un échec sans nettoyer l’environnement à l’aveugle. Le jour suivant, répétez le même commit en parallèle, seul et avec une portée localement sérialisée. Ensuite, attribuez chaque ressource à un propriétaire : auteur de test pour l’état interne, ingénieur applicatif pour fichiers et ports, ingénieur CI pour le workspace, responsable de plateforme pour le nœud.
Si la preuve désigne une fixture, refactorisez-la. Si elle désigne une ressource externe momentanément impossible à isoler, utilisez .serialized sur la plus petite portée. Si elle désigne le partage du Runner, changez l’architecture du nœud. Dans tous les cas, conservez une condition explicite de retour à la parallélisation.
Lorsque votre ordinateur actuel ne permet pas de reproduire le problème, un Mac distant indépendant peut fournir un environnement jetable pour comparer les trois modes sans bloquer votre poste principal. Cette approche évite les limites d’un ordinateur partagé : état résiduel, ports occupés et ressources disputées. Elle évite aussi de transformer une simple location en solution permanente lorsque vos tests exigent au contraire une charge stable, un accès physique ou une capacité prévisible. Pour une expérimentation de courte durée ou une validation de pipeline, consultez les possibilités de Mac distant de VpsMesh et conservez vos critères techniques avant de choisir une formule.