Un test peut en cacher un autre – Tests unitaires – P1

Introduction

L’article d’introduction débute en listant certaines différences entre ma vision en terme d’architecture applicative ou encore de rédaction des tests, que je peux avoir avec d’autres développeurs. À travers elles, j’évoque les difficultés qu’ils peuvent rencontrer à identifier précisément quoi tester et comment.

Deux phrases extraites de l’article de Ian Cooper ont été mises en avant :

  • Le code issu d’un refactoring ne requiert pas de nouveaux tests
  • Je vous recommande d’utiliser ports/adaptateurs et d’écrire les tests en outside-in depuis le use case

Ces deux axes sont à mon sens essentiels. Nous allons essayer de les décortiquer un par un, à travers des exemples, pour en comprendre leur essence et en quoi ils sont si importants pour faciliter le quotidien de l’équipe de développement.

Ici, nous débuterons par le premier axe : “Le code issu d’un refactoring ne requiert pas de nouveaux tests” en illustrant ce qu’il implique dans l’écriture des tests.


Kata

On va travailler avec un Kata qui sera la gestion d’un système de réservation de livres dans une librairie, les règles sont les suivantes :

  • Un client peut ajouter des livres dans son panier
  • Le client peut visualiser les informations du panier (prix et livres ajoutés)
  • Le panier est sauvegardé
  • Tous les livres coûtent 8€
  • Le prix du panier varie en fonction du nombre de livres identiques ajoutés :
    • 2 livres identiques donne une réduction de 5% de la somme du prix des 2 livres
    • 3 livres et plus identiques donne une réduction de 25% de la somme du prix des n livres
    • Exemple pour l’achat de :
      • 2 fois le livre 1
      • 3 fois le livre 2
      • 4 fois le livre 3
      • 1 fois le livre 4
      • 1 fois le livre 5
      • Le montant du panier est égale à 73,20€
  • Quand le client emprunte des livres :
    • Les livres ne sont plus disponibles (stock de 1)
    • Il reçoit un SMS
    • Il reçoit un email
  • Quand le client retourne des livres :
    • Les livres sont disponibles
    • Il reçoit un email

NDR : le code est en TypeScript qui propose un typage statique fort. Néanmoins, le code peut être transposé dans des langages comme JavaScript, Python ou PHP qui qui ont nativement un typage dynamique et faible pour certains.


Les tests unitaires (TU)

Nous allons illustrer comment dans le code la phrase : Le code issu d’un refactoring ne requiert pas de nouveaux tests! peut s’appliquer.

Après plusieurs cycle TDD, les tests suivants ont émergé :

Cela a permis de construire la méthode privée applyDiscount de la classe Basket.ts :

Ici, on pourrait y voir plusieurs violations :

  • Du principe de responsabilité unique (SRP) : en effet cette méthode détermine la réduction possible en fonction du nombre de livres empruntés ET l’applique au prix.
    De plus, on connaît le détails du calcul pour chaque cas possible.
  • Du principe d’ouverture/fermeture (OCP) : cette méthode est fermée à l’extension, si une nouvelle réduction apparaît, il faudra modifier la classe Basket.ts ce qui n’a pas de sens.

Pour éviter ces deux violations on va remanier (refactoring) le code pour arriver à une nouvelle possibilité d’implémentation (ce n’est pas la seule possible).

Nouvelle méthode applyDiscount de la classe Basket.ts :

La suite de tests est restée inchangée, il y a aucun nouveau test pour vérifier les nouvelles classes créées via le refactoring. Elles sont forcément couvertes par ceux existants, dans le cas contraire les tests seraient devenus rouges.


Conclusion

Un TU doit tester unitairement un comportement/une intention utilisateur et non une implémentation possible. Le nom du TU doit également le refléter.

De ce fait une classe ou un fichier de code de production n’équivaut pas à un fichier de tests unitaires.

Unitaire désigne une unité de comportement du système et non une unité de code.

Plusieurs symptômes peuvent indiquer quand nous sortons de cette conclusion :

  • Les tests sont plus durs à maintenir :
    • Multiplication des fichiers de tests
    • Plusieurs tests qui vont vérifier exactement la même chose
    • Gestion des tests potentiellement obsolètes et en refaire d’autres quand on change l’implémentation
    • Nom des tests qui doivent changer pour chaque implémentation choisie
  • Les noms des tests reflètent moins les intentions du code, on perd le contexte de l’intention métier que l’on développe.
    Exemple : vérifier que j’ai 25% de remise quand je fait : new ThreeOrMoreSameBooksDiscountCalculator().applyDiscountOn(100)
    Pourquoi ? Il manque le contexte général
  • Les tests sont plus durs à faire évoluer :
    • Révélation de l’implémentation choisie
    • Difficultés à changer de design et refactoriser. Beaucoup de tests peuvent virer au rouge alors que potentiellement ils auraient dû rester tous verts.

Le dernier est certainement l’un des plus importants, car nous pratiquons TDD notamment pour avoir cette puissance pour refactorer la structure profonde du code de manière totalement sécurisée.

Résumé en une image :

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *


Ce formulaire est protégé par Google Recaptcha