Introduction à TDD en Swift (Partie 3) - Une bonne documentation
10/02/2020 · Tutoriel (14 minutes)
Versions : Swift 5, iOS 12.2, Xcode 10.2.1, AppCode 2019.1.4
Cet article fait partie de la série "Introduction à TDD en Swift"
- Introduction à TDD en Swift (Partie 1) - 7 étapes essentielles
- Introduction à TDD en Swift (Partie 2) - Vive le typage et la généralisation
- Introduction à TDD en Swift (Partie 3) - Une bonne documentation
Dans le précédent article de cette série tu as appris les bienfaits de la pratique de TDD :
- les tests sont fiables et nous permettent de ne rien casser,
- les tests nous poussent à raisoner plus profondément, à avoir un code plus robuste,
- les tests sont plus importants que le code de production.
Nous allons continuer aujourd’hui le kata FizzBuzz en implémentant plusieurs nouveaux tests en TDD.
Au programme :
- pourquoi une suite de tests doit constituer la meilleure documentation possible,
- je vais déroger au cycle RED-GREEN-REFACTOR (crime crime ! 😂),
- tu vas avoir un petit aperçu du property-based testing (wat?),
- et enfin on va utiliser le résultat d’un test pour écrire un test (waaaat?).
Alors, prêt(e) ?
Règle métier : “Pour les nombres non multiples de 3
ou 5
, affiche le nombre”
Je viens de me rendre compte que ma checklist ne reflète pas vraiment les règles métiers, je la mets donc à jour.
Pour les nombres non multiples de 3 ou 5 , affiche le nombre |
3 -> [1, 2, Fizz] |
Pour les multiples de 3 , affiche Fizz au lieu du nombre |
5 -> last == Buzz |
Pour les multiples de 5 , affiche Buzz au lieu du nombre |
15 -> last == FizzBuzz |
Pour les multiples de 15 , affiche FizzBuzz au lieu du nombre |
Afficher les nombres de 1 à 100 |
Voilà aussi une belle illustration des bienfaits de TDD.
Pour écrire de bons tests, je dois capture fidèlement le besoin.
Mon envie d’avoir une bonne suite de tests, une bonne documentation me pousse à me concentrer sur le besoin.
Souvent, en écrivant des tests, je me rends compte qu’il me manque des informations.
Je découvre des cas d’usage (des cas d’erreur la plupart du temps), pour lesquels j’ai besoin de questionner les utilisateurs & utilisatrices (ou le Product Owner).
Révisons nos deux derniers tests :
test_FizzBuzz_up_to_1_is_a_list_containing_1_as_string
test_FizzBuzz_up_to_2_is_a_list_containing_1_and_2_as_string
Ce sont deux tests qui sont des exemples vérifiant la première règle métier : “Pour les nombres non multiples de 3
ou 5
, affiche le nombre”.
Dans un soucis de bonne documentation (je me répète mais c’est important !), ces deux tests me dérangent car ils ne communiquent pas efficacement la règle métier.
Comment pourrions-nous nommer un test unique qui communique efficacement cette règle métier ?
…
…
…
Hum, que penses-tu de
test_Numbers_not_multiple_of_3_or_5_are_displayed_as_is
?
Ça me semble parfait, bien joué !
Quel serait le contenu de cette méthode de test ?
Pourquoi pas ceci :
Pas mal…
Le problème c’est qu’on ne gère que les nombres jusqu’à 2
.
C’est un test assez limité.
Ok, alors ceci peut-être ?
Là on teste jusqu’à 10
, c’est mieux ; le problème c’est que les prochaines règles métiers vont faire échouer ce test.
Or on ne veut pas écrire de tests “faux”.
Il serait pertinent, dans un premier temps, de vérifier que certains nombres sont bien affichés tels quels.
Que penses-tu ce ceci ?
Mouais…ça va nous faire un test long comme le bras ça ! 😕 Et tu ne vérifies pas que la liste retournée est bonne, juste le dernier élément.
Tu as raison…
Pour ta deuxième remarque j’ajoute un test à ma liste : FizzBuzz jusqu’à un certain nombre retourne une liste de la taille de ce nombre.
Concernant ta première remarque, nous pouvons généraliser en faisant une boucle avec nos exemples.
Comme ceci :
Excellent !
Oui ça me plaît assez…
Mais j’ai l’impression de réécrire l’algo dans mes tests !
En effet je transforme un nombre en String
("\(n)"
) comme dans mon code de prod.
Ce n’est pas top car cela introduit de la duplication et du couplage entre les tests et le code de prod !
Modifions le test pour éviter cela :
Beaucoup mieux !
Passons maintenant au refactoring.
Je vois plusieurs duplications :
- il y a des tests redondants,
- il y a de la duplication dans les deux méthodes d’assertions.
Commençons par le premier point, on peut supprimer les tests suivants :
test_FizzBuzz_up_to_1_is_a_list_containing_1_as_string
test_FizzBuzz_up_to_2_is_a_list_containing_1_and_2_as_string
Et enfin le deuxième, on crée une nouvelle méthode pour factoriser le code similaire dans nos deux assertions :
Done !
Le fait de tester le comportement de FizzBuzz
avec des valeurs prédéfinies est une approche basée sur l’exemple.
Elle présente l’avantage d’être simple à mettre en œuvre et suffit la plupart du temps.
L’inconvénient est que sur certains algorithmes, nous pouvons difficilement couvrir tous les exemples efficacement. Un de ces algorithmes est celui du kata Roman Numerals où il faut transformer des nombres romains en nombres arabes.
Autre inconvénient : cette approche n’offre pas de “preuve” au sens mathématique. Nos tests prouvent uniquement que notre programme fonctionne pour les exemples choisis.
Pour aller plus loin, tu peux aller fouiller du côté de l’approche basée sur les propriétés : le property-based testing. Pour cela, nous utilisons notamment des outils comme SwiftCheck. Je prévois de réviser FizzBuzz
en utilisant SwiftCheck dans un prochain article, pour ne pas le manquer, inscris-toi à la newsletter !
Propriété : la taille de la liste
J’ai ajouté à ma liste le test suivant : FizzBuzz jusqu’à un certain nombre retourne une liste de la taille de ce nombre.
Il est maintenant temps de l’implémenter !
Là nous gérons le cas 0
, allons plus loin en restant dans l’approche par l’exemple pour tester jusqu’à 100.
J’ai quand même une interrogation par rapport à TDD…
Je t’écoute.
En écrivant ce test, nous ne sommes pas passés par la phase RED. C’est encore du TDD du coup ?
Très bonne remarque !
Pourquoi est-il utile de passer par la phase RED ?
Pour vérifier que notre test est utile et s’assurer que son échec donne assez d’informations pour nous aider à le faire passer à nouveau.
Exactement !
Je prends le risque ici d’écrire un test inutile et je ne vérifie pas les informations liées à son éventuel échec.
Les tests servent à vérifier la non-régression et à guider la conception.
Ils servent aussi de documentation des comportements attendus !
Je souhaite spécifier les comportements attendus ! Un des comportements que j’attends est que FizzBuzz
me retourne une bonne liste de String
donc il me faut un test pour le spécifier.
Cela suffit à justifier l’utilité de ce test.
Concernant le message d’erreur, je peux forcer l’échec de ce test en modifiant le code de production comme ceci :
J’obtiens 98 erreurs de la forme : -[TDDFizzBuzzTests.FizzBuzz_Spec test_Result_list_is_of_the_same_size_as_requested_upper_bound] : XCTAssertEqual failed: ("2") is not equal to ("1")
.
Je peux facilement déduire, à partir du nom du test et du message, que le problème se situe au niveau de la taille de la liste.
Remettons le code de production en état pour refaire passer le test :
Ce n'est pas parcequ'un test ne nous fait pas écrire du code de prod qu'il est inutile. Un test est là pour spécifier un comportement et sert à la documentation du code.
Deuxième règle métier : les multiples de 3 donnent “Fizz”
Au regard des tests existants, j’ai mis à jour ma liste :
Pour les multiples de 3 , affiche Fizz au lieu du nombre |
Pour les multiples de 5 , affiche Buzz au lieu du nombre |
Pour les multiples de 15 , affiche FizzBuzz au lieu du nombre |
Afficher les nombres de 1 à 100 |
J’ai supprimé des exemples comme “3 -> [1, 2, Fizz]” car ils n’apportent rien en terme de documentation.
J’accélère en passant directement aux règles métiers.
C’est parti pour le test de notre deuxième règle métier :
Nous pourrions y aller par plus petites étapes en testant d’abord 3
, puis 6
, etc. Mais je me sens en confiance pour accélérer un peu. Je sais que si jamais je n’y arrive pas, je pourrais ralentir.
Je vais en GREEN le plus vite possible :
Et je passe au REFACTORING.
La méthode upTo
commence à devenir assez longue, c’est le smell Long Method.
Comme traitement j’utilise Extract Method.
J’aimerai aussi rendre plus explicite la condition pour vraiment faire ressortir la règle métier “multiple de 3”.
On pourrait ajouter un commentaire au dessus du if
mais les commentaires sont le signe que le code peut être amélioré.
Là aussi, un Extract Method à la rescousse !
Je pense encore pouvoir amélioré la lisibilité du code en utilisant une super feature de Swift : les extensions !
Là je suis content ! Passons au test suivant !
Troisième règle métier : les multiples de 5 donnent “Buzz”
Pour les multiples de 5 , affiche Buzz au lieu du nombre |
Pour les multiples de 15 , affiche FizzBuzz au lieu du nombre |
Afficher les nombres de 1 à 100 |
Le test :
RED !
Je le fais passer :
GREEN !
Et maintenant REFACTORING !
D’abord le code de prod :
Et enfin les tests, car je vois un pattern qui se répète :
- une liste d’exemples,
- je parcours la liste,
- pour chaque élément je vérifie que le résultat termine par une
string
donnée.
Voilà ce que ça donne :
Dernière règle métier : les multiples de 3 et 5 donnent “FizzBuzz”
Pour les multiples de 15 , affiche FizzBuzz au lieu du nombre |
Afficher les nombres de 1 à 100 |
Nous y sommes presque !
Ouiii !! Mais ne crions pas victoire trop vite, il nous reste 2 tests à implémenter.
Commençons avec notre dernière règle métier.
En route pour GREEN !
❌ FAIL
Oups, quoi ?
Héhé, il faut mettre le troisème
if
avant les deux autres !
Ah mais oui, suis-je bête ? Merci !
Heureusement qu’il y a les tests quand j’ai un coup de mou !
Comme ça c’est mieux :
REFACTORING !
Je trouve qu’il y a deux tests qui ne sont pas assez explicites.
test_Multiples_of_3_are_displayed_as_Fizz
et test_Multiples_of_5_are_displayed_as_Buzz
.
En effet, je m’attends à voir des multiples de 3 dans le premier, or je ne vois pas 15, on passe de 12 à 18.
Et je m’attends à voir des multiples de 5 dans le deuxième, or je ne vois pas 15 non plus, on passe de 10 à 20.
Le nom des tests ne reflète pas exactement le besoin, il faut donc les renommer.
Comme ceci :
Test d’acceptance : FizzBuzz de 1 à 100
Afficher les nombres de 1 à 100 |
Bon, je ne sais pas toi mais écrire les valeurs d’exemples pour ce test à la main m’ennuie.
Tu as confiance dans nos tests ?
Oui !
Et si on laissait ce dernier test échouer en nous donnant les valeurs ?
Ensuite on n’aura plus qu’à copier le resultat donnée par le test dans notre test et le tour est joué !
Genius ! 🤯
❌ FAIL
Yeah ! Haha ! J’ai beaucoup trop de fun à faire ça. 🤣
Et je copie les valeurs dans mon test :
✅ SUCCESS
Tadaaaa ! 🎉
Conclusion
Voilà qui achève cette belle introduction à TDD en Swift.
J’espère que cela t’a aidé à apprécier cette pratique qui change mon quotidien depuis 2016 et qui m’apporte une grande sérénité.
Tu as appris dans ce dernier article que :
- pour écrire de bons tests, il faut capturer fidèlement le besoin,
- ce n’est pas parcequ’un test ne nous fait pas écrire du code de prod qu’il est inutile,
- un test est avant tout là pour spécifier un comportement et sert à la documentation du code.
Si tu veux aller plus loin et plus vite dans ta maîtrise de cette pratique tu peux faire appel à moi directement !
Pour télécharger le projet finalisé, c’est par ici !
Si tu as apprécié cette série sur TDD, n’hésite pas à t’inscrire à la newsletter ci-dessous pour ne rien manquer des prochains articles. 👇
J'ai écrit cet article pendant 7h et j'ai eu 2 "ah-ah moment" sous la douche ou en pleine nuit. Tu as aimé cet article ? Pourquoi ne pas le partager avec un(e) ami(e) ou sur les réseaux sociaux ?! ❤️
Envoyer un commentaire