Nos actualités

Et si les tests unitaires étaient l’ennemi de votre productivité ?

11 (1)

Dans le monde du développement logiciel, une vérité semble intouchable : il faut écrire des tests unitaires. Depuis des années, cette pratique est présentée comme une garantie de qualité, une protection contre les régressions, voire une marque de professionnalisme. Et si ce dogme était en réalité un piège, particulièrement dans le contexte des services backend ?

Rassurez vous, je ne vais pas vous dire de supprimer tous vos tests unitaires (enfin, pas tout de suite 😜).

Mais si vous avez déjà maudit un test unitaire à 1h du matin, restez avec moi, on va explorer pourquoi.

Imaginez : vous passez des heures à maintenir des tests unitaires qui cassent à chaque refactoring et bloquent les évolutions du code. C'est comme vérifier qu'un chauffeur suit un trajet GPS précis. S'il prend un raccourci pour éviter un bouchon, l'alarme sonne. Vous perdez alors votre temps à analyser une fausse alerte, au lieu de faire la seule chose qui compte : vérifier que le colis est arrivé à bon port.

La raison ? Les tests unitaires ne garantissent pas toujours la qualité — Pire, quand ils figent les interfaces et alourdissent chaque changement, ils deviennent un coût, pas un atout.

Alors, faut-il les abandonner ? Non. Mais il est temps de remettre en question leur place et de se concentrer sur ce qui compte : tester les comportements, pas les implémentations.

 

1. Pourquoi mettre en place des tests ?

 

Confiance dans les évolutions


Sans une suite de tests fiables, chaque modification, que ce soit l'ajout d'une fonctionnalité ou une simple correction, devient une opération à haut risque. La peur d'introduire une régression — c'est-à-dire de casser une fonctionnalité existante qui marchait très bien — peut paralyser les équipes et freiner considérablement les améliorations du logiciel.

À l'inverse, un ensemble de tests robustes agit comme un filet de sécurité. Après chaque changement, ils valident non seulement que la nouvelle logique fonctionne, mais surtout qu'aucune fonctionnalité existante n'a été cassée. Cette protection constante contre les régressions est ce qui permet aux équipes de modifier le code, de le refactoriser et de livrer de la valeur plus rapidement et avec sérénité.

 

Détection précoce des bugs : vérifier le câblage avant de fermer les murs


Les tests sont nos meilleurs alliés pour la détection précoce des bugs. Trouver un bug en production, c'est comme se rendre compte d'une erreur dans le câblage électrique après avoir posé et peint les cloisons. Pour corriger le problème, il faut tout casser, réparer le câblage, puis reconstruire le mur. Le coût et l'effort sont démultipliés. Les tests permettent de s'assurer que chaque circuit est fonctionnel avant de sceller les murs, c'est-à-dire de valider la logique de fond avant qu'elle ne soit intégrée et livrée aux utilisateurs.

L'impact financier de cette détection précoce est colossal. Des études, comme celles menées par le National Institute of Standards and Technology (NIST), démontrent que le coût de correction d'un bug augmente de manière exponentielle tout au long du cycle de vie logiciel. Selon ces travaux, corriger une erreur pendant la phase de maintenance peut coûter jusqu'à 100 fois plus cher durant la phase de maintenance. (https://www.functionize.com/blog/the-cost-of-finding-bugs-later-in-the-sdlc)
À plus grande échelle, l'impact est vertigineux : un rapport du Consortium for Information & Software Quality (CISQ) a estimé le coût de la mauvaise qualité logicielle aux États-Unis à 2,41 billions de dollars pour la seule année 2022 (https://ctofraction.com/blog/cost-of-software-production-bugs/).

 

Qualité perçue par les utilisateurs: l'expérience client avant tout


Un véritable indicateur de la qualité de votre logiciel est la perception de l'utilisateur final. L'élégance du code, la couverture de test ou la complexité de l'architecture sont invisibles et sans importance pour la personne qui utilise l'application. Son unique critère de jugement est simple et binaire : est-ce que le produit répond à mon besoin de manière fiable et sans friction ?

Chaque bug, chaque ralentissement, chaque message d'erreur est une rupture de la promesse que vous lui avez faite. C'est ici que les tests prennent tout leur sens. Ils sont le moyen le plus direct de vous assurer que l'expérience vécue par l'utilisateur est bien celle que vous avez conçue.

Mais si tout le monde s'accorde sur le pourquoi tester, le comment a été dominé pendant des années par une vision très spécifique.

 

2. La pyramide des tests : un modèle à interroger

 

Dans le vaste paysage des stratégies de test, un modèle règne en maître : la pyramide des tests. Érigée comme un principe fondamental, elle a guidé d'innombrables équipes vers ce qui était perçu comme la bonne pratique. Mais comme toute construction, il est essentiel de questionner ses fondations. Et si cette pyramide, si souvent vénérée, était devenue un dogme plutôt qu'un simple guide pragmatique ?

 

Origine et principe de la pyramide : un idéal de performance

Popularisée et formalisée par Mike Cohn en 2009 dans son livre "Succeeding with Agile", la pyramide des tests propose une vision hiérarchique de la stratégie de tests. Son principe est simple : une large base de tests unitaires, un niveau intermédiaire de tests d'intégration, et un sommet étroit de tests de bout en bout.

L'idée derrière cette répartition est de maximiser la vitesse de feedback et de minimiser les coûts.

C’est un peu comme si, pour construire une maison, on nous disait de passer 70% de notre temps à vérifier la qualité de chaque vis et clou individuellement, 25% à s'assurer que les murs tiennent, et 5% à vérifier si la maison est habitable.

Le cœur de notre critique se portera sur les tests unitaires. Nous allons les analyser mais remettre en question la base de la pyramide va créer un effet d'entraînement modifiant l'étage juste au-dessus, les tests d’intégrations.

 

Pourquoi ce modèle a dominé : la promesse de l’efficacité

Ce modèle a rapidement dominé les discours et les pratiques pour une raison simple : il promettait l'efficacité. Les tests unitaires étaient vantés pour leur rapidité d'exécution et leur faible coût apparent. Ils permettaient aux développeurs d'obtenir un feedback quasi instantané sur de petites portions de code, encourageant ainsi une boucle de développement agile et réactive. L'argument était puissant : plus on teste tôt et souvent à un niveau granulaire, moins les problèmes coûtent cher à corriger.

 

 

3. Tests unitaires : avantages et pièges

 

Attention, il est essentiel de ne pas jeter le bébé avec l'eau du bain. Les tests unitaires ont leur place et des mérites. Mais, comme toute pratique mal appliquée, ils peuvent se transformer en un piège pour la productivité.

 

Avantages des tests unitaires : La rapidité et la précision

 

Là où les tests unitaires excellent, c'est dans leur rapidité d'exécution et leur capacité à isoler les problèmes. Ils sont conçus pour tester de petites unités de code, comme une fonction ou une méthode, indépendamment des autres composants. Cela permet un feedback quasi instantané, essentiel pour le développement en continu.

Ils sont particulièrement utiles pour :

  • Les algorithmes :vérifier des calculs mathématiques ou des logiques complexes.
  • Les interfaces stables :tester des APIs internes ou des librairies dont l'implémentation est peu susceptible de changer. Pour ces "briques" fondamentales et pérennes, les tests unitaires offrent une excellente couverture à un coût de maintenance maîtrisé.

 

Inconvénients des tests unitaires : fragilité et faux sentiment de sécurité

 

Cependant, les tests unitaires cachent aussi des inconvénients majeurs, souvent ignorés au profit de la fameuse "couverture de code ».

  • Le piège du couplage : quand les tests figent le code
    Le principal écueil des tests unitaires, lorsqu'ils sont mal conçus, est leur couplage excessif à l'implémentation interne du code plutôt qu'à son comportement observable. Ils créent un "contrat invisible" avec les détails profonds du code (noms de variables, méthodes privées, structure interne des classes, etc.).
    Cette dépendance forte rend les tests extrêmement fragiles. Changer le moindre détail anodin dans l'implémentation – un renommage de méthode, une réorganisation de logique interne – devient alors un cauchemar de mise à jour des tests, même si le comportement final de l'application n'a pas changé.
    Le paradoxe est frappant : si un test casse au moindre refactoring, comment peut-on être assurer que le nouveau code fonctionne toujours comme attendu ?
  • L’illusion du « 100% de couverture de code ».
    Le plus pernicieux des pièges est peut-être le faux sentiment de sécurité que les tests unitaires procurent. Atteindre une couverture unitaire de 100% ne garantit absolument pas que votre application fonctionne correctement d'un point de vue fonctionnel ou utilisateur.
    C'est comme s'assurer que chaque pièce d'un moteur fonctionne parfaitement individuellement, sans jamais vérifier si l'ensemble démarre et roule.
    Les tests unitaires, par leur nature isolée, ne valident pas le comportement global attendu. L'idée reçue veut que ce rôle incombe aux tests de bout en bout (end-to-end) mais ces derniers arrivent bien plus tard dans le cycle de livraison et sont plus longs à exécuter.
    Attendre ces tests pour valider le comportement fonctionnel signifie un feedback plus tardif, ce qui ralentit la détection des problèmes critiques.
    Pour illustrer ce point, un test unitaire vérifiera qu'une fonction « calculerTaxes(prix) » donne le bon résultat isolément. Mais il ne dira rien si cette fonction n'est pas appelée au bon moment ou si des données invalides lui sont envoyées en amont. Le test unitaire n'a pas la visibilité sur ce comportement global essentiel.
  • La course aveugle à la couverture de code
    Une autre idée largement répandue est qu'il faudrait viser un pourcentage arbitraire et élevé de couverture de code des tests unitaires – souvent 70, 80, voire 90% – indépendamment du contexte du projet. Cette interprétation a conduit des équipes à privilégier la quantité de tests unitaires sur leur pertinence.
    Or, la couverture de code est devenue un objectif en soi, souvent poursuivi aveuglément, plutôt qu'un simple indicateur de la partie du code exécutée par les tests. Cette course aux chiffres a ainsi transformé un guide pratique en un dogme aveugle.

 

Et si, au lieu de nous concentrer sur la quantité de tests, nous nous préoccupions davantage de leur utilité et de leur valeur réelle ? Il est peut-être temps de repenser notre approche et de nous focaliser sur ce qui compte vraiment : le comportement de notre logiciel.

 

 

 

 

4. Tests de comportement : une alternative plus robuste ?

 

Face aux limites des tests unitaires, une approche plus pragmatique consiste à se concentrer sur les tests de comportement (aussi appelés tests de fonctionnalité). Plutôt que de vérifier des détails d'implémentation internes, cette méthode valide un scénario utilisateur du point de vue de l’application. L’idée est de passer d'une question : « Est-ce que mon code fait les choses bien ? » à la question qui a réellement de la valeur : « Est-ce que mon code fait la bonne chose ? »

Pourquoi ils résolvent les problèmes des tests unitaires :

  • Résilience au refactoring : Les tests de comportement sont par nature moins sensibles aux changements internes. Tant que le parcours utilisateur fonctionne comme attendu, peu importe que vous ayez renommé une méthode ou changé la structure d'une classe. C'est comme tester une voiture en vérifiant qu'elle accélère, freine et tourne, sans se soucier de savoir si le moteur est un V6 ou un moteur électrique.
  • Alignement avec la valeur métier : Par définition, ces tests valident une exigence fonctionnelle ou un parcours utilisateur. Chaque test qui réussit est une preuve directe que votre application délivre de la valeur. Leur nommage et leur structure décrivent une fonctionnalité, ce qui les rend compréhensibles par tous les membres de l'équipe, y compris les chefs de produit.

Le meilleur des deux mondes : l'agilité des tests unitaires, la confiance en plus

Un avantage historique des tests unitaires est leur capacité à être lancés rapidement en local et dans la chaîne d’intégration (CI) mais grâce à des outils comme les Testcontainers ou les dépendances en mémoire, les tests de comportement deviennent complètement autonomes. Ils conservent ainsi l'avantage crucial des tests unitaires : ils peuvent être exécutés de manière fiable et rapide aussi bien en local sur le poste du développeur que dans la CI.

Une nouvelle perspective sur la couverture de code

Cette capacité à tester des fonctionnalités entières de manière fiable transforme notre rapport à la couverture de code. On quitte le paradigme du « Je n'ai pas 80% de couverture, vite, un test ! » pour adopter une vision bien plus saine : « Si mes tests de comportement ne passent pas par ces lignes de code, c'est probablement du code mort, supprimons-le ! »

5. La place des tests de comportement : redéfinir le milieu de la pyramide

 

À ce stade, une question légitime se pose : si ces tests ne sont pas des tests unitaires, où se situent-ils dans la fameuse pyramide des tests ? Sont-ils des tests d'intégration ?

La réponse est oui, ils sont la meilleure expression de ce que la couche "intégration" aurait toujours dû être.

Le problème vient du terme "test d'intégration" lui-même, qui est devenu terriblement ambigu. Comme le souligne Martin Fowler, ce mot est utilisé pour tout et n'importe quoi, créant une confusion permanente.

Pour y voir plus clair, demandons aux copains :

  1. L'intention originelle de Mike Cohn

Quand Mike Cohn a dessiné sa pyramide, la couche intermédiaire n'était pas initialement nommée « tests d'intégration » mais « tests de service ». Cette appellation est bien plus parlante ! Son but était de décrire des tests qui valident une fonctionnalité complète ou un service de l'application, mais sans passer par l'interface utilisateur. Cela correspond exactement à nos tests de comportement : on appelle, par exemple, une API (le point d'entrée de notre service) et on vérifie le résultat, en faisant fi de l'implémentation interne.

  1. La clarification de Martin Fowler

Pour résoudre l'ambiguïté, Martin Fowler distingue deux types de tests d'intégration (https://martinfowler.com/bliki/IntegrationTest.html) :

  • Les tests d'intégration "larges" (Broad Integration Tests) :testent le flux à travers plusieurs services distincts pour vérifier leur collaboration. Ce sont des tests coûteux, fragiles et lents. Ce n'est pas ce dont nous parlons ici.
  • Les tests d'intégration "étroits" (Narrow Integration Tests) :vérifient la communication de notre code avec une seule dépendance externe (base de données, broker de messages, etc.).

Martin Fowler privilégie les tests d’intégration étroits pour gagner en vitesse et en simplicité qui correspond parfaitement à la définition des tests de comportement.

Ces derniers sont une version intelligente et pragmatique de ces tests "étroits". Ils intègrent les différentes couches de notre propre service (contrôleur, logique métier, accès aux données etc…) avec ses dépendances directes (une base de données, un broker de message, un serveur d’API etc…).

En résumé : les tests de comportement sont des véritables occupants du milieu de la pyramide. Ils intègrent les composants internes d'un seul service pour valider une fonctionnalité métier. Ils sont le juste équilibre parfait : plus fiables et orientés métier que les tests unitaires, mais bien plus rapides, stables et faciles à diagnostiquer que les tests de bout en bout qui traversent plusieurs services.

La conséquence : de la pyramide au losange

Si l'on suit cette logique jusqu'au bout, on aboutit à une conclusion : la forme même de la pyramide peut être remise en question.

La base large et massive de tests unitaires n'est plus l'objectif. Au contraire, la majorité de nos tests — et donc de notre confiance — se trouvera dans cette couche intermédiaire de tests de comportement. Les tests unitaires, eux, seront moins nombreux et réservés à des cas très spécifiques (algorithmes, logique pure, cas limites).

La pyramide se transforme alors en une nouvelle forme, souvent appelée le Diamant de Tests (Test Diamond) :

  • Une petite base de tests unitaires, très ciblé
  • Un large centre de tests de comportement, qui constituent le cœur de notre stratégie.
  • Un sommet très étroit de tests de bout en bout
  • Des tests manuels (si nécessaire).

5. Comment choisir entre tests unitaires et tests de comportement ?

 

La question n’est pas de savoir s'il faut tester, mais comment tester intelligemment. Abandonner une stratégie dogmatique ne signifie pas naviguer sans boussole. Il s'agit de remplacer une règle aveugle ("il faut 80% de tests unitaires") par un arbitrage conscient, basé sur la valeur et le coût de chaque test.

Alors, comment décider ? Voici quelques critères pour vous guider.

 

De la création du service…

 

Une approche pragmatique : le test unitaire comme échafaudage

Faut-il complètement bannir les tests unitaires du processus de création ? Pas nécessairement. Ils peuvent jouer un rôle crucial, mais temporaire : celui d'un échafaudage.

Imaginez que vous devez développer une nouvelle fonctionnalité, par exemple un endpoint d'API avec une logique métier complexe alors qu’aucun design applicatif ou configuration n’existe. Écrire d'emblée un test de comportement complet peut être difficile, car il vous faudrait avoir déjà mis en place le contrôleur, le service, le repository... Vous risqueriez de coder une grande partie de la fonctionnalité "à l'aveugle", sans aucun filet de sécurité.

C'est là que le test unitaire devient un excellent outil de développement :

  1. Développez le cœur métier, par exemple en TDD, pour valider la logique pure.
  2. Faire de même pour les couches techniques autour de ce cœur : d'abord l'API, puis la persistance.
  3. Écrivez le test de comportement qui valide le scénario complet, de l'appel API à la base de donné

 

Maintenant vient l'étape cruciale : une fois que ce test de comportement est en place, les tests unitaires initiaux ont rempli leur rôle. Ils deviennent redondants. Le test de comportement couvre déjà, de manière implicite mais bien plus robuste, la logique qu'ils vérifiaient.

À ce moment-là, il est souvent souhaitable de supprimer certains tests unitaires. Les conserver tous ne ferait qu'augmenter le coût de maintenance pour une valeur quasi nulle. Le test unitaire n'est plus un filet de sécurité permanent, mais un outil de construction que l'on retire une fois le mur solide.

 

… à l’évolution et la maintenance

 

Une fois le service en place, la perspective change. L'enjeu n'est plus de construire, mais de faire évoluer sans régression. L'arbitrage entre les tests devient alors une pratique quotidienne.

 

Critères de décision : une question de contexte

  1. Stabilité de l'implémentation vs stabilité du comportement
  • Quand choisir un test unitaire ? Pour une logique "pure", algorithmique et stable. Un test unitaire est parfait pour vérifier une fonction qui calcule une TVA, qui trie une liste ou qui valide un format d'email. Le comportement de ces "briques" ne changera probablement jamais, même si on optimise leur code interne.
  • Quand choisir un test de comportement ? Pour tout ce qui orchestre plusieurs composants. Un processus de connexion, un formulaire d'inscription ou un panier d'achat sont des chorégraphies de plusieurs briques. L'important est que la danse se déroule sans accroc pour l'utilisateur, même si les danseurs changent leurs pas.

 

  1. Valeur métier vs Proximité du code
  • Le test couvre-t-il un scénario utilisateur ? Si la réponse est oui, un test de comportement est presque toujours supérieur. Il valide directement que vous livrez de la valeur.
  • Le test vérifie-t-il un détail technique ? S'il s'agit de tester un cas limite d'une librairie interne ou la gestion d'une erreur très spécifique, un test unitaire peut être plus simple et plus direct.

 

 

Règle pratique : le temps comme juge de paix

En fin de compte, la meilleure heuristique est celle-ci : un bon test est un test qui vous fait gagner du temps, pas en perdre. Si vous passez plus de temps à maintenir et corriger votre suite de tests qu'à livrer de nouvelles fonctionnalités, votre stratégie est défaillante.

 

  • L'exemple concret : Imaginez un service pour créer une commande. Vous pourriez écrire 10 tests unitaires : un pour le contrôleur d'API (en moquant le service de commande), plusieurs pour le service lui-même (en moquant le repository des produits et celui des commandes), un autre pour la logique de calcul du prix, etc. Au moindre refactoring — comme changer la signature d'une méthode entre le service et le repository — la moitié de ces tests cassent, même si le comportement final reste parfaitement correct.
  • L'alternative robuste : Vous pourriez écrire un seul test de comportement (En tant que client je veux vérifier le statut de ma commande). Il envoie une requête HTTP à votre endpoint (POST /orders) avec des données valides, puis vérifie via une autre requête HTTP (GET /order/{id}) que la commande a bien été créée avec le bon statut et le bon prix total. Ce test unique couvre la fonctionnalité entière, est insensible au refactoring interne et apporte une confiance bien plus grande.

6. Conclusion : tester avec intention

 

Nous avons commencé cet article en posant une question provocatrice. Loin de vouloir bannir les tests unitaires, notre objectif était de briser une idole et de remettre l'intention au cœur de nos pratiques.

Les tests ne sont pas une fin en soi, mais un moyen. Un moyen d'obtenir de la confiance, de la sérénité et, in fine, de livrer un meilleur produit à nos utilisateurs. La pyramide de tests, avec sa base surdimensionnée de tests unitaires, nous a souvent fait perdre de vue cet objectif. En se concentrant sur des métriques de couverture de code vides de sens, on a fini par écrire des tests qui vérifient le code, mais pas le produit.

 

Les tests de comportement, en se focalisant sur les fonctionnalités et les parcours utilisateurs, nous ramènent à l'essentiel. Ils sont plus résiliants, plus proches de la valeur métier et offrent souvent un bien meilleur retour sur investissement. Les tests unitaires gardent bien sûr leur place pour des logiques algorithmiques précises et stables, mais ils ne devraient plus constituer la fondation aveugle de notre stratégie de qualité.

 

Il est temps de passer d'une approche quantitative à une approche qualitative.

 

Alors, la prochaine fois que vous écrirez un test, posez-vous la question : "Ce test est-il un simple garde-fou pour mon code, ou un véritable garant de la promesse faite à mon utilisateur ?".

 

La qualité de votre logiciel dépend moins du nombre de tests… que de leur pertinence !

 

 

Technologies associées

Désolé, aucun contenu trouvé.