Introduction à TDD en Swift (Partie 2) - Vive le typage et la généralisation
17/12/2019 · Tutoriel (10 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 bases de TDD en faisant un cycle complet de 7 étapes.
Nous allons continuer aujourd’hui le kata FizzBuzz en implémentant plusieurs nouveaux tests en TDD.
Cela te permettra de mieux comprendre la profondeur et l’intérêt de cette pratique qui a changé ma manière de travailler.
Au programme :
- trois techniques de refactoring pour améliorer la qualité du code,
- l’utilisation du système de type pour éviter l’écriture d’un test,
- des propriétés intéressantes du TDD, au-delà des tests en eux-même,
- du fun comme jamais ! (Ok ça c’est peut-être un peu exagéré ! 😂)
Alors, prêt(e) ?
Et le cycle recommence !
Les tests : plus importants que le code de prod ?
Modifie FizzBuzz_Spec.swift pour y ajouter ce test :
Lance les tests. Erreur de compilation !
Ajoute la méthode upTo
à FizzBuzz
.
Relance les tests. Échec à nouveau, quelle tristesse !
Non, un test qui échoue c’est du progrès !
Aller, on ne se décourage pas, on fait passer le test le plus vite possible !
Met à jour la méthode upTo
:
Et relance les tests une nouvelle fois (tu comprends maintenant pourquoi ils doivent être rapide !).
Ils passent !
Youpi ! Test suivant !
Hum hum…
…je voulais dire : refactoring !
Ah ! Je préfère ça !
Quels crimes avons-nous commis ?
J’allais te le demander !
Je regarde le code dans
FizzBuzz
, ça m’a l’air très bien, je ne vois pas quoi améliorer…
Et dans les tests ?
Quoi ? On doit aussi refactorer les tests ?
Et comment ! C’est encore plus important que le code de production !
Les tests sont-ils bien conçus selon toi ?
Y’a-t-il de la duplication ?
…
…
…
Oh c’est difficile, je ne sais pas !
Ok ok je vais t’aider !
Il y a un test qui est devenu inutile…
…notre test “marche-pied” !
Tu peux donc le supprimer, il s’agit de la méthode test_Creation
.
Whoop! Supprimé!
Ensuite on met à jour la liste !
Exact !
-1 -> [] |
1 -> [1] |
3 -> [1, 2, Fizz] |
5 -> last == Buzz |
15 -> last == FizzBuzz |
100 -> [1, 2, Fizz, 4, Buzz, Fizz, ... ] |
Et si on utilisait le système de type plutôt qu’écrire un test ?
Je me pose la question si je dois réellement écrire un test pour celui-ci.
Ne pourrais-je pas gérer ce cas marginal autrement ?
À ton avis ?
…
…
…
Le compilateur ! On met un
UInt
au lieu d’unInt
pour le paramètren
!
Euréka ! Très bonne idée !
Modifie la méthode upTo
comme ceci :
Relance les tests pour être sûr.
Ça marche toujours !
On vient de s’épargner un test, donc du code à maintenir, génial ! Merci le système de type !
1 -> [1] |
3 -> [1, 2, Fizz] |
5 -> last == Buzz |
15 -> last == FizzBuzz |
100 -> [1, 2, Fizz, 4, Buzz, Fizz, ... ] |
Plus les tests deviennent spécifiques, plus le code de prod devient générique !
Ajoute le test suivant :
Lance les tests. Le dernier échoue !
Modifie le code pour le faire passer :
Relance les tests. Ils passent !
REFACTORING !!!
WOW! Oui c’est ça haha !
Y’a une duplication ! C’est pô bien !
Effectivement, le "1"
est dupliqué !
Il est présent dans le code de test, et dans le code de production.
Nous sommes donc forcés de le supprimer à un des deux endroits et de le remplacer par autre chose sans rien casser.
Nous allons généraliser le code de production pour supprimer la duplication sans rien casser.
Comment ça “généraliser” ?
Généraliser revient à supprimer ce qui est spécifique, ici "1"
en utilisant des variables.
Quelle variable peux-tu utiliser dans ce cas précis pour remplacer "1"
dans la méthode upTo
?
…
…
…
n
!
C’est ça ! Modifie le code de upTo
comme ceci :
Relance les tests. Ils passent toujours !
Ceci est très intéressant et illustre une des propriétés des bons tests.
Au fur et à mesure que les tests deviennent spécifiques, le code de production devient générique.
Il y a encore de la duplication, mais dans les tests cette fois.
Simplifions les grâce à trois refactorings : extract variable, extract method et inline temp.
Premièrement fais un extract variable des paramètres en entrée de upTo
dans les deux tests :
Ensuite, fais remonter les variables input
et expected
en haut de chaque méthode de test :
Et enfin un extract method des trois dernières lignes :
- Tu noteras que j’ai ajouté un paramètre
line
, avec la valeur spéciale#line
. Cela permet d’indiquer à Xcode à quelle ligne aller lorsque l’assertion échoue et que l’on clique sur le test qui a échoué. Sans ce paramètre, Xcode nous emmènerait dans la méthodeassertThatFizzBuzz...
et on devrait aller nous-même dans le code appelant la méthode. Ce qui est très ennuyeux !
Et enfin, ultime étape, tu peux inline temp les variables input
& expected
:
Un petit run des tests pour vérifier qu’on n’a rien cassé au passage…
…et ça marche ! Merveilleux !
TDD : une aide pour raisonner plus profondément
Je viens de penser à un nouveau test.
Un test qui nous obligera à boucler : FizzBuzz jusqu’à 2 !
Je l’ajoute en haut de la liste :
2 -> [1, 2] |
3 -> [1, 2, Fizz] |
5 -> last == Buzz |
15 -> last == FizzBuzz |
100 -> [1, 2, Fizz, 4, Buzz, Fizz, ... ] |
Tu modifies souvent ta liste comme ça ?
Oui cela m’arrive tout le temps !
Au fur et à mesure que je fais passer des tests, d’autres cas me viennent en tête.
C’est un des bienfaits de la pratique du TDD.
Elle nous pousse à raisonner plus intensivement sur notre code et ainsi nous permet de trouver plus de cas à tester.
Cela a pour effet de rendre le code plus robuste, d’éviter certains bugs.
On continue ? 😉
Ajoute le test suivant :
Grâce aux refactorings d’avant, ce test a été très simple à écrire !
De plus, il est on ne peut plus parlant et clair.
Toujours prendre soin de ses tests hein ?
Toujours ! 👍
On le fait passer ?
Avec plaisir ! Mais comment ?
…
…
…
Comme ça !
Bien joué ! Le problème c’est que ce n’est pas très propre…
C’est pour ça que la phase de Refactoring existe !
😢 Oh…je suis si fier de toi !
Supprimons les duplications en généralisant. Le
"1"
peut devenir"\(n-1)"
dans un premier temps.
Hum…je vois un schéma qui se répète grâce à ce refactoring.
Ça m’a tout l’air d’être un bon candidat pour une boucle.
En plus, une boucle est la généralisation d’un if !
Déjà mieux !
On peut le faire en plus “prog fonctionnelle” please ? 🤗
Oh oui, ce var
me dérange aussi, mais comment ?
map
!
Woohoo ! 🎉
Je me demande…Est-ce que le premier if
est toujours nécessaire ?
On peut regarder la doc de
map
sur lesClosedRange
pour vérifier ?
Trop long ! Je vais simplement virer ce if
et vérifier si mes tests passent toujours !
Ah ouais pas bête… 😅
❌ FAIL
Oups, ça ne marche pas !
Au moins nous avons pu vérifier en un rien de temps !
Oui, c’est ce qui est intéressant avec de bons tests, on peut vérifier nos idées rapidement !
Continuons…
Le test échoue avec l’erreur suivante : Fatal error: Can't form Range with upperBound < lowerBound
Effectivement, j’essaye de créer un Range
avec n = 0
soit (1...0)
et ce n’est pas possible.
Et si je mettais (0...n)
plutôt ?
❌ FAIL
Tous les tests échouent !
Effectivement, mais les erreurs sont claires :
XCTAssertEqual failed: ("[]") is not equal to ("["0"]")
XCTAssertEqual failed: ("["1"]") is not equal to ("["0", "1"]")
XCTAssertEqual failed: ("["1", "2"]") is not equal to ("["0", "1", "2"]")
Un magnifique "0"
est ajouté.
Cela est dû au fait que mon range démarre par 0
.
Et si j’ignorais simplement le premier élément de mon range ?
✅ SUCCESS
Aaaah voilà qui est mieux !
Une ligne de code, waouh !
Et tout ça grâce aux tests actuels qui vérifient la non-régression !
Conclusion
Nous avons vu beaucoup de concepts dans cet article.
Les tests nous ont permis de vérifier que l’on ne casse rien lorsqu’on essaye une idée (supprimer un if
par exemple). De plus, il est parfois plus efficace de faire une rapide expérience en changeant le code plutôt qu’étudier la documentation. Grâce aux tests, je peux donc gagner du temps à ce niveau-là. Et ce même sur un exemple simpliste. Je te laisse imaginer sur un cas plus complexe de la vraie vie !
Les tests nous poussent à raisonner sur notre code, à imaginer de nouveaux cas à la marge, à rendre notre code plus robuste. Ce ne sont pas tant les tests en soi qui le permettent mais la pratique de leur écriture, la pratique du TDD.
Les tests sont plus importants que le code de production, il faut en prendre soin. C’est grâce à leur présence et à la confiance que nous leur accordons que nous pouvons manipuler le code de production avec sérénité (et l’améliorer !).
Enfin, une propriété importante de la pratique est qu’au fur et à mesure que les tests deviennent spécifiques, le code de production devient générique.
Je te dis à très vite dans le prochain article de cette série “Introduction à TDD en Swift” !
J'ai écrit cet article pendant 5h et j'ai eu 1 "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