Les tests unitaires
L'article du jour est fait en effort conjoint avec mon ancien collègue et mentor Guillaume Faas (🔹) sous la forme d'une interview d'un développeur.
Joins-toi à moi pour le remercier pour son incroyable implication dans l'écriture de cet article! Nous espérons tous les deux que tu vas l'adorer, autant que nous avons adoré l'écrire.
tldr; fais des katas en appliquant la méthodologie "Test Driven Development" !
Présentation
🔸 Salut Guillaume! Tu sais te présenter s'il te plaît?
🔹 Salut Tinaël! Merci de m'avoir invité à parler sur ton site. Je m'appelle Guillaume Faas et je suis un expert .NET / Software Craftsman, actuellement employé chez Squaremiled S.A.. Je développe des logiciels depuis une dizaine d'années en ayant évolué dans des environnements et secteurs d'activités variés.
Le sujet!
🔸 De quoi va-t-on parler aujourd'hui?
🔹 À ton avis? Tu n'as pas lu le titre de l'article on dirait. On va parler de test unitaire!
🔸 Quand est-ce que t'y as touché pour la première fois?
🔹 C'était il y a bien longtemps, dans une galaxie lointaine... J'avais à peine quelques années d'expérience à l'époque. J'étais dans la même société pendant une majeure partie de ma carrière et, par rapport à mon day-to-day, je pensais que j'avais déjà tout vu. Je commençais à regarder en ligne à des guidelines, des best practices, des patterns, etc. Je suis tombé sur plein de sujets excitants et surtout nouveaux. C'était comme si j'avais mis les pieds dans un nouveau monde qui n'avait rien à voir avec la routine dans laquelle j'étais ancré. Le testing était évidemment l'un de ces sujets. Cependant, j'ai vite réalisé que je devais progresser sur d'autres sujets avant d'être capable d'introduire des tests. Cela m'a pris du temps avant que je ne sois vraiment apte à travailler avec une approche test-driven.
🔸 Ok! Par contre, histoire de ne pas perdre les lecteurs... tu sais expliquer ce qu'est un test unitaire s'il te plaît?
🔹 Oui, bien sûr! Un test unitaire, c'est un test qui vérifie un unit of code. La notion de unit of code varie en fonction des écoles de testing. L'école London (ou Mockist) va voir cela comme le plus petit chunk of code, on parlera d'une classe ou d'une méthode. Par contre, l'école Detroit (ou Classicist) va voir cela comme un behavior, donc un ensemble de classes ou de méthodes. Pour les plus curieux, voici un article qui détaille les différences entre les deux écoles. Je précise qu'une école n'est pas meilleure que l'autre, chacune a ses avantages et inconvénients. C'est une histoire de préférence et de compromis. La différence principale tient surtout de la taille de ton System Under Test (SUT) et de la relation entre les différents collaborateurs.
Mais à la fin, un test unitaire est simplement un morceau de code qui valide qu'un autre morceau de code a le résultat et/ou side effect attendu par rapport à un scénario donné.
🔸 D'accord, mais ça se situe où dans la "hiérachie des tests"?
🔹 Il s'agit de la pyramide de testing (Agile Testing Pyramid) mais de gauche à droite au lieu de bas en haut. Plus tu seras situé vers la gauche, plus ton scope est petit et plus tes tests seront nombreux et rapides. La logique inverse est appliquée lorsque tu vas dans l'autre sens.
Nous, on se situe sur la partie "unit" puisque techniquement, il n'y a pas plus petit qu'une unit. Un test unitaire doit être exécuté de façon standalone dans un environnement sandbox. C'est-à-dire qu'un test unitaire n'a aucun impact sur l'extérieur du test, et si tu en lances plusieurs en parallèle, ils ne doivent pas avoir de side effects entre eux.
Cela signifie donc que dans un test unitaire: on ne contacte pas de DB, on évite de faire une requête HTTP, d'accéder à un fichier du système, etc. De la même façon, on ne va pas contacter les autres dépendances de la solution. On reste vraiment "interne" à la fonction.
Voici quelques points de la partie functional testing:
- l'unit testing, c'est vérifier qu'un composant fonctionne bien. Il s'agit du test le plus rapide, on parlera de fast feedback loop ;
- l'integration testing, c'est vérifier que plusieurs composants fonctionnent bien entre eux ;
- le user acceptance testing qui consiste à vérifier l'entièreté de l'application tout en évitant de contacter les dépendances externes (ex: des requêtes vers des fournisseurs de données extérieurs à ton application). Il s'agit du test le plus représentatif, probablement celui qui a le plus de valeur à l'échelle du produit car il vérifie des business requirements.
À noter qu'il n'y a pas d'obligation de tous les implémenter. On pourrait très bien avoir une test suite composée uniquement de tests d'une seule de ces catégories. Mais il convient de garder à l'esprit que notre test suite ne sera pas des plus efficaces.
🔸 D'accord! Mais pourquoi le testing, c'est pas réalisé dans le monde professionnel?
🔹 En réalité, une grande partie des développeurs n'écrivent pas ou peu de tests. De plus, les tests d'intégrations sont moins répandus que les tests unitaires car ils sont plus complexes à écrire. Au final, cette tâche est souvent vue comme une corvée ou alors une extra step que l'on fera uniquement si on a le temps.
Uncle Bob en a également parlé à une conférence à Londre en 2018:
🔸 C'est quoi le but du testing unitaire et quand est-ce que ça devrait être appliqué?
🔹 Le but est assez simple: c'est de montrer qu'une méthode fonctionne comme tu l'attends. C'est-à-dire que tu vas avoir un comportement attendu, par exemple ta méthode getSomething
doit te retourner quelque chose. Tu vas donc tester différents scénarios et vérifier qu'elle se comporte toujours de la bonne façon.
Pour ce qui est du "quand", c'est encore plus simple: ça doit être appliqué à partir du moment où tu as de la logique quelque part.
🔸 Bah du coup, quels en sont les avantages et inconvénients de la mise en place de tests unitaires?
🔹 Comme dit précédemment, tu écris du code qui teste du code. Vu comme cela, on dirait surtout une charge de travail supplémentaire sauf qu'il existe de réels intérêts derrière.
- Le test unitaire sert de filet de sécurité contre les régressions.
L'étape du refactoring intervient systématiquement dans un projet. Le problème étant qu'à partir du moment où l'on modifie quelque chose, il y a un risque de régression. On parle de régression lorsque quelque chose fonctionnait auparavant et ne fonctionne plus suite à un changement. C'est précisement ici que le test intervient: il permet de garantir que tes composants fonctionnent toujours comme attendu. Si jamais quelque chose ne fonctionne plus suite à un changement, la test suite t'affichera les tests qui ont détecté un problème avec un beau rond rouge. Et ça, du point de vue d'un développeur, c'est énorme! Cela veut dire que tu es beaucoup plus serein et que tu passes bien moins de temps à vérifier que tes changements n'ont pas eu d'effets indésirables sur le reste des fonctionnalités.
- L'écriture de tests améliore la qualité du code de ton application.
C'est lié à ce dont on vient d'aborder. Il est probable que les lecteurs aient déjà rencontré une situation similaire: lorsque l'on parle de refactoring à un Product Owner ou Product Manager, la première crainte est toujours que quelque chose ne fonctionne plus. Si tu es couvert par une test suite, tu n'as pas peur du refactoring. C'est même l'inverse, tu es encouragé à faire du refactoring régulièrement tout en étant protégé.
- Une suite de tests devient ce qu'on appelle une living documentation.
Lorsque l'on parle de documentation, on a tous à l'esprit des commentaires. Le problème est qu'ils ne sont jamais à jour avec le reste du code. Le code évolue, la documentation non. Par contre, ton test unitaire sera toujours up-to-date. Si ce n'est pas le cas, alors ta suite de tests ne te donnera pas le feu vert pour aller plus loin.
C'est d'autant plus intéressant dans le cadre d'une arrivée d'un nouveau développeur sur le projet. Plutôt que de lire tout le code d'une méthode pour savoir ce qu'elle fait, il lui suffit de regarder les différents tests de cette méthode. Chaque comportement sera représenté par un test avec un naming explicite sur le scénario et résultat attendu (ex: GetItem_ShouldReturnNotFoundResult_GivenItemIsMissing
). Ça facilite donc l'onboarding!
- Cela réduit le temps de détection des bugs.
On a évoqué le terme short feedback loop un peu plus tôt dans la discussion. Les tests unitaires sont très rapides à exécuter, ils nous donnent un feedback presque instantané sur la santé de la solution. Cela veut dire que l'on doit les exécuter régulièrement. Je schématise mais on a un bouton (ou un raccourci) qui nous donne un statut Vert/Rouge en quelques secondes. Actionner ce bouton doit devenir quelque chose de systématique. Cela a déjà un intérêt pour nous en tant que développeurs mais ce n'est pas tout. On en parlera un peu plus tard!
- Ce n'est pas un avantage direct mais plutôt un effet secondaire: faire du testing te rend meilleur.
Pour faire en sorte que tu puisses écrire des tests apportant une réelle valeur, tu dois respecter certains principes. Tu dois toujours avoir une certaine couche d'abstraction pour mocker tes dépendances, tu dois pouvoir les injecter, tu dois limiter les responsabilités de tes composants, etc... en fait tu vas te forcer à appliquer plusieurs principes régulièrement (SOLID par exemple). Du coup, cela te force à casser tes composants, à les découpler, à penser à leurs interations et responsabilités. Bref, à réfléchir et à te poser beaucoup de questions. Mine de rien, on parle de code design! Et donc, action-réaction: tu deviens meilleur au fil du temps. Cela fait très Happy End mais tu vois où je veux en venir.
🔸 Et comme c'est peu mis en place dans les entreprises, c'est vu comme "nouveau" et ça motive à en apprendre plus!
🔹 Je te rejoins sur l'aspect nouveauté mais il reste surtout présent au début lorsqu'on découvre le sujet. Mais il y a une partie d'interprétation dans tout cela: certains (comme toi) le voient comme quelque chose d'intéressant, d'autres le voient comme une corvée ou une pression supplémentaire. Tu trouveras toujours des personnes réfractaires aux tests pour des raisons diverses et variées. Peut-être qu'on aura l'occasion d'aborder les raisons qui sont généralement évoquées.
Pour revenir à ta question, voici les inconvénients qui me viennent à l'esprit:
- On l'a mentionné, il y a des pré-requis: il faut comprendre les piliers de l'orienté objet, l'injection de dépendances, les principes SOLID, etc.
- Le fait que l'on trouve peu de projets avec de réelles test suites rend l'apprentissage moins accessible. Il en va de même pour trouver un coach passionné par ce sujet.
- Il y a une courbe d'apprentissage/de progression assez importante. Tout le monde passe par une phase de frustration au début parce que l'on est pas à l'aise et on a l'impression d'être plus lent. Il faut résister et persévérer car les tests nous feront en réalité aller plus vite. On en parlera avec l'approche TDD.
- Cela demande de la préparation : il faut réfléchir à l'architecture du projet, aux relations entre les différents composants, etc... Vu comme cela, ce n'est pas vraiment un inconvénient mais on ne peut pas (plus?) se lancer tête baissée dans un développement sans un minimum de réflexion.
- Il y a un manque de compréhension du côté des autres équipes intervenant sur le développement du produit, notamment non-IT. On retombe toujours sur des discussions sur le Return On Investement (ROI) ou l'impact sur la vélocité.
On entend souvent dire que "cela prend du temps et que cela sera planifié plus tard" ou que "les développeurs n'ont pas le temps" mais ces arguments ne sont pas vraiment valables. En effet, le premier indique clairement un manque de vision et de compréhension du testing. Déjà parce que "plus tard" n'arrive jamais. Mais surtout, faire les tests à la fin du développement n'a aucun sens. On perd tous les avantages qu'apporte le testing. Je l'ai déjà dit mais on en parlera avec l'approche TDD. Ensuite, le second indique un problème d'organisation. Les tests devraient être inclus dans les estimations et pas comme un travail supplémentaire à réaliser.
🔸 Et le test coverage, dans tout ça?
🔹 Tester, c'est super et on ressent les bénéfices. Cependant, il faut aussi faire un statut sur l'état de la test suite. C'est là qu'on arrive sur le code coverage. C'est une métrique informative sur la progression de couverture de tests de ton application. J'insiste vraiment sur le côté informatif. Ce serait une erreur de mesurer la qualité de la suite de tests sur base de sa couverture. C'est une métrique de quantité et non de qualité. J'ai déjà lu des articles sur des sociétés qui ont intégré la valeur de code coverage dans les objectifs des développeurs et cela a incité les développeurs à utiliser de faux tests pour faire gonfler le coverage.
Le seul moyen de vérifier la qualité d'une test suite d'un projet, c'est de se poser quelques questions:
- Est-ce que le temps de développement général des fonctionnalités reste approximativement le même avec le temps?
- Est-ce que la quantité de bugs trouvés en production diminue avec le temps?
- Est-ce que tu arrives à facilement accueillir une nouvelle ressource au sein de l'équipe de développement?
- Est-ce que les développeurs ont confiance en leur test suite? Est-ce qu'elle est représentative de l'état de santé de la solution? Est-ce qu'un rond vert garantit vraiment qu'un composant fonctionne?
Si tu es en mesure de répondre "oui" à toutes ces questions, félicitations! Tu peux être fier de la test suite que tu as mis en place. Le souci? C'est difficile d'avoir une réponse à ces questions alors que tu dois rendre des comptes day one... Tu remarques d'ailleurs que les trois premières font référence au temps.
🔸 Bon sinon... question coût, qu'est-ce qu'il en est? Parce que finalement, écrire un test unitaire, c'est tout de même écrire du code. Ça coûte!
🔹 Je vois là où tu veux en venir. Non, cela ne coûte pas plus sauf si tu factures au caractère! Même si tu écris plus de code, tu es vraiment gagnant et pas que sur l'aspect temps. Je t'ai dit qu'on devait parler de TDD? Parce que cela te fait même gagner du temps à court-terme. Bref. Développer une fonctionnalité peut te prendre un peu plus de temps en sachant que cela dépendra surtout de ton aisance avec l'écriture de tests. D'un autre côté, cela va surtout te "sauver la vie" pas mal de fois parce que tu vas éviter énormement de bugs qui, en temps normal, seraient arrivés bien plus tard dans ton process, lors des tests utilisateur sur un environnement de QA ou en production. Toi qui voulais parler d'argent, plus un bug est découvert tard, plus il coûte cher:
Et c'est tout à fait normal.
On peut reparler de la fast feedback loop: si un bug est découvert par un test unitaire, c'est en local sur ta machine, juste après le changement (n'oublie pas de rebuild et de rerun ta suite de tests). Il est identifié rapidement et corrigé rapidement. À contrario, un bug qui passe en production... il est découvert par un utilisateur qui remonte le problème à ta product team qui elle doit analyser le feedback et ouvrir un ticket dans ton backlog. Ce ticket, il va être priorisé par ton Product Owner pour être inclus dans la prochaine itération puis il sera assigné à un développeur. En admettant que ce ne soit pas toi, il y aura une phase d'investigation (reproduction du bug), une phase de correction de bug et après il doit repartir sur tous les environnements et être validé par des Quality Assurance Users.
J'ai volontairement pris un cas extrême pour montrer le pire scénario mais c'est aussi la façon d'être le plus explicite sur le problème. Ce qu'il est important de retenir, c'est qu'un test peut faire gagner beaucoup de temps à beaucoup de personnes, aussi simple soit-il.
ndlr: pour en savoir plus sur les raisons qui font qu'un logiciel a des bugs, n'hésitez pas à consulter cette page et d'autres sur le web!
En détails
🔸 Ok! Et si on parlait maintenant de black box et white box testing?
🔹 J'aime les schémas, tu aimes les schémas? C'est bien les schémas!
Le black box testing, c'est donner une information d'entrée au SUT et vérifier l'information de sortie. C'est aussi simple que ça: on ne prend pas en compte ce qu'il se passe à l'intérieur de la méthode. Il y a un cas précis où ce type de testing sera obligatoire: les méthodes pures. Ces méthodes n'ayant aucune dépendance ou variables partagées, elles n'ont donc aucun side effect. Le black box testing est donc une évidence mais cela rend aussi le test extrêmement robuste car rien ne vient impacter le résultat du test.
Prenons par exemple une méthode Sum
d'une classe Calculator
. On est exactement sur le scénario mentionné plus haut:
[TestClass]
public class CalculatorTests
{
[TestMethod]
public int Sum_Should_ReturnTheSumOfTheTwoNumbers()
{
Calculator calculator = new();
int result = calculator.Sum(2,3);
Assert.AreEqual(expected: 5, actual: result);
}
}
On ne connaît pas l'implémentation de la méthode, mais on a écrit un test. On lui donne des valeurs en entrée, et on vérifie la valeur de sortie. Pour le reste des scénarios, je trouve dommage de s'arrêter là. C'est une préférence personnelle, je trouve le white box testing plus pertinent en tant que Mockist.
De l'autre côté, on a donc ce white box testing. À première vue, c'est la même chose: on donne un input et on vérifie l'output. Mais on va aussi vérifier ce qu'il se passe à l'intérieur du SUT. On peut donc vérifier que le SUT a bien fait appel à sa dépendance, que la valeur a bien été mise dans un cache, sauvegardée dans un repo, qu'un event a bien été émis, etc. Cela nous permet de vérifier chaque behavior avec ses side effects.
🔸 La question que tous se posent... Comment écrire de bons tests unitaires?
🔹 Je ne pense pas qu'il y ait de bons ou de mauvais tests unitaires... Evidemment que si! Il faut réfléchir avant sur ce que tu veux faire. Ça peut paraître bête dit comme ça mais think before you do. C'est que j'expliquais lorsque je parlais du fait que faire des tests te rend meilleur. Si tu veux faire des tests efficaces, il faut réfléchir sur la façon dont tes composants vont communiquer entre eux. En fait, tes tests seront efficaces à partir du moment où ils seront faciles à faire. Et si tu te rends compte qu'ils ne le sont pas, c'est qu'il y a un soucis dans ton code.
Exemple: j'ai un service qui doit créer un utilisateur. Avant d'écrire mon test, je dois me poser quelques questions: quelles sont les responsabilités de mon service? Est-il responsable d'envoyer une requête HTTP à un fournisseur externe pour récupérer des informations? Est-il responsable de la persistence en base de données? Est-il responsable du logging? Divide & Conquer: une dépendance ici, une là, et une autre là... Au final, que reste-t-il dans mon service? L'orchestration d'un processus délégué à différentes dépendances (ex: client http, repository, logger, etc) et éventuellement une modification de l'état d'une entité. C'est tout.
Au final, un "bon" test doit:
- te protéger contre les régressions ;
- être résistant au refactoring ;
- te donner un feedback rapide ;
- être maintenable.
🔸 Et, sinon... T'as des conseils pour se lancer dans le testing unitaire?
🔹 Je recommande aux personnes qui veulent démarrer le testing de commencer directement avec le Test Driven Development. Si le test est écrit après l'implémentation, c'est pas vraiment objectif car tu connais déjà l'implémentation donc ton test est fortement lié à ton implémentation. De plus, le code fonctionne déjà donc le test sera perçu comme une perte de temps. Mais surtout: on a bénéficié d'aucun avantage du testing lors de la phase d'implémentation.
Pour cela, vous n'êtes pas seul. Il existe des tonnes de resources disponibles pour vous aider. Voici plusieurs livres que j'aurais aimé avoir lus au début de ma carrière:
- "Test Driven Development - By Example" par Kent Beck.
- "Unit Testing - Principles, Practices and Patterns" par Vladimir Khorikov.
Également, voici un site rempli de conseils et astuces sur TDD avec un grand nombre de katas pour progresser: TDD Buddy.
En parlant de katas, faites des katas. Faites pleins de katas et faites en à plusieurs (pair et/ou mob programming) si vous en avez la possibilité. C'est fun et c'est très formatteur, notamment sur le fait de démarrer avec des exercices simples et progressivement augmenter la difficulté jusqu'à se retrouver avec des situations similaires à ce que l'on peut trouver dans des projets réels. En plus de TDD Buddy, je pourrais recommander Code Wars si vous êtes en manque d'inspiration. Sans forcément faire de l'auto-promotion, vous pouvez aussi trouver quelques katas sur mon GitHub.
Un dernier conseil pour démarrer le testing, on peut se référer à ce qu'on appelle le triple A (AAA), qui signifie Arrange, Act, Assert, pour rendre les tests plus clairs et organisés. Le but est de diviser son test unitaire en 3 parties distinctes:
- arrange : c'est le scénario, la partie où tu prépares les données input de ta méthode ;
- act : c'est l'action, le fait de réaliser l'appel à la méthode que tu vas tester ;
- assert : c'est la vérification du behavior, là partie où tu vérifies l'output ou les side effects.
🔸 Quels sont les "bad smells" dans l'unit testing?
🔹 J'en vois quelques-uns...
- une partie arrange qui fait 15 lignes... C'est trop compliqué. On voit clairement que la méthode testée fait trop de choses car le scénario est trop compliqué à mettre en place!
- On dit qu'un test ne doit avoir qu'une et une seule raison d'échouer. Un test ne devrait contenir qu'un seul assert.
- Le fait que tu aies du mal à écrire des tests unitaires, non pas à cause du fait que tu n'aies pas la connaissance nécessaire mais plutôt en rapport au code à tester... c'est qu'il y a un soucis au niveau de ton composant. Alors, prends du recul et penses aux responsabilités.
Pour aller plus loin
🔸 Tu as des librairies intéressantes en tête pour faciliter le travail?
🔹 Oui. Pour moi, on peut considérer trois groupes de librairies:
- les librairies de testing qui permettent de générer des tests ;
- les librairies de mocking qui permettent de surcharger le comportement de tes dépendances et de les monitorer ;
- les librairies de génération de données.
Pour ma part:
Frameworks de test | Librairies de mocking | Librairies de génération de données |
---|---|---|
MSTest | Moq | AutoFixture |
NUnit | NInject | |
XUnit | WireMock |
🔸 Sur ce point-ci particuli èrement, j'aimerais mettre en avant le fait qu'il existe aussi des librairies de test pour le front-end. En fait, le testing unitaire n'est pas réservé aux développeurs back-end. On citera notamment Jest, Mocha, Cypress et Jasmine comme librairies fortement liées au testing dans des applications JavaScript.
Du coup, tu n'arrêtes pas d'en parler. C'est quoi le Test Driven Development (TDD)?
🔹 Je suis content que tu poses enfin la question! C'est pas comme si je t'avais tendu la perche plus d'une fois... C'est la joie, le bonheur, la réponse ultime au sens de la vie, c'est tout ça! Non, je rigole. En fait, c'est une façon de mettre les tests au centre de ce que tu fais. On a parlé de tous les points positifs de faire des tests unitaires et aussi du fait qu'on les perdait si on faisait les tests à la fin sans forcément aller dans le détail. En fait, la meilleure façon de bénéficier des avantages des tests, c'est de les faire en premier mais ce n'est pas que ça. Ce n'est pas d'abord faire tous les tests puis ensuite faire l'implémentation. Non, il y vraiment un aspect itératif que l'on retrouve d'ailleurs dans l'Agilité. Tu y vas étape par étape (baby steps), tu ajoutes de nouveaux behaviors en garantissant que ceux précédemment ajoutés fonctionnent toujours. Le filet de sécurité s'agrandit petit à petit naturellement. On peut présenter cela différemment: imaginons une échelle. Cela sera toujours plus facile de la monter marche par marche que de les monter trois par trois.
J'ajouterais que contrairement aux idées reçues, TDD ne rend pas la durée de développement plus longue, bien au contraire. Par exemple, il n'est pas nécessaire d'exécuter la solution pour savoir que le code fonctionne car il a entièrement été développé sur base de tests.
La première étape, c'est l'écriture d'un test unitaire. Normalement, ce test doit obligatoirement échouer puisqu'aucune implémentation n'a été écrite pour qu'il réussise. L'étape suivante, c'est donc d'écrire le code qui permet de faire passer le test au vert. Et là, c'est très important de savoir que ce passage de rouge vers le vert doit être le plus court possible. C'est le moment où on à le droit d'écrire du code "moche", d'hardcoder un résultat, de dupliquer, de coller une réponse de StackOverflow, etc. Cela peut paraître bizarre au début mais il y a un vrai intérêt: vérifier que l'ajout d'un nouveau behavior est possible sans casser tout ce qui a été fait auparavant. L'étape suivante, c'est le refactoring. On a fait un code horrible, il faut maintenant faire quelque chose de propre. J'ai parlé plus tôt de refactoring plus simple et sécurisé: on y est! On a notre green light et le behavior est garanti tant que cette light reste green. On a notre fast feedback loop à portée de main (ou de clic, ou de raccourci) pour savoir si tout est ok. Tu me suis? Ensuite, on atteint la fin du cycle. Cela veut dire une chose: on recommence.
🔸 Je vais fournir un petit exemple pour que tout le monde se situe! Je vais faire ça avec un calculateur de longueur de chaîne de caractères tiens, c'est simple à réaliser. Donc, je commence par écrire le test unitaire:
[TestClass]
public class StringCalculatorTests
{
[TestMethod]
public int Length_ShouldReturn_CorrectLength()
{
StringCalculator calculator = new();
int result = calculator.Length("string"); // "string" fait 6 caractères de long, non?
Assert.AreEqual(expected: 6, actual: result);
}
}
Le test échoue parce que je n'ai pas encore créé la classe StringCalculator
. Prochaine étape!
public class StringCalculator
{
public int Length (string str)
{
return 6;
}
}
Ici, nous sommes donc à l'étape verte. On doit donc passer à l'étape bleue.
🔹 J'ajouterais que ton code est super moche vu que tu as hardcodé la valeur. Mais c'est bien! C'est le but!
🔸 La plus longue: réaliser un refactoring du code qui nous permet de répondre au besoin demandé (calculer la longueur d'une chaîne de caractères) tout en ne cassant pas le test:
public class StringCalculator
{
public int Length (string str)
{
return str.Length; // la manière la plus propre de renvoyer la longueur d'une chaîne de caractères en C#
}
}
Et voilà! Nous pouvons commencer l'écriture d'un nouveau test unitaire.
🔹 C'est un exemple assez simple mais tu y es. Il faut bien noter qu'il y a tout de même des règles à respecter avec TDD mais je pourrais en parler pendant des heures alors on va s'arrêter ici!
🔸 D'accord! D'ailleurs, j'ai ouïe dire que t'as récemment appris le
Test && Commit || Revert
(TCR). Tu sais expliquer en quoi ça consiste?
🔹 Exact, j'ai eu la chance de prendre connaissance de cette pratique via un workshop. Pour schématiser, disons que c'est une vision extrême de TDD. Le meilleur moyen de l'utiliser est avec un script séparé. Ce script va analyser ta solution à chaque sauvegarde et va ensuite exécuter tous tes tests. Si tous tes tests sont vert, il crée un commit qui représente un état stable de ta branche (Test && Commit). Si tu as un seul test qui ne passe plus, il fait un rollback pour revenir à l'état du dernier commit (Revert), qui lui est stable. Cela te force à avancer en baby steps et une chose est mise en évidence: c'est ton dernier changement qui a cassé quelque chose.
Au début, tu passes par une phase de frustration parce que tu peux perdre du code mais justement, cela t'incite à avancer petit à petit pour limiter tes pertes. Plus tes steps sont petites, moins tu risques de perdre du code. C'est un super enseignement en complément de TDD. Losque tu deviens relativement à l'aise avec tout ça, tu remarques que tu avances de plus en plus vite et surtout, tu as toujours une branche qui fonctionne.
Conclusion
🔸 Un dernier mot pour clôturer cette interview?
🔹 "Victoriae mundis et mundis lacrima", ca ne veut absolument rien dire mais je trouve que c'est assez dans le ton. Plus sérieusement, ça fait déjà un moment qu'on discute mais on a seulement gratté la surface. Il reste beaucoup de points à aborder sur le testing. Je conseillerais donc vivement aux lecteurs d'être curieux sur le sujet, de lire et surtout de pratiquer. N'hésitez pas à demander de l'aide autour de vous. Et sinon, le testing, ça vous tente?