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

le 10/03/2020 par Mickael Wegerich
Tags: Software Engineering

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.

Dans cette article : Un test peut en cacher un autre — Tests unitaires — P2, nous nous attardons davantage sur le second axe : “Je vous recommande d’utiliser ports/adaptateurs et d’écrire les tests en outside-in depuis le use case.”.


Kata

On va travailler avec un Kata qui sera la gestion d’un système de réservation de livres dans une librairie, dont 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)
    • Un SMS est envoyé
    • Un email est envoyé
  • Quand le client retourne des livres :
    • Les livres sont disponibles
    • Un email est envoyé

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 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é :

https://gist.github.com/mickaelw/07fa70b57b8df2a7d232be7bd7bd9056

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

https://gist.github.com/mickaelw/8c3114a8592ed35aeca9bbd7c23a7e30

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 (r_efactoring_) 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 :

https://gist.github.com/mickaelw/4bc3b4c8aa186af99e195d43c7e51173

https://gist.github.com/mickaelw/1be81c0344d007d00fce08d7f0967953

https://gist.github.com/mickaelw/0cb36222116fa457cde7ffd8804264ed

https://gist.github.com/mickaelw/84cd2d64e7fbb60c1a1e71c801b2037b

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, ce qui permet d’avoir une documentation fonctionnelle.

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 :