Développement piloté par les tests: ce que c'est et ce qu'il n'est pas.

Le développement piloté par les tests est devenu populaire au cours des dernières années. De nombreux programmeurs ont essayé cette technique, ont échoué et ont conclu que le TDD ne valait pas l'effort requis.

Certains programmeurs pensent que, en théorie, c'est une bonne pratique, mais qu'il n'y a jamais assez de temps pour vraiment utiliser TDD. Et d'autres pensent que c'est essentiellement une perte de temps.

Si vous ressentez cela, je pense que vous ne comprendrez peut-être pas ce qu'est vraiment le TDD. (OK, la phrase précédente était pour attirer votre attention). Il existe un très bon livre sur le TDD, Test Driven Development: By Example, de Kent Beck, si vous voulez le vérifier et en savoir plus.

Dans cet article, je vais passer en revue les principes de base du développement piloté par les tests, en abordant les idées fausses courantes sur la technique TDD. Cet article est également le premier d'un certain nombre d'articles que je vais publier, tous sur le développement piloté par les tests.

Pourquoi utiliser TDD?

Il existe des études, des articles et des discussions sur l'efficacité du TDD. Même s'il est vraiment utile d'avoir des chiffres, je ne pense pas qu'ils répondent à la question de savoir pourquoi nous devrions utiliser le TDD en premier lieu.

Dites que vous êtes un développeur Web. Vous venez de terminer une petite fonctionnalité. Pensez-vous qu'il suffit de tester cette fonctionnalité simplement en interagissant manuellement avec le navigateur? Je ne pense pas qu'il soit suffisant de se fier uniquement aux tests effectués manuellement par les développeurs. Malheureusement, cela signifie qu'une partie du code n'est pas assez bonne.

Mais la considération ci-dessus concerne les tests, pas le TDD lui-même. Alors pourquoi TDD? La réponse courte est «parce que c'est le moyen le plus simple d'obtenir à la fois un code de bonne qualité et une bonne couverture de test».

La réponse la plus longue vient de ce qu'est vraiment le TDD… Commençons par les règles.

Règles du jeu

L'oncle Bob décrit TDD avec trois règles:

- Vous n'êtes pas autorisé à écrire un code de production à moins que ce ne soit pour réussir un test unitaire échoué - Vous n'êtes pas autorisé à écrire plus d'un test unitaire qu'il ne suffit pour échouer; et les échecs de compilation sont des échecs.- Vous n'êtes pas autorisé à écrire plus de code de production que ce qui est suffisant pour réussir le test unitaire qui a échoué.

J'aime aussi une version plus courte, que j'ai trouvée ici:

- Ecrire seulement assez de test unitaire pour échouer - Ecrire seulement assez de code de production pour que le test unitaire échouant réussisse.

Ces règles sont simples, mais les personnes qui approchent TDD enfreignent souvent une ou plusieurs d'entre elles. Je vous mets au défi: pouvez-vous écrire un petit projet en suivant strictement ces règles? Par petit projet, j'entends quelque chose de réel, pas seulement un exemple qui nécessite environ 50 lignes de code.

Ces règles définissent les mécanismes du TDD, mais elles ne sont certainement pas tout ce que vous devez savoir. En fait, le processus d'utilisation du TDD est souvent décrit comme un cycle Rouge / Vert / Refactor. Voyons de quoi il s'agit.

Cycle de refactorisation rouge vert

Phase rouge

Dans la phase rouge, vous devez écrire un test sur un comportement que vous êtes sur le point de mettre en œuvre. Oui, j'ai écrit le comportement . Le mot «test» dans Test Driven Development est trompeur. Nous aurions dû l'appeler «développement axé sur le comportement» en premier lieu. Oui, je sais, certaines personnes soutiennent que BDD est différent de TDD, mais je ne sais pas si je suis d'accord. Donc, dans ma définition simplifiée, BDD = TDD.

Voici une idée fausse courante: «J'écris d'abord une classe et une méthode (mais pas d'implémentation), puis j'écris un test pour tester cette méthode de classe». Cela ne fonctionne pas de cette façon.

Prenons du recul. Pourquoi la première règle de TDD exige-t-elle que vous écriviez un test avant d'écrire un morceau de code de production? Sommes-nous des maniaques de personnes TDD?

Chaque phase du cycle RGR représente une phase du cycle de vie du code et la manière dont vous pouvez vous y rapporter.

Dans la phase rouge, vous agissez comme un utilisateur exigeant qui souhaite utiliser le code qui est sur le point d'être écrit de la manière la plus simple possible. Vous devez écrire un test qui utilise un morceau de code comme s'il était déjà implémenté. Oubliez la mise en œuvre! Si, dans cette phase, vous pensez à la manière dont vous allez écrire le code de production, vous le faites mal!

C'est dans cette phase que vous vous concentrez sur l'écriture d'une interface propre pour les futurs utilisateurs. C'est la phase où vous concevez comment votre code sera utilisé par les clients.

Cette première règle est la plus importante et c'est la règle qui différencie le TDD des tests réguliers. Vous écrivez un test afin de pouvoir ensuite écrire du code de production. Vous n'écrivez pas de test pour tester votre code.

Regardons un exemple.

// LeapYear.spec.jsdescribe('Leap year calculator', () => { it('should consider 1996 as leap', () => { expect(LeapYear.isLeap(1996)).toBe(true); });});

Le code ci-dessus est un exemple de l'apparence d'un test en JavaScript, en utilisant le framework de test Jasmine. Vous n'avez pas besoin de connaître Jasmine - il suffit de comprendre que it(...)c'est un test et expect(...).toBe(...)c'est un moyen de faire en sorte que Jasmine vérifie si quelque chose est comme prévu.

Dans le test ci-dessus, j'ai vérifié que la fonction LeapYear.isLeap(...)retourne truepour l'année 1996. Vous pouvez penser que 1996 est un nombre magique et donc une mauvaise pratique. Ce n'est pas. Dans le code de test, les nombres magiques sont bons, alors que dans le code de production, ils doivent être évités.

Ce test a en fait des implications:

  • Le nom du calculateur d'année bissextile est LeapYear
  • isLeap(...)est une méthode statique de LeapYear
  • isLeap(...)prend un nombre (et non un tableau, par exemple) comme argument et renvoie trueou false.

C'est un test, mais il a en fait de nombreuses implications! Avons-nous besoin d'une méthode pour dire si une année est une année bissextile, ou avons-nous besoin d'une méthode qui renvoie une liste des années bissextiles entre une date de début et une date de fin? Le nom des éléments est-il significatif? Ce sont les types de questions que vous devez garder à l'esprit lors de la rédaction des tests dans la phase rouge.

Dans cette phase, vous devez prendre des décisions sur la manière dont le code sera utilisé. Vous basez cela sur ce dont vous avez vraiment besoin pour le moment et non sur ce que vous pensez être nécessaire.

Voici une autre erreur: n'écrivez pas un tas de fonctions / classes dont vous pensez avoir besoin. Concentrez-vous sur la fonctionnalité que vous implémentez et sur ce qui est vraiment nécessaire. Écrire quelque chose dont la fonctionnalité n'a pas besoin est une sur-ingénierie.

Qu'en est-il de l'abstraction? Je le verrai plus tard, dans la phase de refactorisation.

Phase verte

C'est généralement la phase la plus simple, car dans cette phase, vous écrivez du code (de production). Si vous êtes programmeur, vous faites cela tout le temps.

Voici une autre grosse erreur: au lieu d'écrire suffisamment de code pour passer le test rouge, vous écrivez tous les algorithmes. En faisant cela, vous pensez probablement à quelle est l'implémentation la plus performante. En aucune façon!

Dans cette phase, vous devez agir comme un programmeur qui a une tâche simple: écrire une solution simple qui fait passer le test (et rend le rouge alarmant sur le rapport de test devient un vert amical). Dans cette phase, vous êtes autorisé à enfreindre les meilleures pratiques et même à dupliquer du code. La duplication de code sera supprimée lors de la phase de refactorisation.

Mais pourquoi avons-nous cette règle? Pourquoi ne puis-je pas écrire tout le code qui est déjà dans ma tête? Pour deux raisons:

  • Une tâche simple est moins sujette aux erreurs et vous souhaitez minimiser les bogues.
  • Vous ne voulez certainement pas mélanger du code qui est en cours de test avec du code qui ne l'est pas. Vous pouvez écrire du code qui n'est pas en cours de test (c'est-à-dire hérité), mais la pire chose que vous puissiez faire est de mélanger du code testé et non testé.

Qu'en est-il du code propre? Et les performances? Et si l'écriture de code me faisait découvrir un problème? Et les doutes?

La performance est une longue histoire et n'entre pas dans le cadre de cet article. Disons simplement que le réglage des performances dans cette phase est, la plupart du temps, une optimisation prématurée.

La technique de développement piloté par les tests fournit deux autres choses: une liste de tâches et la phase de refactorisation.

The refactor phase is used to clean up the code. The to-do list is used to write down the steps required to complete the feature you are implementing. It also contains doubts or problems you discover during the process. A possible to-do list for the leap year calculator could be:

Feature: Every year that is exactly divisible by four is a leap year, except for years that are exactly divisible by 100, but these centurial years are leap years if they are exactly divisible by 400.
- divisible by 4- but not by 100- years divisible by 400 are leap anyway
What about leap years in Julian calendar? And years before Julian calendar?

The to-do list is live: it changes while you are coding and, ideally, at the end of the feature implementation it will be blank.

Refactor phase

In the refactor phase, you are allowed to change the code, while keeping all tests green, so that it becomes better. What “better” means is up to you. But there is something mandatory: you have to remove code duplication. Kent Becks suggests in his book that removing code duplication is all you need to do.

In this phase you play the part of a picky programmer who wants to fix/refactor the code to bring it to a professional level. In the red phase, you’re showing off your skills to your users. But in the refactor phase, you’re showing off your skills to the programmers who will read your implementation.

Removing code duplication often results in abstraction. A typical example is when you move two pieces of similar code into a helper class that works for both the functions/classes where the code has been removed.

For example the following code:

class Hello { greet() { return new Promise((resolve) => { setTimeout(()=>resolve('Hello'), 100); }); }}class Random { toss() { return new Promise((resolve) => { setTimeout(()=>resolve(Math.random()), 200); }); }}new Hello().greet().then(result => console.log(result));new Random().toss().then(result => console.log(result));

could be refactored into:

class Hello { greet() { return PromiseHelper.timeout(100).then(() => 'hello'); }}class Random { toss() { return PromiseHelper.timeout(200).then(() => Math.random()); }}class PromiseHelper { static timeout(delay) { return new Promise(resolve => setTimeout(resolve, delay)); }}const logResult = result => console.log(result);new Hello().greet().then(logResult);new Random().toss().then(logResult);

As you can see, in order to remove thenew Promise and setTimeout code duplication, I created a PromiseHelper.timeout(delay) method, which serves both Hello and Random classes.

Just keep in mind that you cannot move to another test unless you’ve removed all the code duplication.

Final considerations

In this section I will try to answer to some common questions and misconceptions about Test Drive Development.

  • T.D.D. requires much more time than “normal” programming!

What actually requires a lot of time is learning/mastering TDD as well as understanding how to set up and use a testing environment. When you are familiar with the testing tools and the TDD technique, it actually doesn’t require more time. On the contrary, it helps keep a project as simple as possible and thus saves time.

  • How many test do I have to write?

The minimum amount that lets you write all the production code. The minimum amount, because every test slows down refactoring (when you change production code, you have to fix all the failing tests). On the other hand, refactoring is much simpler and safer on code under tests.

  • With Test Driven Development I don’t need to spend time on analysis and on designing the architecture.

This cannot be more false. If what you are going to implement is not well-designed, at a certain point you will think “Ouch! I didn’t consider…”. And this means that you will have to delete production and test code. It is true that TDD helps with the “Just enough, just in time” recommendation of agile techniques, but it is definitely not a substitution for the analysis/design phase.

  • Should test coverage be 100%?

No. As I said earlier, don’t mix up tested and untested code. But you can avoid using TDD on some parts of a project. For example I don’t test views (although a lot of frameworks make UI testing easy) because they are likely to change often. I also ensure that there is very a little logic inside views.

  • I am able to write code with very a few bugs, I don’t need testing.

You may able to to that, but is the same consideration valid for all your team members? They will eventually modify your code and break it. It would be nice if you wrote tests so that a bug can be spotted immediately and not in production.

  • TDD works well on examples, but in a real application a lot of the code is not testable.

I wrote a whole Tetris (as well as progressive web apps at work) using TDD. If you test first, code is clearly testable. It is more a matter of understanding how to mock dependencies and how to write simple but effective tests.

  • Tests should not be written by the developers who write the code, they should be written by others, possibly QA people.

If you are speaking about testing your application, yes it is a good idea to ask other people to test what your team did. If you are speaking about writing production code, then that’s the wrong approach.

What’s next?

This article was about the philosophy and common misconceptions of TDD. I am planning to write other articles on TDD where you will see a lot of code and fewer words. If you are interested on how to develop Tetris using TDD, stay tuned!