Accueil / Articles / Au-delà du pourcentage de couverture : tester les pannes auxquelles les utilisateurs se heurtent réellement

Au-delà du pourcentage de couverture : tester les pannes auxquelles les utilisateurs se heurtent réellement

Pourquoi un chiffre élevé de couverture du code peut cacher des modes de défaillance non testés, les trois types de tests inutiles qu’il encourage, et comment écrire des tests qui protègent le véritable comportement du logiciel.

920 mots

Une demande de fusion avec tous les tests réussis et un rapport de couverture supérieur à 98 % semble sécurisée. Pourtant, une API renvoie null alors que l’interface attendait un objet, une requête est traitée dans le mauvais ordre en raison d’une connexion mobile lente, ou quelqu’un double-clique sur le bouton de soumission, ce qui fait que l’interface devient vide. La question posée après analyse est toujours la même : comment cela a-t-il pu casser alors que le fichier était couvert ? Cet article explique ce que mesure réellement la couverture, les schémas de tests qui l’augmentent sans apporter de protection, et comment orienter son ensemble de tests vers les échecs qui comptent vraiment.

Ce que mesure la couverture, et ce qu’elle ne mesure pas

Outil de couverture : il enregistre quelles lignes, branches et fonctions ont été exécutées pendant l’exécution de vos tests. C’est tout. Il ne sait pas si une assertion a vérifié le résultat, si les données d’entrée ressemblaient à du trafic réel, ou si le code se comporte correctement en cas de dysfonctionnement d’une dépendance. Exécuté et vérifié sont deux propriétés distinctes, et la couverture ne rapporte que la première.

Une analogie utile est l’inspection d’un bâtiment, où quelqu’un parcourt chaque pièce avec une lampe de poche. Chaque pièce a été visitée, mais personne n’a testé le toit en cas de tempête. Un taux de couverture élevé indique que vos tests ont consulté le code, mais pas qu’ils l’ont mis à l’épreuve.

Trois schémas qui gonflent la couverture sans ajouter de sécurité

Lorsqu’un pourcentage devient l’objectif, les gens optimisent en fonction de ce pourcentage. Le résultat sont des tests qui satisfont l’outil avec le moins d’effort possible. Trois schémas réapparaissent sans cesse.

Le mirage du scénario idéal

Imaginez un outil qui analyse les entrées de l’utilisateur et met à jour l’état. Son test fonctionne avec une chaîne de caractères propre et bien formatée, vérifie la sortie attendue et atteint une couverture de branches complète. Ce qu’il n’essaie jamais, c’est d’utiliser une chaîne vide, des caractères inhabituels, un argument undefined, une réponse lente ou du JSON mal formaté. Chaque ligne de code s’exécute, mais aucune des entrées susceptibles de provoquer des incidents en production n’est testée.

Le composant trop mocké

Ici, chaque appel API, fournisseur de contexte et élément enfant imbriqué est remplacé par un faux objet. Le test s’achève en quelques millisecondes et couvre tous les scénarios de rendu. En production, cependant, le vrai point d’entrée renvoie une structure légèrement différente de celle prévue par le mock, ou une bibliothèque modifie la manière dont elle émet des événements après mise à jour. Les tests continuent de passer car ils ne prennent en compte que vos propres hypothèses, et la première intégration réelle a lieu dans le navigateur de l’utilisateur.

Tests sans assertions significatives

La forme la plus faible se manifeste sous des exigences strictes, comme un seuil obligatoire de 90 % au sein de l’équipe. Les tests appellent des fonctions uniquement pour enregistrer les exécutions et ne font que peu ou pas d’assertions. Le rapport devient vert sans qu’aucune vérification ne protège le code des futures modifications logicielles.

Comment tester le comportement plutôt que le nombre de lignes

Les tests qui détectent les bugs avant les utilisateurs se concentrent sur ce que fait le système, et non sur les lignes de code qu’il touche.

  • Testez les états et les transitions, pas les fonctions individuelles. Les utilisateurs se moquent du fait qu’un outil d’aide ait été exécuté. Ce qui les intéresse, c’est ce qui se passe lorsque le réseau tombe au milieu de la soumission d’un formulaire, ou lorsqu’ils tentent de quitter la page alors que le téléchargement d’un fichier est encore en cours. Vérifiez les indicateurs de chargement, les états d’erreur, les tentatives de réessai et les limites en cas d’erreur.
  • Préférez l’intégration lorsque c’est pratique. Simuler de vraies frontières externes, comme une passerelle de paiement tierce, est raisonnable. Simuler vos propres outils internes ou votre propre couche de données cache principalement les bugs qui existent entre eux. Laissez les composants et modules fonctionner ensemble autant que le coût le permet.
  • Laissez les bugs de production enrichir le jeu de tests. Lorsqu’un défaut échappe, écrivez un test qui le reproduit et échoue avant d’intervenir pour le corriger, puis modifiez le code jusqu’à ce que le test passe. Avec le temps, le jeu de tests accumule les cas limites que votre système rencontre réellement plutôt que ceux imaginés par quelqu’un d’autre.
  • Une technique apparentée et bien établie est le test de mutation, qui modifie délibérément votre code (en inversant une condition, en supprimant une ligne) et vérifie si un test échoue. Les mutations qui survivent indiquent directement les parties du code qui sont exécutées mais pas vérifiées, ce que la couverture ne peut pas révéler. Pour des détails au niveau des composants, consultez ces anti-modèles de test React qui créent une fausse confiance.

    Utiliser la couverture comme signal, non comme objectif

    Le taux de couverture n’est pas inutile. Un faible pourcentage dans un module critique constitue un avertissement réel, et un rapport peut révéler des chemins de code qui ne sont jamais testés. Le problème commence lorsque le pourcentage devient l’objectif, car cela favorise la quantité au détriment de la rigueur.

    Un ensemble de tests efficace avec un taux de couverture d’environ 65 %, axé sur les workflows à risque, les règles métier complexes et la récupération en cas de panne, permettra d’éviter bien plus d’incidents qu’un ensemble fragile à 95 %, basé uniquement sur des scénarios sans risque et des simulations. Avant d’ajouter un test, une meilleure question que « quelles lignes ne sont pas couvertes ? » est « quel échec réel ce test permettrait-il de détecter ? »

    Points clés

    • Le taux de couverture indique quel code a été exécuté, mais pas si son comportement a été vérifié.
    • Les entrées correspondant à des scénarios sans risque, les simulations internes abondantes et les tests sans assertions augmentent tous le taux de couverture sans réduire les risques.
  • Transitions d’état cibles, gestion des erreurs et intégration entre vos propres modules.
  • Transformez d’abord chaque bug en un test qui échoue, puis corrigez-le.
  • Surveillez les modes de défaillance contre lesquels votre suite de tests protège, et considérez le taux de couverture comme un indice complémentaire.
  • Lectures associées