Tout commence en octobre 1999 lorsque
Kent Beck présente une nouvelle
méthode de programmation agile : l'eXtreme Programmming abrégé XP.
l'XP définit des pratiques de développement optimisées qui améliorent la production
et la robustesse du code.
Parmi les principes les plus connus de sa méthode on pourra citer
l'intégration continue aussi appelée CI pour Continuous Integration et
la programmation en binôme ou pair programming en anglais.
L'aspect qui nous intéresse ici est un autre pilier de la méthode qui consiste à piloter le développement par les tests alias TDD.
Le TDD est une pratique controversée car coûteuse à mettre en place. Popularisée par les développeurs elle peine à émerger.
Avec la multiplication des environnements d'execution, la complexité des
applications web et l'essor des projets Open-source, les développeurs se
heurtent à des problèmes de compatibilités croisées et d'inconsistances.
Aujourd’hui les standards sont de plus en plus permissifs et favorisent ainsi les
comportements à risque. La plupart du temps, les systèmes sont capables de corriger
vos erreurs, des fois sans même vous en avertir.
Dans ce contexte :
Commencer par vous imposer des pratiques de développement rigoureuses est impératif mais les tests seront un complément indispensable à la qualité de votre code.
Heureusement, le TDD apporte alors une réponse élégante à l'ensemble de ces problématiques.
Le Test Driven Development (Développement Dirigé par les Tests), est une technique de développement qui impose l’écriture de tests avant même l’écriture de la première ligne de code.
Dans la théorie, la méthode requiert l’intervention d’au moins deux intervenants différents, une personne écrit les tests, l’autre le code testé. Cela permet d’éviter les problèmes liés à la subjectivité.
Dans la pratique les choses sont plus compliquées, parfois on développe seul ou on écrit soi-même les tests qui garantissent l’intégrité d'une nouvelle fonctionnalité dans un projet collaboratif.
Quoi qu’il arrive, un test peu efficace vaudra toujours mieux que pas de test du tout. Le but étant de prendre l’habitude d’en écrire et d’être objectif dans leur rédaction.
Le TDD tend à se démocratiser et requiert l’effort de chacun pour devenir un
standard. Tout développeur soucieux de son environnement et de son héritage doit
se poser sérieusement la question.
Les frameworks de tests, les guides et les documentations sur le sujet fleurissent,
vous pouvez donc vous lancer sans crainte.
On peut découper le TDD en 5 étapes distinctes :
Pour simplifier cette logique on peut regrouper ces cinq étapes en trois grandes idées :
Bill Wake définit ainsi la méthode
3A, pour Arrange, Act,
Assert (Arranger, Agir, Affirmer).
Il insiste sur le fait que la méthode ne définit pas un ordre immuable, l’affirmation
peut ainsi venir avant l’action, etc...
Pour la série de tests suivante on utilisera EcmaScript 6 et la méthode
.assert()
de
la console navigateur: Vous pourrez ainsi reproduire ces tests vous-même.
Objectif : Ecrire une fonction countWords()
qui compte les mots d'une phrase.
ITERATION 1 : écriture et échec du test initial
On écrit tout d'abord une affirmation de base.
console.assert(countWords("") === 0, "test 0: le texte ne contient aucun mot");
Uncaught ReferenceError: countWords is not defined
Après exécution la console rejette le test. On doit d'abord définir
countWords()
.
const countWords = () => {};
console.assert(countWords("") === 0, "test 0: le texte ne contient aucun mot");
Assertion failed: test 0: le texte ne contient aucun mot
countWords()
est définie et le test échoue mais l'erreur a changé.
Il faut à présent définir la logique du cœur de notre fonction.
const countWords = text => text || 0;
console.assert(countWords("") === 0, "test 0: le texte ne contient aucun mot");
undefined
La console ne renvoie rien, le test est donc passé.
countWords()
étant très simple nous omettrons les phases d'optimisation. On peut aussi considérer les itérations suivantes comme des optimisations.
ITERATION 2 : test pour les phrases d'un seul mot
Très bien. Essayons à présent une phrase d'un seul mot.
const countWords = text => text || 0;
console.assert(countWords("") === 0, "test 0: le texte ne contient aucun mot");
console.assert(countWords("nope") === 1, "test 1: le texte contient 1 mot");
Assertion failed: test 1: le texte contient 1 mot
countWords()
ne compte pas correctement, ajoutons le code suffisant pour
passer le test.
const countWords = text => (text ? text.split(" ").length : 0);
console.assert(countWords("") === 0, "test 0: le texte ne contient aucun mot");
console.assert(countWords("nope") === 1, "test 1: le texte contient 1 mot");
undefined
Le test est passé, ajoutons un autre cas standard.
ITERATION 3 : test pour les phrases de plusieurs mots
const countWords = text => (text ? text.split(" ").length : 0);
console.assert(countWords("") === 0, "test 0: le texte ne contient aucun mot");
console.assert(countWords("nope") === 1, "test 1: le texte contient 1 mot");
console.assert(
countWords("tdd is so fun") === 4,
"test 2: le texte contient 4 mots",
);
undefined
Le nouveau test passe sans modification, on peut continuer.
ITERATION 4 : test pour les phrases contenant des espaces au début et à la fin
Vérifions à présent la robustesse de la fonction.
const countWords = text => (text ? text.split(" ").length : 0);
console.assert(countWords("") === 0, "test 0: le texte ne contient aucun mot");
console.assert(countWords("nope") === 1, "test 1: le texte contient 1 mot");
console.assert(
countWords("tdd is so fun") === 4,
"test 2: le texte contient 4 mots",
);
console.assert(
countWords(" so is skateboarding ") === 3,
"test 3: le texte contient 3 mots",
);
Assertion failed: test 3: le texte contient 3 mots
Aïe… notre fonction n'est pas assez solide. Corrigeons-la pour capter ce nouveau cas en supprimant les espaces inutiles avant et après le texte.
const countWords = text => (text ? text.trim().split(" ").length : 0);
console.assert(countWords("") === 0, "test 0: le texte ne contient aucun mot");
console.assert(countWords("nope") === 1, "test 1: le texte contient 1 mot");
console.assert(
countWords("tdd is so fun") === 4,
"test 2: le texte contient 4 mots",
);
console.assert(
countWords(" so is skateboarding ") === 3,
"test 3: le texte contient 3 mots",
);
undefined
Parfait, La fonction est améliorée ! Ajoutons quand même un dernier test pour être sur.
ITERATION 5 : test pour les phrases contenant un nombre inégal d'espaces entre les mots
const countWords = text => (text ? text.trim().split(" ").length : 0);
console.assert(countWords("") === 0, "test 0: le texte ne contient aucun mot");
console.assert(countWords("nope") === 1, "test 1: le texte contient 1 mot");
console.assert(
countWords("tdd is so fun") === 4,
"test 2: le texte contient 4 mots",
);
console.assert(
countWords(" so is skateboarding ") === 3,
"test 3: le texte contient 3 mots",
);
console.assert(
countWords(` I'm 28, I love $#@! and multi-spaces `) === 7,
"test 4: le texte contient 7 mots",
);
Assertion failed: test 4: le texte contient 7 mots
Et mince… Encore un cas particulier, modifions l'algorithme en conséquence. On doit ici retirer les espaces inutiles entre les mots.
const countWords = text =>
text
? text
.trim()
.replace(/\s+/g, " ")
.split(" ").length
: 0;
console.assert(countWords("") === 0, "test 0: le texte ne contient aucun mot");
console.assert(countWords("nope") === 1, "test 1: le texte contient 1 mot");
console.assert(
countWords("tdd is so fun") === 4,
"test 2: le texte contient 4 mots",
);
console.assert(
countWords(" so is skateboarding ") === 3,
"test 3: le texte contient 3 mots",
);
console.assert(
countWords(` I'm 28, I love $#@! and multi-spaces `) === 7,
"test 4: le texte contient 7 mots",
);
undefined
Le test final est passé sans que les précédents n'échouent.
On notera que l'écriture de tests est un processus itératif.
La phase d'optimisation implique l'écriture d'un nouveau test qui échoue et relance
donc une nouvelle itération.
Evidemment countWords()
est très largement sous-optimisée et ne couvre pas
tous les cas spéciaux. On aurait pu ajouter une vérification sur le paramètre
text
et compter avec une expression régulière comme ceci :
const countWords = text =>
typeof text === "string" && text.trim()
? text.match(/\S+\s{0,1}/g).length
: 0;
L'idée ici est que coder est un processus incrémental et que chaque nouveau cycle doit être initié par un besoin spécifique défini par un test dédié.
L'écriture des tests est simple : on décompose notre script en une suite d'affirmations correspondant chacune à une fonctionnalité précise de notre algorithme.
Grâce à ce processus on évite :
Une variante plus agnostique de la logique du développeur existe et permet à des intervenants externes de faire partie intégrante du processus créatif.
le BDD, Behaviour Driven Development (Développement Dirigé par le Comportement), permet de définir de manière compréhensible pour tous les intervenants les spécifications d’une fonctionnalité. Cela permet aussi aux développeurs de comprendre le comportement général sans évoquer les détails techniques. La discussion est donc facilitée entre les différents acteurs.
Pour illustrer cette variante adaptons l'exemple précédent :
Note : Pour exécuter ce type de code vous aurez besoin d'un test-runner comme Jest, Mocha ou Karma.
const countWords = text =>
text
? text
.trim()
.replace(/\s+/g, " ")
.split(" ").length
: 0;
describe("countWords()", () => {
it("doit traiter un texte vide", () => {
expect(countWords("")).toBe(0);
});
it("doit traiter un texte d'un seul mot", () => {
expect(countWords("nope")).toBe(1);
});
it("doit traiter un texte de n mots", () => {
expect(countWords("tdd is so fun")).toBe(4);
});
it("doit traiter un texte avec des espaces aux extrémités", () => {
expect(countWords(" so is skateboarding ")).toBe(3);
});
it("doit traiter un texte avec des espaces inégaux entre les mots", () => {
expect(countWords(` I'm 28, I love $#@! and multi-spaces `)).toBe(7);
});
});
Voici le résultat du run :
La relecture est simplifiée pour tous les participants non techniques.
Le TDD est destiné à être incorporé à un processus
d'Intégration Continue pour s'assurer
du bon fonctionnement de l'application sur tous les environnements de production
après chaque nouveau commit
.
J'espère vous avez apprécié la démo et que ça vous a donné envie de tester le TDD pour apporter équilibre et harmonie à votre code.
Dans tous les cas, je peux vous certifier que les autres codeurs vous en seront reconnaissants, croyez-moi.