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.
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.
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.
Lectures associées
- Au-delà de la taille du bundle : Découvrir ce qui réellement ralentit votre application web — Pourquoi réduire les kilooctets ne résout que rarement un problème de lenteur, et comment tracer le temps réel d’attente à travers les serveurs, les processus en cascade, l’hydratation des données, les scripts et images de tiers.
- Au-delà du P95 : Mesurer la latence que vos utilisateurs expérimentent réellement — Pourquoi un P95 sain peut coexister avec un produit lent, comment le temps d’attente dans la file et les effets en chaîne échappent aux tableaux de bord, et comment le suivi du temps par étape met fin aux accusations concernant la latence.