Partager via


Décalage de test vers la gauche avec des tests unitaires

Les tests permettent de s’assurer que le code s’exécute comme prévu, mais le temps et l’effort de génération des tests prennent du temps à partir d’autres tâches telles que le développement de fonctionnalités. Avec ce coût, il est important d’extraire la valeur maximale du test. Cet article décrit les principes de test DevOps, en se concentrant sur la valeur des tests unitaires et sur une stratégie de test shift-left.

Les testeurs dédiés utilisés pour écrire la plupart des tests, et de nombreux développeurs de produits n’ont pas appris à écrire des tests unitaires. L’écriture de tests peut sembler trop difficile ou comme trop de travail. Il peut y avoir du scepticisme quant à l'efficacité d'une stratégie de test unitaire, de mauvaises expériences avec des tests unitaires mal écrits ou la crainte que les tests unitaires ne remplacent les tests fonctionnels.

Graphique qui décrit les arguments relatifs à l’adoption de tests unitaires.

Pour implémenter une stratégie de test DevOps, soyez pragmatique et concentrez-vous sur la création d’un élan. Bien que vous puissiez insister sur les tests unitaires pour le nouveau code ou le code existant qui peuvent être refactorisé correctement, il peut être judicieux pour une base de code héritée d’autoriser une dépendance. Si des parties significatives du code produit utilisent SQL, permettre aux tests unitaires de s'appuyer sur le fournisseur de ressources SQL au lieu de simuler cette couche peut être une approche temporaire pour avancer.

À mesure que les organisations DevOps arrivent à maturité, il devient plus facile pour le leadership d’améliorer les processus. Bien qu'il puisse y avoir une résistance au changement, les organisations Agile valorisent les changements qui paient clairement des dividendes. Il devrait être facile de vendre la vision d’exécutions de tests plus rapides avec moins d’échecs, car cela signifie plus de temps pour investir dans la génération de nouvelles valeurs par le biais du développement de fonctionnalités.

Taxonomie des tests DevOps

La définition d’une taxonomie de test est un aspect important du processus de test DevOps. Une taxonomie de test DevOps classifie les tests individuels par leurs dépendances et le temps nécessaire à l’exécution. Les développeurs doivent comprendre les types appropriés de tests à utiliser dans différents scénarios et les différents tests requis par le processus. La plupart des organisations classent les tests sur quatre niveaux :

  • Les tests L0 et L1 sont des tests unitaires, ou des tests qui dépendent du code dans l’assembly sous test et rien d’autre. L0 est une vaste classe de tests unitaires rapides en mémoire.
  • L2 sont des tests fonctionnels qui peuvent nécessiter l’assemblage ainsi que d’autres dépendances, comme le SQL ou le système de fichiers.
  • Les tests fonctionnels L3 s’exécutent sur des déploiements de service testables. Cette catégorie de test nécessite un déploiement de service, mais peut utiliser des stubs (ou prototypes) pour les dépendances de service clés.
  • Les tests L4 sont une classe restreinte de tests d’intégration qui s’exécutent sur la production. Les tests L4 nécessitent un déploiement complet du produit.

Bien qu'il soit idéalement souhaitable que tous les tests s'exécutent à tout moment, ce n'est pas faisable. Teams peut sélectionner l’emplacement où dans le processus DevOps pour exécuter chaque test, et utiliser des stratégies shift-left ou shift-right pour déplacer différents types de test antérieurs ou ultérieurs dans le processus.

Par exemple, l’attente peut être que les développeurs effectuent toujours des tests L2 avant de valider le code, une pull request échoue automatiquement si l’exécution du test L3 échoue, et le déploiement peut être bloqué si les tests L4 échouent. Les règles spécifiques peuvent varier d’une organisation à l’autre, mais l’application des attentes pour toutes les équipes au sein d’une organisation déplace tout le monde vers les mêmes objectifs de vision de qualité.

Instructions relatives aux tests unitaires

Définissez des instructions strictes pour les tests unitaires L0 et L1. Ces tests doivent être très rapides et fiables. Par exemple, le temps d’exécution moyen par test L0 dans un assembly doit être inférieur à 60 millisecondes. Le temps d’exécution moyen par test L1 dans un assembly doit être inférieur à 400 millisecondes. Aucun test à ce niveau ne doit dépasser 2 secondes.

Une équipe Microsoft exécute plus de 60 000 tests unitaires en parallèle en moins de six minutes. Leur objectif est de réduire ce temps à moins d’une minute. L'équipe suit le temps d'exécution des tests unitaires avec des outils comme celui figurant dans le graphique suivant et signale des bogues pour les tests qui dépassent le temps autorisé.

Graphique montrant le focus continu sur le temps d’exécution des tests.

Recommandations en matière de test fonctionnel

Les tests fonctionnels doivent être indépendants. Le concept clé pour les tests L2 est l’isolation. Les tests correctement isolés peuvent s’exécuter de manière fiable dans n’importe quelle séquence, car ils ont un contrôle total sur l’environnement dans lequel ils s’exécutent. L’état doit être connu au début du test. Si un test a créé des données et l’a laissé dans la base de données, il peut endommager l’exécution d’un autre test qui s’appuie sur un autre état de base de données.

Les tests de l'héritage qui nécessitent une identité utilisateur ont peut-être fait appel à des fournisseurs d'authentification externes pour obtenir l'identité. Cette pratique présente plusieurs défis. La dépendance externe peut être peu fiable ou indisponible momentanément, cassant le test. Cette pratique enfreint également le principe d’isolation des tests, car un test peut modifier l’état d’une identité, comme l’autorisation, ce qui entraîne un état par défaut inattendu pour d’autres tests. Envisagez d’empêcher ces problèmes en investissant dans la prise en charge des identités dans l’infrastructure de test.

Principes de test DevOps

Pour faciliter la transition d’un portefeuille de tests vers des processus DevOps modernes, articulez une vision de qualité. Teams doit respecter les principes de test suivants lors de la définition et de l’implémentation d’une stratégie de test DevOps.

Diagramme montrant un exemple de vision de qualité et répertorie les principes de test.

Déplacer vers la gauche pour tester précédemment

Les tests peuvent prendre beaucoup de temps. À mesure que les projets sont mis à l’échelle, les numéros de test et les types augmentent considérablement. Lorsque les suites de test mettent des heures ou des jours à se terminer, elles peuvent être reportées jusqu'à être exécutées in extremis. Les avantages des tests pour la qualité du code ne se concrétisent que longtemps après la validation du code.

Les tests de longue durée peuvent également produire des échecs qui prennent beaucoup de temps à examiner. Les équipes peuvent développer une tolérance aux échecs, surtout au début des sprints. Cette tolérance sape la valeur des tests en tant qu’aperçu de la qualité codebase. Les tests de longue durée et de dernière minute ajoutent également une imprévisibilité aux attentes de fin de sprint, car une dette technique inconnue doit être payée pour rendre le code livrable.

L’objectif du déplacement du test en amont est d’améliorer la qualité en intégrant les tâches de test plus tôt dans la chaîne de production. Grâce à une combinaison d’améliorations de test et de processus, le déplacement vers la gauche réduit le temps nécessaire à l’exécution des tests et l’impact des défaillances plus tard dans le cycle. Le déplacement vers la gauche garantit que la plupart des tests sont effectués avant qu’une modification ne se fusionne dans la branche principale.

Diagramme montrant le déplacement vers les tests décalés vers la gauche.

En plus de déplacer certaines responsabilités de test à gauche pour améliorer la qualité du code, les équipes peuvent déplacer d'autres aspects de test à droite, ou plus tard dans le cycle DevOps, pour améliorer le produit final. Pour plus d’informations, consultez Test en production par glissement vers la droite.

Écrire des tests au niveau le plus bas possible

Écrivez d’autres tests unitaires. Privilégiez les tests avec les dépendances externes les plus rares et concentrez-vous sur l’exécution de la plupart des tests dans le cadre de la build. Considérez un système de build parallèle qui peut exécuter des tests unitaires pour un assembly dès que l’assembly et les tests associés sont arrêtés. Il n’est pas possible de tester tous les aspects d’un service à ce niveau, mais le principe consiste à utiliser des tests unitaires plus légers s’ils peuvent produire les mêmes résultats que les tests fonctionnels plus lourds.

Objectif de fiabilité des tests

Un test non fiable est coûteux pour l’organisation à maintenir. Un tel test fonctionne directement contre l’objectif d’efficacité de l’ingénierie en rendant difficile d’apporter des modifications avec confiance. Les développeurs doivent pouvoir apporter des modifications n’importe où et gagner rapidement en confiance que rien n’a été rompu. Maintenez une barre haute pour la fiabilité. Découragez l’utilisation des tests d’interface utilisateur, car ils ont tendance à être peu fiables.

Écrire des tests fonctionnels qui peuvent s’exécuter n’importe où

Les tests peuvent utiliser des points d’intégration spécialisés conçus spécifiquement pour activer les tests. Une des raisons de cette pratique est un manque de testabilité dans le produit lui-même. Malheureusement, les tests comme ceux-ci dépendent souvent des connaissances internes et utilisent des détails d’implémentation qui n’ont pas d’importance du point de vue des tests fonctionnels. Ces tests sont limités aux environnements qui ont les secrets et la configuration nécessaires pour exécuter les tests, ce qui exclut généralement les déploiements de production. Les tests fonctionnels doivent utiliser uniquement l’API publique du produit.

Concevoir des produits pour la testabilité

Les organisations dans un processus DevOps à maturité adoptent une vision globale de ce que cela signifie de livrer un produit de qualité avec une cadence de cloud. Le déplacement de l'équilibre fortement en faveur des tests unitaires par rapport aux tests fonctionnels nécessite que les équipes prennent des décisions de conception et de mise en œuvre qui favorisent la testabilité. Il existe différentes idées sur ce qui constitue du code bien conçu et bien implémenté pour la testabilité, tout comme il existe différents styles de codage. Le principe est que la conception pour la testabilité doit devenir une partie principale de la discussion sur la conception et la qualité du code.

Traiter le code de test comme code de produit

En indiquant explicitement que le code de test est le code de produit, il est clair que la qualité du code de test est aussi importante pour l’expédition que celle du code de produit. Les équipes doivent traiter le code de test de la même façon qu’elles traitent le code logiciel et doivent appliquer le même soin à la conception et à l’implémentation des tests et des frameworks de test. Cet effort est similaire à la gestion de la configuration et de l’infrastructure en tant que code. Pour être complète, une révision de code doit prendre en compte le code de test et le soumettre au même niveau de qualité que le code du produit.

Utiliser l’infrastructure de test partagée

Réduisez la barre d’utilisation de l’infrastructure de test pour générer des signaux de qualité approuvés. Affichez les tests en tant que service partagé pour l’ensemble de l’équipe. Stockez le code de test unitaire en même temps que le code de produit et générez-le avec le produit. Les tests qui s’exécutent dans le cadre du processus de génération doivent également s’exécuter sous des outils de développement tels qu’Azure DevOps. Si les tests peuvent s’exécuter dans chaque environnement du développement local via la production, ils ont la même fiabilité que le code de produit.

Rendre les propriétaires de code responsables des tests

Le code de test doit résider en regard du code de produit dans un dépôt. Pour que le code soit testé aux limites des composants, poussez la responsabilité des tests vers la personne qui écrit le code du composant. Ne vous fiez pas à d’autres personnes pour tester le composant.

Étude de cas : Déplacement vers la gauche avec des tests unitaires

Une équipe Microsoft a décidé de remplacer ses suites de tests existantes par des tests unitaires de DevOps modernes et une approche de développement anticipée. L'équipe a suivi les progrès des sprints de trois semaines, comme illustré dans le graphique suivant. Le graphique couvre les sprints 78-120, qui représentent 42 sprints sur 126 semaines, ou environ deux et demi ans d’effort.

L’équipe a commencé avec 27K tests hérités au sprint 78, et a atteint zéro test hérité à S120. Un ensemble de tests unitaires L0 et L1 a remplacé la plupart des anciens tests fonctionnels. Les nouveaux tests L2 ont remplacé certains des tests, et de nombreux anciens tests ont été supprimés.

Diagramme montrant un exemple de solde de portefeuille de test au fil du temps.

Dans un parcours logiciel qui prend plus de deux ans, il y a beaucoup à apprendre du processus lui-même. Dans l’ensemble, l’effort visant à rétablir complètement le système de test sur deux ans a été un investissement massif. Toutes les équipes de fonctionnalités n’ont pas effectué le travail en même temps. De nombreuses équipes de l’organisation ont investi du temps dans chaque sprint, et dans certains sprints, c’était la plupart de ce que l’équipe a fait. Bien qu’il soit difficile de mesurer le coût du changement, il s’agissait d’une exigence non négociée pour les objectifs de qualité et de performance de l’équipe.

Mise en route

Au début, l’équipe a laissé les anciens tests fonctionnels, appelés tests TRA, seuls. L’équipe voulait que les développeurs adhèrent à l’idée de rédiger des tests unitaires, en particulier pour les nouvelles fonctionnalités. L’accent a été mis sur la création aussi facile que possible de tests L0 et L1. L’équipe devait d’abord développer cette capacité et créer un élan.

Le graphique précédent montre le nombre de tests unitaires commençant à augmenter tôt, car l’équipe a vu l’avantage de la création de tests unitaires. Les tests unitaires étaient plus faciles à gérer, plus rapides à exécuter et avaient moins de défaillances. Il était facile d’obtenir le soutien pour exécuter tous les tests unitaires dans le flux de pull request.

L’équipe ne s’est pas concentrée sur l’écriture de nouveaux tests L2 jusqu’au sprint 101. Pendant ce temps, le nombre de tests TRA est passé de 27 000 à 14 000 entre le Sprint 78 et le Sprint 101. De nouveaux tests unitaires ont remplacé certains des tests TRA, mais beaucoup d’entre eux ont simplement été supprimés, en fonction de l’analyse d’équipe de leur utilité.

Les tests TRA ont passé de 2100 à 3800 au sprint 110, car d’autres tests ont été découverts dans l’arborescence source et ajoutés au graphique. Il s’est avéré que les tests étaient toujours en cours d’exécution, mais qu’ils n’étaient pas suivis correctement. Ce n’était pas une crise, mais il était important d’être honnête et réévalué si nécessaire.

Devenir plus rapide

Une fois que l’équipe avait un signal d’intégration continue (CI) qui était extrêmement rapide et fiable, il est devenu un indicateur approuvé pour la qualité des produits. La capture d’écran suivante montre la pull request et le pipeline d'intégration continue en action, ainsi que le temps nécessaire pour passer à travers diverses étapes.

Diagramme montrant le pull request et le pipeline CI continu en action.

Il faut environ 30 minutes pour passer de la pull request à la fusion, ce qui comprend l’exécution de 60 000 tests unitaires. De la fusion de code au build CI, cela prend environ 22 minutes. Le premier signal de qualité de CI, SelfTest, vient après environ une heure. Ensuite, la plupart du produit est testé avec la modification proposée. Dans les deux heures qui suivent la fusion vers SelfHost, l’ensemble du produit est testé et la modification est prête à passer en production.

Utilisation de métriques

L’équipe effectue le suivi d’un tableau de bord suivant l'exemple ci-dessous. À un niveau élevé, la carte de performance suit deux types de métriques : Santé ou dette, et vitesse.

Diagramme montrant un tableau de bord des métriques pour le suivi des tests.

Pour les métriques de santé du site en temps réel, l’équipe surveille le temps de détection, le temps d’atténuation et combien d’éléments de réparation l’équipe porte. Un élément de réparation désigne le travail que l’équipe identifie dans une rétrospective en temps réel du site pour empêcher les incidents similaires de se reproduire. Le tableau de bord indique également si les équipes terminent les éléments de réparation dans un délai raisonnable.

Pour les métriques de santé d’ingénierie, l’équipe effectue le suivi des bogues actifs par développeur. Si une équipe a plus de cinq bogues par développeur, l’équipe doit hiérarchiser la résolution de ces bogues avant le nouveau développement de fonctionnalités. L’équipe suit également les anomalies vieillissantes dans des catégories spéciales comme la sécurité.

Les métriques de vitesse d’ingénierie mesurent la vitesse dans différentes parties du pipeline d’intégration continue et de livraison continue (CI/CD). L’objectif global est d’augmenter la vitesse du pipeline DevOps : à partir d’une idée, d’obtenir le code en production et de recevoir des données des clients.

Étapes suivantes