measurement-and-instrumentation
Tests de dépannage à l'huile: causes communes et solutions pratiques
Table of Contents
Comprendre les tests de flaky et leur impact sur le développement de logiciels
Les tests flaky sont l'un des défis les plus frustrants dans le développement de logiciels modernes. Ce sont des tests automatisés qui présentent un comportement incohérent, qui transmettent certaines exécutions et échouent sur d'autres, malgré aucun changement apporté à la base de code sous-jacente.
Lorsque les développeurs ne peuvent pas faire confiance à leur suite de test, ils commencent à ignorer les échecs de test, ce qui entraîne une dangereuse érosion de la confiance dans l'ensemble du processus d'assurance de la qualité. Les équipes perdent d'innombrables heures à étudier les faux positifs, à refaire les suites de test et à débattre si une défaillance représente un véritable bug ou un autre test de flaky.
Dans les pipelines d'intégration continue et de déploiement continu (IC/CD), les essais en panne deviennent encore plus problématiques. Un seul essai en panne peut bloquer les déploiements, forcer des équipes de recul inutiles ou pire, pour ignorer les échecs légitimes. Des études ont montré que même un faible pourcentage de tests en panne peut réduire la productivité du développeur de 16% et augmenter considérablement les temps de construction.
Il est essentiel de comprendre les causes profondes de la flocosité des tests et de mettre en oeuvre des approches systématiques pour prévenir et résoudre ces problèmes pour maintenir un processus de développement sain et efficace. Ce guide exhaustif explore les causes communes des tests de flocons, fournit des solutions pratiques pour les résoudre et propose des stratégies pour construire des suites de test plus résistantes que les équipes peuvent faire confiance.
Causes communes des essais de flocons
L'identification de la cause fondamentale des tests de flocons est la première étape vers la résolution. Bien que chaque test de flocons puisse avoir des caractéristiques uniques, la plupart se trouvent dans plusieurs catégories bien documentées.
Questions relatives au calendrier et à la synchronisation
Les problèmes liés au calendrier sont peut-être la source la plus courante de flakiness de test. Ces problèmes se posent lorsque les tests font des hypothèses sur la rapidité des opérations, conduisant à des conditions de course et des défaillances intermittentes.
Lorsque les développeurs écrivent des tests qui font une pause pour une durée fixe (comme attendre 2 secondes pour une réponse API), ils créent des tests fragiles qui peuvent passer sur des systèmes rapides mais échouent sur des systèmes plus lents, ou vice versa. Ces attentes arbitraires perdent du temps en attendant plus longtemps que nécessaire ou ne parviennent pas à attendre assez longtemps sous différentes charges système.
Les attentes implicites et explicites dans les cadres de test d'interface utilisateur peuvent également contribuer à la flakiness lorsque configurés incorrectement. Les tests qui vérifient la présence d'éléments avant que le DOM ait été complètement mis à jour, ou qui tentent d'interagir avec des éléments avant qu'ils deviennent cliquables, échoueront intermittentement en fonction des performances du système et des conditions du réseau.
Les effets d'animation et de transition dans les interfaces utilisateur introduisent une complexité de chronométrage supplémentaire. Un test qui tente de cliquer sur un bouton alors qu'il est toujours en train d'animer en position peut parfois réussir et échouer d'autres, selon le moment exact de l'exécution du test par rapport à la fin de l'animation.
Dépendances sur les systèmes externes
Les tests qui reposent sur des systèmes externes – tels que les API tierces, les bases de données, les systèmes de fichiers ou les services de réseau – contribuent à l'infiabilité de ces systèmes.
Les appels d'API vers des services externes sont particulièrement problématiques. Ces services peuvent connaître des temps d'arrêt, des demandes d'accélérateurs, retourner des temps de réponse différents, ou changer leurs données sans préavis. Un test qui dépend d'une réponse spécifique d'une API météorologique, d'une passerelle de paiement ou d'une plateforme de médias sociaux échouera chaque fois que ce service se comportera de façon inattendue.
Les bases de données de test partagées peuvent conduire à des conflits de données lorsque plusieurs tests se déroulent simultanément. L'épuisement du pool de connexion, les problèmes d'isolement des transactions et le décalage de réplication dans les bases de données distribuées contribuent tous à un comportement de test incohérent. Les tests qui supposent un état de base de données spécifique sans mettre en place et démolir correctement cet état échoueront lorsque d'autres tests modifieront les données partagées.
Les opérations du système de fichiers introduisent la flakiness à travers des problèmes de synchronisation, des problèmes de permission et de verrouillage des ressources. Les tests qui lisent ou écrivent des fichiers peuvent échouer si le système de fichiers est lent, si les fichiers sont verrouillés par d'autres processus, ou si le nettoyage à partir des tests précédents n'a pas été effectué avec succès.
Conditions de race et problèmes de devises
Les conditions de course se produisent lorsque le résultat d'un test dépend du moment imprévisible ou de l'ordre des opérations simultanées. Ces problèmes sont notoirement difficiles à diagnostiquer parce qu'ils peuvent se manifester uniquement dans des conditions spécifiques ou des charges système, ce qui les rend aléatoires et irréproductibles.
Le code multi-threaded est une source commune de conditions de course. Lorsque les tests code d'exercice qui utilise des threads, des pools de threads, ou un traitement asynchrone, l'interleaving exact des opérations peut varier entre les essais. Un test peut passer lorsque le fil A se termine avant le fil B, mais échoue lorsque l'ordre s'inverse.
Lorsque plusieurs tests modifient simultanément des variables globales, des objets à simpleton ou des champs statiques, ils peuvent interférer de manière imprévisible. Les modifications d'un test peuvent affecter les affirmations d'un autre test, ce qui entraîne des défaillances qui ne surviennent que lorsque des tests spécifiques se déroulent simultanément.
Les tests qui publient des événements ou des messages et qui vérifient immédiatement les effets secondaires peuvent échouer si le traitement des événements n'est pas terminé. La nature asynchrone de ces systèmes signifie que le moment de la livraison et du traitement des événements n'est pas déterministe.
Dépendances de l'ordre des essais
Les tests bien conçus doivent être indépendants et produire les mêmes résultats quel que soit l'ordre d'exécution. Cependant, de nombreuses suites de test contiennent des dépendances cachées où le succès d'un test dépend d'un autre test en cours d'exécution en premier, ou où les tests échouent lorsqu'ils sont exécutés en isolement mais passent lorsqu'ils sont exécutés dans le cadre de la suite complète.
Les problèmes de configuration et de démontage sont une cause principale de dépendances de l'ordre. Les tests qui ne nettoient pas correctement après eux laissent derrière eux l'état qui affecte les tests ultérieurs. Cela peut inclure des enregistrements de base de données, des fichiers, des variables d'environnement ou des objets simpleston modifiés.
Les hypothèses implicites sur l'état initial créent la fragilité. Un test qui suppose qu'une table de base de données est vide, qu'un cache est effacé ou qu'une configuration spécifique est chargée échouera si un test précédent viole ces hypothèses. Ces dépendances passent souvent inaperçues lorsque les tests sont régulièrement exécutés dans le même ordre pendant le développement mais en surface lorsque l'exécution des tests est randomisée ou parallélisée.
Contraintes de ressources et charge du système
Les tests qui passent sur les postes de travail de développeur peuvent échouer dans les environnements CI/CD en raison des différences dans les ressources disponibles. CPU, mémoire, E/S disque et bande passante réseau tout affecte l'exécution des tests, et la discordance des ressources peut causer des tests sensibles au moment de l'échec intermittent.
Une suite de tests qui consomme progressivement la mémoire sans la libérer peut causer des tests ultérieurs à échouer en raison d'erreurs hors de la mémoire. De même, des tests qui ouvrent des connexions de base de données, des poignées de fichiers ou des prises réseau sans les fermer peuvent épuiser les ressources du système, entraînant des défaillances dans les tests ultérieurs.
Les essais en cours dans des conteneurs Docker ou des machines virtuelles peuvent présenter des caractéristiques de performance différentes de celles qui fonctionnent sur du métal nu. Le throttling du CPU, les ressources partagées entre les conteneurs et les frais généraux de virtualisation du réseau peuvent tous contribuer à la flocosité liée au timing.
Code non déterministe et données aléatoires
Le code qui produit des sorties différentes pour les mêmes entrées crée une flakiness de test inhérente. Générateurs de nombre aléatoire, logique basée sur l'horodatage et génération UUID tous introduire non-déterminisme qui peut causer des échecs de test lorsque les valeurs générées ne correspondent pas aux attentes de test.
Les tests qui utilisent l'heure ou la date actuelle sont particulièrement sujets à la flakiness. Logique qui se comporte différemment en fonction de l'heure du jour, du jour de la semaine, ou de la proximité des limites du mois causera des tests à échouer à des moments précis. Un test qui passe en semaine mais échoue le week-end, ou qui échoue seulement pendant la première heure de chaque mois, montre ce type de flakiness dépendante du temps.
Bien que les tests basés sur des propriétés utilisent intentionnellement des données aléatoires pour explorer l'espace d'entrée, des tests mal conçus peuvent générer des données qui violent parfois des hypothèses ou déclenchent des chemins de code inattendus.
Différences d'environnement et de configuration
Les tests qui dépendent de configurations d'environnement spécifiques échoueront lorsque ces configurations varieront. Les différences dans les systèmes d'exploitation, les versions logicielles installées, les variables d'environnement, les chemins de fichiers et les localisations du système peuvent tous amener des tests à se comporter de façon incohérente dans différents environnements d'exécution.
Les séparateurs de chemin et la sensibilité des cas de système de fichiers créent une flakiness multiplateforme. Les tests qui codent les chemins de style Windows avec des rétro-slashs échoueront sur les systèmes de type Unix. De même, les tests qui supposent des systèmes de fichiers insensibles aux cas (comme Windows et macOS par défaut) peuvent échouer sur les systèmes de fichiers Linux sensibles aux cas.
Les différences de locale et de fuseau horaire affectent le formatage des chaînes, l'analyse de la date et le comportement de tri. Un test qui formate une date et attend une représentation spécifique des chaînes échouera si la locale du système diffère de ce que le test attend. Les bogues liés à la fuseau horaire sont particulièrement insidieux, car ils ne peuvent se manifester que lorsque les tests se déroulent dans différentes régions géographiques ou pendant les transitions de temps d'heure.
Solutions pratiques pour la fixation des essais de flocons
Une fois que vous avez identifié les causes de la flakiness dans votre suite de test, vous pouvez appliquer des solutions ciblées pour éliminer le comportement peu fiable. Les stratégies suivantes s'attaquent aux sources les plus communes de flakiness test et aident à construire des suites de test plus robustes et fiables.
Mise en œuvre de stratégies d'attente appropriées
Remplacer les énoncés de sommeil codés avec dures formes par des mécanismes d'attente intelligents est l'un des moyens les plus efficaces pour éliminer la flakiness liée au moment.
Pour les tests d'interface utilisateur, utilisez des attente explicites qui vérifient les conditions spécifiques avant de procéder. Au lieu de dormir pendant 5 secondes et en espérant qu'un bouton apparaît, attendez explicitement que le bouton soit présent et cliquable. La plupart des cadres de test d'interface utilisateur comme Selenium, Playwright et Cypress fournissent des méthodes intégrées pour attendre la visibilité des éléments, la clickabilité et le contenu texte.
Pour les tests d'API et d'intégration, mettre en place des mécanismes de sondage qui vérifient les changements d'état prévus. Lorsque vous testez des opérations asynchrones comme le traitement d'emploi ou la gestion d'événements, faites un sondage sur l'état du système à intervalles réguliers jusqu'à ce que le résultat attendu apparaisse ou que le délai raisonnable expire.
Configurez des valeurs de temps d'attente appropriées basées sur des attentes réalistes. Les délais devraient être suffisamment longs pour tenir compte de la variabilité normale du système mais suffisamment courts pour échouer rapidement lorsque quelque chose ne va pas. Un délai d'attente de 30 secondes pourrait être approprié pour un appel API complexe, tandis que 5 secondes pourraient suffire pour une simple requête de base de données.
Essais d'isolement des dépendances externes
L'élimination des dépendances sur les systèmes externes est cruciale pour créer des tests fiables et rapides. En isolant les tests des services externes, des bases de données et des systèmes de fichiers, vous supprimez les principales sources de variabilité et faites des tests déterministes.
Utilisez la moquerie et le scoop pour remplacer les dépendances externes par des doubles de test contrôlés. Les cadres de mocking vous permettent de simuler le comportement des API externes, des bases de données et des services sans les appeler réellement. Cela vous donne un contrôle complet sur les réponses, le timing et les conditions d'erreur que votre code rencontre lors des tests. Par exemple, au lieu d'appeler une véritable API passerelle de paiement, utilisez une maquette qui retourne des réponses prédéfinies de succès ou d'échec, vous permettant de tester à la fois des chemins heureux et la gestion des erreurs sans dépendre de la disponibilité du service externe.
De nombreuses bases de données offrent des modes in-memory qui fournissent la même interface que la base de données de production mais fonctionnent entièrement en mémoire, éliminant la latence du réseau et la variabilité des E/S disque. Les bases de données in-memory comme le mode in-memory H2, SQLite ou les instances in-memory Redis fournissent des environnements de test rapides et isolés qui réinitialisent proprement entre les tests.
Utiliser des tests contractuels pour les dépendances externes de l'API. Plutôt que de tester contre les API externes en direct, définir des contrats qui spécifient les formats de demande et de réponse attendus, puis vérifier que votre code implémente correctement ces contrats. Des outils comme Pacte permettent des tests contractuels axés sur le consommateur, où vous testez contre une maquette qui fait appliquer le contrat, en assurant que votre code fonctionnera avec la véritable API sans en dépendre lors de l'exécution des tests.
Pour les opérations du système de fichiers, utilisez des systèmes de fichiers virtuels ou in-memory. Des bibliothèques existent pour la plupart des langages de programmation qui fournissent des abstractions de système de fichiers qui peuvent être sauvegardées par la mémoire plutôt que par le disque.
Assurer l'isolement et l'indépendance des tests
Chaque test doit être totalement indépendant, capable de fonctionner dans n'importe quel ordre ou isolément sans affecter ou être affecté par d'autres tests.
Avant chaque test, créez l'état exact requis pour que ce test puisse être exécuté. Après chaque test, nettoyez toutes les modifications, renvoyez le système à un état vierge. Cela comprend les enregistrements de base de données, les fichiers, les variables d'environnement et tout autre état mutable. La plupart des cadres de test fournissent des crochets comme avant] et après] qui fonctionnent avant et après chaque test, assurant une isolation cohérente.
Utilisez les transactions de base de données pour l'isolement des tests. Enveloppez chaque test dans une transaction de base de données qui retourne à la fin du test, défaire automatiquement toutes les modifications de base de données. Cette approche est plus rapide que la suppression manuelle des enregistrements et garantit qu'aucune donnée de test ne persiste entre les tests.
Évitez l'état mutable partagé entre les tests. Les variables globales, les objets monotones et les champs statiques qui persistent entre les exécutions de test créent des dépendances cachées. Soit éliminer ces états partagés, les réinitialiser dans les méthodes de configuration, ou utiliser l'injection de dépendance pour fournir de nouvelles instances pour chaque test.
Beaucoup de coureurs de test supportent l'ordre des tests randomisés, ce qui aide à identifier les tests qui dépendent de séquences d'exécution spécifiques. Les tests qui échouent lorsqu'ils sont exécutés au hasard mais passent dans un ordre fixe ont des dépendances d'ordre qui doivent être traitées.
Gestion des conditions de comptabilisation et de course
Pour s'attaquer aux conditions de course, il faut à la fois concevoir soigneusement les essais et mettre en place des mécanismes de synchronisation appropriés, afin de rendre les opérations simultanées déterministes et prévisibles dans le contexte des essais.
Utilisez des primitives de synchronisation pour contrôler l'exécution simultanée dans les tests. Lors de l'essai de code multifilés, utilisez des verrous, des barrières ou des sémaphores pour coordonner l'exécution des threads et assurer que les opérations se terminent dans l'ordre prévu. Par exemple, utilisez un CountDownLatch pour attendre que plusieurs threads atteignent un point précis avant de procéder à des assertions.
Évitez l'exécution parallèle de tests pour des ressources partagées. Alors que l'exécution parallèle de tests accélère les suites de tests, elle peut exposer ou créer des conditions de course dans des tests qui ne sont pas correctement isolés. Marquez des tests qui doivent être exécutés en série, ou assurez-vous que les tests parallèles utilisent des ressources complètement séparées (différents schémas de base de données, différents répertoires de fichiers, etc.).
Pour les systèmes pilotés par un événement, implémentez des mécanismes de synchronisation spécifiques aux essais. Ajoutez des crochets ou des callbacks qui permettent d'attendre que le traitement des événements soit terminé. Par exemple, fournissez une méthode de test seulement qui bloque jusqu'à ce que tous les événements en attente dans une file d'attente aient été traités, en veillant à ce que les assertions ne fonctionnent qu'après que le système atteint un état stable.
Utilisez des outils de test de proximité déterministes. Certains cadres fournissent des utilitaires pour tester le code concurrent en contrôlant systématiquement le planning des threads et en explorant les différents interleaves d'exécution. Ces outils peuvent aider à identifier les conditions de course qui pourraient autrement apparaître seulement sporadiquement.
Contrôle du non-déterminisme
Pour rendre le code non déterministe déterministe dans les tests, il faut injecter des solutions de rechange contrôlables pour les opérations aléatoires et temporelles.
Utiliser l'injection de dépendance pour fournir des implémentations contrôlées par test de générateurs de nombres aléatoires et de sources de temps. Au lieu d'appeler directement Math.random()[ ou [nouveau Date(), injecter ces dépendances de façon à ce que les tests puissent fournir des générateurs de nombres aléatoires ensemencés ou des implémentations fixes d'horloges.
Lorsque le caractère aléatoire est nécessaire pour la production de données d'essai, utilisez une graine fixe de sorte que la même séquence de « aléatoire » soit générée sur chaque essai, ce qui maintient les avantages des tests randomisés tout en assurant la reproductibilité.
Utilisez des bibliothèques d'abstraction d'horloge qui permettent la manipulation du temps dans les tests. Bibliothèques comme la classe de Java Horloge, les minuteurs faux de Sinon de JavaScript, ou le gegeggun de Python permettent des tests pour contrôler l'heure actuelle, avancez le temps programmatiquement, et testez le comportement dépendant du temps de façon déterministe.
Pour la génération UUID et d'autres créations d'identificateurs uniques, utilisez des doubles test qui retournent des valeurs prévisibles. Cela facilite l'écriture des assertions test et élimine une source de non-déterminisme.
Normalisation des environnements d'essai
Assurer des environnements d'essai cohérents entre les différentes machines et les contextes d'exécution élimine la flakiness liée à l'environnement.
Utilisez la conteneurisation pour créer des environnements de test reproductibles. Les conteneurs Docker fournissent des environnements isolés et cohérents qui incluent toutes les dépendances, configurations et services nécessaires. En exécutant des tests dans des conteneurs, vous assurez que chaque développeur et système CI/CD utilise des environnements identiques, éliminant les problèmes de "travail sur ma machine".
Définissez explicitement les variables locales, timezone et autres variables d'environnement dans la configuration de test. Ne comptez pas sur les défauts du système qui peuvent varier d'un environnement à l'autre. Configurez ces paramètres programmatiquement au début de votre suite de test pour assurer la cohérence.
Utilisez des références de fichiers indépendantes du chemin. Au lieu de coder des chemins absolus ou de faire des hypothèses sur les structures de répertoires, utilisez des chemins relatifs à partir de répertoires de base bien définis ou de répertoires temporaires créés spécifiquement pour l'exécution de tests.
Les versions de dépendances à l'épingle pour assurer un comportement cohérent. Les versions de dépendance flottantes peuvent introduire la flakiness lorsque de nouvelles versions changent de comportement.
Mettre en œuvre avec soin la logique de réessayer
Bien que la réessayer des essais échoués puisse réduire l'impact de la flocosité, il faut les utiliser judicieusement pour éviter de masquer les problèmes sous-jacents.
Mettre en œuvre des systèmes de réticulateurs automatiques uniquement pour des scénarios précis et connus, plutôt que de réessayer toutes les défaillances d'essai, d'identifier des catégories spécifiques de défaillances transitoires (comme les temps d'attente du réseau ou les assertions de ressources) et de réessayer seulement celles-ci.
Limiter le nombre de rétries et de statistiques de rétry de la voie. Configurer un maximum de 2-3 rétries pour les tests flasques, et surveiller la fréquence des rétries nécessaires. Si un test exige systématiquement des rétries à passer, il indique un problème sous-jacent qui devrait être corrigé plutôt que travaillé autour.
Enregistrez les informations détaillées sur les tentatives de réessayer. Lorsqu'un test échoue et est réessayer, capturez les informations diagnostiques sur la raison de son échec. Ces données aident à identifier les motifs et les causes profondes, guidant les efforts pour éliminer la flocosité en permanence.
Considérez la possibilité de récupérer une mesure temporaire tout en travaillant vers des correctifs appropriés. L'objectif devrait toujours être d'éliminer la flaquidité à sa source plutôt que de compter sur des rétractations indéfiniment.
Stratégies de prévention des essais de flocons
La prévention est plus efficace que la remise en état en matière de tests flasques. En adoptant des pratiques qui favorisent la fiabilité des tests dès le départ, les équipes peuvent éviter d'introduire la flakiness en premier lieu.
Établir des lignes directrices claires pour les essais
Élaborer et appliquer des normes d'équipe pour la rédaction de tests fiables. Documenter les meilleures pratiques pour l'isolement des tests, les stratégies d'attente et la gestion de la dépendance.
Définir ce qui constitue un test acceptable. Les tests doivent être rapides, isolés, répétables et déterministes. Ils ne doivent pas dépendre de services externes, d'un ordre d'exécution spécifique ou d'hypothèses environnementales.
Montrez aux développeurs comment tester correctement les opérations asynchrones, simulez les dépendances externes et traitez les problèmes de calendrier. Des exemples concrets sont plus efficaces que des lignes directrices abstraites pour enseigner les bonnes pratiques de test.
Mettre en oeuvre une surveillance et une détection continues
Mettre en place des systèmes qui permettent de suivre la fiabilité des tests et des tests de signalisation qui présentent un comportement incohérent.
Surveiller les résultats des tests qui échouent occasionnellement et calculer leur taux de flakiness (pourcentage des essais qui échouent). Les tests avec des taux de flakiness supérieurs à un seuil (comme 1-5 %) doivent être examinés et corrigés rapidement.
Exécuter des tests à plusieurs reprises pour détecter la flocosité. Dans les pipelines CI/CD, envisager de faire fonctionner la suite d'essais à plusieurs reprises ou de faire des tests individuels à plusieurs reprises en parallèle.
Utilisez des outils spécialisés pour la détection de test flaky. Plusieurs outils commerciaux et open-source analysent les résultats des tests, identifient les tests flaky, et fournissent des informations sur les modèles de défaillance.
Créez des tableaux de bord qui visualisent les mesures de fiabilité des tests. Faites en sorte que les tests soient visibles pour toute l'équipe à travers des tableaux de bord qui montrent des taux de flakiness, des tests les plus problématiques et des tendances au fil du temps.
Mise en quarantaine et traitement Essais de scintillement systématique
Lorsque des tests flasques sont identifiés, les manipuler systématiquement plutôt que de leur permettre d'éroder la confiance dans la suite d'essais.
Les tests en quarantaine sont marqués d'annotations spéciales ou sont déplacés vers des suites d'essai distinctes. Cela les empêche de bloquer les constructions tout en les maintenant visibles et suivis. De nombreux cadres d'essai supportent des annotations comme @Flaky ou @Quarantine qui excluent les tests des parcours standard mais permettent de les exécuter séparément.
Créez des tickets ou des problèmes pour chaque test mis en quarantaine. Documentez le comportement flou, y compris les modèles de défaillance, les messages d'erreur et toute hypothèse sur les causes profondes.
Établir une politique selon laquelle les tests mis en quarantaine doivent être fixés dans un délai précis (comme deux semaines) ou supprimés s'ils ne peuvent être fiables, ce qui empêche l'accumulation de tests invalidants qui ne donnent aucune valeur.
Si un test est si flou qu'il ne peut être rendu fiable malgré de multiples tentatives, et si la fonctionnalité qu'il teste est couverte par d'autres tests, la suppression peut être la meilleure option. Une plus petite série de tests fiables est plus précieuse qu'une plus grande suite qui comprend des tests non fiables.
Conception pour l'essai
Écrire le code de production en gardant à l'esprit les tests. Le code conçu pour la testabilité est naturellement plus facile à tester de façon fiable.
Lorsque des bases de données, des API, des systèmes de fichiers et d'autres ressources externes sont injectés plutôt que codés dur, les tests peuvent facilement remplacer les doubles tests, éliminant ainsi les principales sources de flakiness.
Évitez les variables statiques et globales, qui créent des dépendances cachées entre les tests et rendent l'isolement difficile. Préférez les méthodes d'instance et les dépendances injectées par rapport aux méthodes statiques et à l'état global.
Fournir des crochets spécifiques aux essais et une observabilité. Inclure des mécanismes dans le code de production qui permettent aux tests d'observer l'état interne et le timing de contrôle. Par exemple, fournir des rappels qui s'allument lorsque les opérations asynchrones se terminent, ou exposer les files d'attente internes qui peuvent vérifier le vide.
Lorsque la logique d'entreprise est enchevêtrée avec l'accès à la base de données, les appels réseau ou les E/S de fichiers, il devient difficile de tester séparément. Utilisez des modèles architecturaux comme l'architecture hexagonale ou une architecture propre pour séparer la logique de base de l'infrastructure, ce qui rend la logique de base facile à tester sans dépendances externes.
Investir dans l'infrastructure d'essai
Des tests fiables nécessitent une infrastructure fiable. Investir dans les outils, les cadres et les environnements qui supportent l'exécution stable des tests.
Fournir des ressources suffisantes pour l'exécution des tests. Les agents sous-alimentés de CI/CD qui sont surchargés de constructions concurrentes présenteront une flakiness liée au moment.
Utiliser des bases de données et des services d'essai spécialisés. Le partage de bases de données ou de services entre les essais crée des assertions et la pollution de l'état.
Mettre en œuvre une gestion des données de test appropriée. Fournir des outils et des cadres pour créer des données de test de façon cohérente et les nettoyer de façon fiable.
Les bogues dans les cadres de test eux-mêmes peuvent causer de la flakiness. Mettre régulièrement à jour les dernières versions stables pour bénéficier des corrections et améliorations de bugs.
Favoriser une culture de la qualité des tests
Les solutions techniques seules sont insuffisantes sans une culture d'équipe qui valorise la fiabilité des tests.
Faites de la fiabilité des tests une priorité dans les revues de code. Examiner les tests avec la même rigueur que le code de production. Recherchez des modèles de flakiness communs comme les sommeils codés dur, les dépendances externes et l'état partagé.
Célébrez les améliorations pour tester la fiabilité. Reconnaître les membres de l'équipe qui fixent des tests flasques ou améliorent l'infrastructure de test. Faire de la qualité des tests une partie visible des mesures de succès de l'équipe.
Attribuez du temps pour la maintenance des tests. Ne traitez pas l'amélioration des tests comme quelque chose à faire « quand il y a du temps ».
Partager les connaissances sur les pratiques exemplaires de l'essai. Déjeuner et apprendre, rédiger des documents internes et discuter des défis de l'essai dans les rétrospectives d'équipe.
Techniques avancées pour la gestion des essais par collage
Au-delà de la prévention et de la remise en état de base, plusieurs techniques avancées peuvent aider les équipes à gérer plus efficacement les essais de flocons dans des systèmes complexes.
Mise en œuvre de l'analyse d'impact des essais
L'analyse d'impact des tests identifie les tests qui sont affectés par les changements de code, permettant aux équipes de n'exécuter que des tests pertinents et de détecter la flakiness plus efficacement. En comprenant la relation entre le code et les tests, vous pouvez exécuter des tests affectés plusieurs fois pour vérifier la stabilité tout en sautant des tests non affectés pour gagner du temps.
Les plateformes et outils de test modernes de CI/CD offrent des fonctionnalités d'analyse d'impact qui suivent la couverture du code et déterminent quels tests sont les chemins de code. Lorsqu'un développeur modifie un fichier ou une fonction spécifique, le système identifie tous les tests qui couvrent ce code et les exécute de façon préférentielle.
Utilisation des principes d'ingénierie du chaos
En introduisant intentionnellement des défaillances, des retards et des contraintes de ressources pendant l'exécution des tests, vous pouvez découvrir quels tests sont fragiles et quels chemins de code ne sont pas correctement manipulés.
Les tests qui échouent dans ces conditions révèlent des dépendances sur des hypothèses spécifiques de temps, de disponibilité ou de ressources. Bien que cela puisse sembler contre-intuitif – faisant intentionnellement échouer les tests – il aide à identifier et à corriger la fragilité avant qu'elle ne cause des problèmes de production.
Tirer parti de l'apprentissage automatique pour la prévision de la flakiness
Certaines plates-formes de test avancées utilisent l'apprentissage automatique pour prédire quels tests sont susceptibles d'être flous en fonction des modèles historiques, des changements de code et des caractéristiques de test. Ces systèmes analysent des milliers de tests pour identifier des modèles qui sont en corrélation avec la flakiness, comme des modèles de test spécifiques, des dépendances ou des structures de code.
En prédisant la flakiness avant qu'elle ne devienne un problème généralisé, les équipes peuvent s'attaquer de façon proactive aux problèmes potentiels. Ces systèmes peuvent signaler des tests nouvellement écrits qui présentent des caractéristiques semblables à des tests connus, incitant les développeurs à les examiner et à les renforcer avant qu'ils ne soient fusionnés.
Mise en œuvre du traçage distribué pour l'exécution des essais
Les outils de traçage distribués, généralement utilisés pour la surveillance de la production, peuvent également fournir des informations précieuses sur l'exécution des tests. En instrumentant les tests avec le traçage, vous pouvez visualiser la séquence exacte des opérations, le calendrier de chaque étape et les dépendances entre les composants pendant l'exécution des tests.
Lorsqu'un test échoue, la trace fournit un calendrier détaillé montrant exactement ce qui s'est passé, où des retards ont eu lieu et quelles opérations ont été menées à bien ou ont échoué.
Outils et cadres pour la gestion des essais de flocons
De nombreux outils et cadres peuvent aider les équipes à détecter, diagnostiquer et corriger les tests flasques. Choisir les bons outils pour votre technologie empile et l'approche de test peut améliorer considérablement votre capacité à maintenir la fiabilité des tests.
Essais de course avec détection de flakiness
Les coureurs de test modernes incluent des fonctionnalités intégrées pour détecter et gérer des tests flaky. JUnit 5 supporte l'exécution de tests répétés par l'annotation @RepeatedTest, vous permettant de faire un test plusieurs fois pour vérifier la stabilité. pytest offre le plugin pytest-repeat pour des fonctionnalités similaires. Ces fonctionnalités permettent de vérifier facilement que les tests passent régulièrement avant de les considérer fiables.
Les coureurs de test comme Jest, Mocha et TestNG offrent des options de configuration pour les réticulations, les timeouts et l'exécution parallèle qui peuvent aider à gérer la flakiness.
Services spécialisés de détection des essais de flaky
Plusieurs services commerciaux et open-source se spécialisent dans la détection et la gestion des tests. BuildPulse détecte automatiquement les tests flasques en analysant les résultats des tests dans tous les bâtiments et fournit des analyses détaillées sur la fiabilité des tests. Launchable utilise l'apprentissage machine pour identifier les tests flasques et optimiser la sélection des tests.
Pour les équipes utilisant les actions GitHub, l'action Flaky Test Detection peut automatiquement identifier et signaler des tests flasques. Des intégrations similaires existent pour Jenkins, CircleCI, GitLab CI et d'autres plateformes CI/CD.
Cadres de macquage et d'écrassage
Des cadres de simulation robustes sont essentiels pour isoler les tests de dépendances externes. Mockito pour Java, unittest.mock pour Python, Sinon pour JavaScript et des cadres similaires pour d'autres langues fournissent des capacités puissantes pour créer des doubles de test qui remplacent les dépendances externes par des alternatives contrôlées.
Pour les simulations d'API HTTP, des outils comme WireMock, MockServer et nock vous permettent de simuler des réponses externes à l'API sans faire de vrais appels réseau. Ces outils peuvent simuler différents scénarios de réponse, notamment des succès, des échecs, des temps d'attente et des charges utiles spécifiques en réponse, vous donnant un contrôle complet sur les dépendances externes pendant les tests.
Bibliothèques de contrôle du temps et de la randomisation
Les bibliothèques qui contrôlent le temps et le hasard sont inestimables pour éliminer le non-déterminisme. L'abstraction de l'horloge de Java, les faux minuteurs Sinon JavaScript, le gegegun de Python et les bibliothèques similaires pour d'autres langues permettent de contrôler l'heure actuelle, rendant les tests dépendants du temps déterministe.
Pour le contrôle de la randomité, la plupart des langues fournissent des moyens de semer des générateurs de nombres aléatoires. De plus, des bibliothèques comme le faux peuvent générer des données de test cohérentes lorsqu'elles sont fournies avec une graine fixe, vous permettant d'utiliser des données de test réalistes tout en maintenant la reproductibilité.
Outils de gestion des conteneurs et de l'environnement
Docker et Docker Compose fournissent des environnements de test cohérents et reproductibles. Testcontainers est une bibliothèque particulièrement utile qui permet de lancer et d'arrêter programmatiquement les conteneurs Docker, fournissant des bases de données isolées, des files d'attente de messages et d'autres services pour chaque essai.
Pour les tests par navigateur, des outils comme Selenium Grid, BrowserStack et Sauce Labs fournissent des environnements de navigateur cohérents qui éliminent la variabilité des installations et des configurations locales du navigateur.
Études de cas : Solutions de test à l'huile d'olive du monde réel
L'examen de la façon dont les organisations ont réussi à faire face aux tests flasques fournit des idées pratiques et de l'inspiration pour vos propres efforts.
Approche de Google pour les tests de flaky
Google a documenté leur approche de gestion des tests flasques sur leur base de code massive. Ils effectuent des tests à plusieurs reprises pour détecter la flakiness, mettre automatiquement en quarantaine les tests flasques, et fournir des analyses détaillées pour aider les développeurs à comprendre et à corriger le comportement flasque.
Une des principales conclusions de l'expérience de Google est que les tests flasques se cluster souvent autour de modèles de code spécifiques ou d'approches de test. En identifiant ces modèles et en fournissant de meilleures alternatives, ils ont été en mesure d'empêcher des catégories entières de flakiness d'être introduits.
Améliorations de fiabilité des tests de Microsoft
Microsoft a partagé son chemin vers l'amélioration de la fiabilité des tests dans les systèmes à grande échelle. Ils ont mis en œuvre une analyse d'impact complète pour identifier les tests à exécuter pour chaque changement de code, leur permettant de faire des tests affectés plusieurs fois pour vérifier la stabilité.
Une partie importante de l'approche de Microsoft a impliqué le changement culturel - faire de la fiabilité des tests un indicateur de performance clé et allouer du temps dédié à l'amélioration des tests.
L'ingénierie du Chaos pour les tests de Netflix
Netflix a appliqué son expertise en ingénierie du chaos pour tester, introduisant intentionnellement des défaillances et des retards pendant l'exécution des essais pour identifier des tests et des codes fragiles.
En embrassant la réalité selon laquelle les systèmes distribués sont intrinsèquement peu fiables, Netflix a conçu leurs tests pour accommoder et vérifier la bonne manipulation des défaillances plutôt que d'assumer des conditions parfaites.
Mesurer le succès : les critères de fiabilité des tests
Pour améliorer la fiabilité des tests, vous devez le mesurer. Plusieurs mesures clés aident à suivre les progrès et à identifier les domaines nécessitant une attention particulière.
Taux de flascité
Le taux de flakiness mesure le pourcentage de tests qui échouent pour des raisons sans rapport avec les changements de code. Calculez ceci en suivant la fréquence de chaque test échoue et en déterminant le pourcentage de ces échecs sont dus à la flakiness par rapport aux vrais bugs. Une suite de tests saine devrait avoir un taux de flakiness inférieur à 1%, avec des tests individuels ayant des taux encore plus bas.
Évaluation de la fiabilité
Le score de fiabilité des tests représente le pourcentage de tests qui passent régulièrement sur plusieurs parcours. Exécutez votre suite de test plusieurs fois (par exemple 10 fois) et calculez le pourcentage de tests qui passent tous les 10 fois.
Temps de détection et de correction
Suivez le temps qu'il faut pour détecter les tests flasques et le temps qu'il faut pour les corriger une fois détectés.
Créer un taux de réussite
Surveiller le pourcentage de constructions qui passent sans nécessiter de ré-exécutions en raison de défaillances de test. Un taux de succès de construction élevé indique que les tests de flocons ne perturbent pas le flux de travail de développement.
Confiance du développeur
Bien qu'il soit plus difficile de quantifier, la confiance du développeur dans la suite de tests est peut-être la mesure la plus importante. Les développeurs de sondages régulièrement sur la confiance dans les résultats de tests et sur la question de savoir s'ils enquêtent sur les échecs ou supposent qu'ils sont flous.
Résumé des pratiques exemplaires
La gestion réussie des tests de flocons nécessite une approche globale qui combine des solutions techniques, des améliorations de processus et des changements culturels. Voici les meilleures pratiques essentielles à mettre en œuvre :
- Utilisez des maquettes et des talons pour simuler des systèmes externes et éliminer les dépendances sur des services externes peu fiables, des bases de données et des API.
- Tests de descente dans un environnement contrôlé pour assurer la cohérence entre les différents contextes d'exécution, en utilisant la conteneurisation et la normalisation de l'environnement.
- L'exécution est soigneusement mise en oeuvre[ pour éviter de masquer les problèmes, limiter les relevés aux scénarios spécifiques et suivre les statistiques de ré-essai afin de cerner les problèmes sous-jacents.
- Analyze test échoue[ pour identifier les motifs et les causes profondes, en utilisant des outils de journalisation et de diagnostic détaillés pour comprendre pourquoi les tests échouent par intermittence.
- Remplacez les sommeils codés dur avec des conditions d'attente intelligentes qui s'interrogent sur des états spécifiques plutôt que d'attendre des durées arbitraires.
- Assurez l'isolation complète des tests[ par une configuration et un démontage appropriés, des transactions dans la base de données et l'élimination de l'état mutable partagé.
- Contrôler le non-déterminisme en injectant des implémentations contrôlées par essai de générateurs de nombres aléatoires, de sources temporelles et de générateurs d'identificateurs uniques.
- Normez les environnements de test[ en utilisant la conteneurisation, la configuration explicite de la locale et du fuseau horaire, et les versions de dépendance sur pied.
- Fiabilité du test de surveillance[ en continu grâce à la détection automatisée de la flakiness, au suivi de la vitesse de passage et aux tableaux de bord de la visibilité.
- Tests de flocons de qualité systématiquement tout en travaillant pour les fixer, les empêchant de bloquer les constructions mais les maintenant visibles et suivis.
- Code de conception pour la testabilité[ en utilisant l'injection de dépendance, en évitant l'état statique et en séparant la logique d'affaires des problèmes d'infrastructure.
- Investir dans l'infrastructure d'essai en fournissant des ressources adéquates, des bases de données d'essai spécialisées et des outils de gestion des données d'essai appropriés.
- Faire naître une culture de qualité des tests[ par des examens rigoureux du code, la célébration des améliorations et le temps consacré à la maintenance des tests.
- Utilisez des outils appropriés pour votre pile technologique, y compris des coureurs de test avec détection de flakiness, des cadres de simulation et des outils de gestion de l'environnement.
- Mesure et piste mesures de fiabilité pour comprendre l'état actuel, identifier les tendances et démontrer une amélioration au fil du temps.
Ressources pour l'apprentissage continu
Pour continuer à développer une expertise en matière de fiabilité des tests, il faut apprendre constamment et rester à jour avec les pratiques exemplaires en évolution.
Le Google Testing Blog publie régulièrement des articles sur la fiabilité des tests, la détection de la flaquidité et les meilleures pratiques de test basées sur l'expérience de Google avec des tests à grande échelle.
Le site de Martin Fowler à martinfowler.com contient de nombreux articles sur les modèles de test, les doubles tests et les pratiques d'intégration continue qui aident à prévenir la flakiness.
La documentation sur le sélénium offre des conseils complets sur la rédaction de tests fiables basés sur le navigateur, y compris des explications détaillées sur les stratégies d'attente et les meilleures pratiques pour la stabilité des tests d'assurance-chômage.
Pour les équipes utilisant des cadres d'essai spécifiques, la documentation officielle pour JUnit, Pytest, Jest et d'autres cadres fournit des informations détaillées sur les caractéristiques qui soutiennent la fiabilité des tests, y compris les mécanismes de ré-essai, l'exécution parallèle et l'isolement des tests.
Les travaux de recherche universitaire sur les tests logiciels continuent de fournir de nouvelles perspectives sur la flakiness des tests.Les documents de conférences comme la Conférence internationale sur le génie logiciel (ICSE) et le Symposium international sur les tests et l'analyse des logiciels (ISSTA) explorent les causes, la détection et la remise en état des tests flakis au moyen d'études empiriques rigoureuses.
Conclusion
Les tests de dépistage de la maladie représentent l'un des défis les plus importants dans le développement de logiciels modernes, sapant la confiance dans les tests automatisés et gaspillant un temps précieux de développement.
La clé du succès réside dans la lutte contre la flakiness à plusieurs niveaux : mettre en œuvre des solutions techniques comme des stratégies d'attente appropriées et l'isolement des tests, établir des processus de surveillance et de gestion des tests flakis, et favoriser une culture qui privilégie la qualité des tests.
N'oubliez pas que la fiabilité des tests n'est pas une réalisation ponctuelle mais un engagement continu. À mesure que les bases de codes évoluent, de nouvelles sources de flakiness émergeront, nécessitant une vigilance et une amélioration constantes. En faisant de la fiabilité des tests une valeur fondamentale et en investissant dans les outils, les processus et la culture qui les soutiennent, les équipes peuvent maintenir des suites de test de haute qualité qui accélèrent le développement plutôt que de l'entraver.
L'effort investi dans l'élimination des tests flasques rapporte des gains grâce à des cycles de développement plus rapides, des déploiements plus confiants et des logiciels de meilleure qualité. Commencez par identifier vos tests flasques les plus problématiques, appliquer les solutions appropriées de ce guide, et progressivement élargir vos efforts pour améliorer la fiabilité globale des suites de test.