Tester pour pouvoir refactorer
Sans tests, pas de refactoring sûr : du test manuel au framework, en passant par le TDD et la boucle rouge-vert-refactor.
Tout le livre d'Evan Burchard repose sur une thèse simple et radicale : le refactoring est impossible sans tests. Refactorer, c'est améliorer la structure du code sans changer son comportement. Or, comment garantir qu'un comportement n'a pas bougé si rien ne le vérifie ? Sans filet, vous ne refactorez pas : vous modifiez du code et vous croisez les doigts. Les tests sont précisément ce qui transforme la modification anxieuse en transformation maîtrisée.
Ce chapitre suit la progression du livre : il commence par le pourquoi du test, puis explore les comment par sophistication croissante — du test manuel jusqu'au framework, en passant par un mini-assert fait main. Il se termine par la boucle rouge-vert-refactor appliquée pas à pas à une vraie fonction JavaScript. Les exemples sont en JavaScript (var/let/const, fonctions, modules node), fidèles aux idiomes du livre.
Pourquoi tester, vraiment
Burchard balaie d'emblée les objections habituelles (« le client paie des fonctionnalités, pas des tests ») pour rappeler le but profond du test : la confiance (confidence). On ne teste pas pour cocher une case ; on teste pour pouvoir regarder une base de code et savoir, plutôt que deviner, qu'elle se comporte comme prévu. Cette confiance se forge dans le scepticisme acquis en voyant du code qui se trompe et résiste au changement.
Le livre énumère plusieurs raisons concrètes, dont voici les plus marquantes.
- Vous testez déjà. Si vous ouvrez la console ou rechargez une page pour vérifier un comportement, vous testez — simplement de façon lente et faillible. Automatiser, c'est juste rendre ce geste répétable.
- Le refactoring en dépend. On ne peut garantir le comportement sans le mesurer, donc sans tester. Et améliorer la qualité du code, c'est tout l'objet du livre.
- Les tests documentent (démontrent) le code. À défaut d'une vraie documentation, un test montre le comportement attendu d'une fonction, exemple à l'appui.
- La boucle de rétroaction (feedback loop) se resserre. Sans tests, le délai entre écrire du code et savoir s'il marche s'allonge. Avec eux, il tombe à quelques secondes — et plus la boucle est courte, plus vous vérifiez souvent, et plus vous osez changer le code.
À retenir
Le but n'est jamais « avoir des tests » : le but est la confiance. Les tests sont une façon pratique de produire une confiance transmissible — au reste de l'équipe, et au « vous du futur ». La couverture (coverage) n'est qu'un moyen.
Le test manuel, et ses limites
Instinctivement, tout le monde veut vérifier son code. Sur une application web, cela signifie charger la page et cliquer un peu ; ailleurs, glisser un console.log pour s'assurer qu'une variable a la bonne valeur. Burchard reconnaît la valeur du test manuel (manual testing) : il est excellent pour explorer et déboguer, et il fait partie du spiking, cette phase de recherche exploratoire qui précède parfois un cycle rouge-vert-refactor.
Mais ses limites sont sévères. Le test manuel est lent, oubliable et non répétable : il repose sur votre mémoire et sur votre présence. Voici le réflexe typique du livre : on parsème le code de console.log pendant qu'on l'écrit.
// ❌ Avant : test manuel, sortie non structurée.
console.log(checkHand(['2-H', '3-C', '4-D', '5-H', '2-C']));
console.log(checkHand(['3-H', '3-C', '3-D', '5-H', '2-H'])); Dès qu'on en a huit ou dix, on ne sait plus ce que chaque ligne signifie. On ajoute alors des préfixes pour s'y retrouver — et l'on se met, sans le savoir, à réimplémenter péniblement un test runner : avec très peu de fonctionnalités, beaucoup de duplication et aucune cohérence.
// ❌ On bricole un "runner" à la main.
console.log('value of checkHand is ' +
checkHand(['2-H', '3-C', '4-D', '5-H', '2-C'])); Attention
Symptôme révélateur : la programmation devient un grand huit émotionnel, entre jurons et poings levés. Être surpris que le code marche (ou pas) trahit une boucle de rétroaction trop lâche. Avec de petits pas, des tests fréquents et du contrôle de version, la surprise disparaît — et avec elle, beaucoup de temps perdu.
Le test manuel documenté
Un premier pas vers l'automatisation consiste à documenter le test manuel : rédiger un plan de QA, une liste détaillée d'étapes couvrant les chemins de code utiles. Ce n'est pas automatisé, mais c'est répétable, distribuable dans l'équipe (comme une checklist) et bien moins faillible que la mémoire. Burchard le présente comme la meilleure option de secours quand un gros déploiement approche avec une suite de tests incomplète. Même avec une bonne couverture, certains systèmes vitaux peuvent justifier un smoke test manuel.
Les qualités d'un bon test, et les types de tests
Chaque test, quelle que soit sa forme, comporte trois phases : la mise en place (setup), l'assertion, puis le démontage (teardown). Un bon test vise quatre qualités : il est déterministe (même entrée, même résultat), rapide (pour resserrer la boucle), isolé (indépendant des autres) et lisible (il sert de documentation).
Pour le refactoring, deux familles dominent.
| Type | Portée | Vitesse | Mocking / stubbing |
|---|---|---|---|
| Test bout-en-bout (end-to-end) | Haut niveau, intégration des parties | Lent | À éviter sauf nécessité |
| Test unitaire (unit test) | Bas niveau, une « unité » | Rapide | Plus libre |
Le test bout-en-bout automatise ce qu'un testeur manuel ferait à travers l'interface : s'inscrire, cliquer, télécharger un fichier. Il exerce les composants en collaboration, donc on évite d'y simuler (mocking/stubbing) sauf pour le système de fichiers ou les requêtes distantes. Le test unitaire, lui, se concentre sur les entrées/sorties d'une unité — en JavaScript, un fichier, un module, une fonction ou un objet. On y simule plus librement les points d'intégration pour rester rapide et focalisé sur les détails.
Burchard recommande de séparer une suite rapide (unitaire, lancée souvent) d'une suite lente (bout-en-bout). Un piège fréquent : organiser les tests par dossier d'application finit par mélanger les deux dans une seule grosse suite lente, qu'on ne lance plus assez.
Note
Test fonctionnel vs non fonctionnel. Le test non fonctionnel (nonfunctional testing) couvre la performance, l'utilisabilité, la sécurité, l'accessibilité, la localisation. Il est crucial — ignorer l'accessibilité, c'est ignorer des gens, « entre méchant et illégal » — mais il ne contribue pas directement à la confiance que le code fonctionne, qui seule autorise le refactoring. C'est pourquoi le livre se concentre sur les tests unitaires et bout-en-bout.
D'autres catégories croisent ces niveaux : les tests de fonctionnalité (feature tests) pour le neuf, les tests de régression (regression tests) qui reproduisent un bug avant de le corriger, et les tests de caractérisation (characterization tests) qu'on écrit pour couvrir du code existant non testé.
Outils et processus de qualité
Le test est un outil de confiance, mais pas le seul. Avant tout, rappelle Burchard, le projet doit être sous contrôle de version (version control) avec une sauvegarde distante : aucune qualité de code ne compte s'il peut disparaître. Côté processus, un guide de style dont les règles sont, idéalement, exécutables (« utiliser const et let plutôt que var », « tout code neuf doit être testé »), la revue de code (code review) et la programmation en binôme (pair programming) créent des occasions répétées de viser la qualité.
Côté outils, le livre passe en revue ceux qui resserrent la boucle ou révèlent les angles morts.
- Frameworks de test : ils définissent comment écrire les fichiers de test et fournissent un runner qui exécute la suite, gère setup/teardown et affiche les échecs (le livre utilise Mocha).
- Bibliothèques d'assertion/expectation : elles permettent d'affirmer qu'une fonction retourne telle valeur (le livre privilégie une lib minimale, wish).
- Vérificateurs de style, alias linters : ils inspectent le code sans l'exécuter pour repérer erreurs et écarts de style ; au mieux, ils sont un guide de style exécutable.
- Rapporteurs de couverture (coverage reporters) : pour savoir si le code est sûr à refactorer, il faut savoir s'il est suffisamment couvert.
- Loaders et watchers : un watcher relance la suite à chaque sauvegarde, ce qui réduit drastiquement la boucle de rétroaction.
Un assert fait main, pour démystifier
Avant de sortir l'artillerie, le livre nous fait transformer nos console.log en assertions. La transformation est minuscule mais décisive : au lieu d'imprimer puis de comparer à l'œil, on affirme le résultat attendu. Node fournit un assert natif, sans aucune installation.
// ❌ Avant : on imprime, et on lit nous-mêmes la sortie.
console.log(checkHand(['2-H', '3-C', '4-D', '5-H', '2-C']));
// ✅ Après : on affirme. Une erreur jaillit si c'est faux.
var assert = require('assert');
assert(checkHand(['2-H', '3-C', '4-D', '5-H', '2-C']) === 'pair');
assert(checkHand(['3-H', '3-C', '3-D',
'5-H', '2-H']) === 'three of a kind'); Le hic d'assert, c'est son message d'erreur opaque : AssertionError: false == true. On peut passer un second paramètre descriptif, mais c'est vite fastidieux. Pour comprendre ce qu'un assert est vraiment, construisons-en un nous-mêmes : il n'y a rien de magique, juste un test d'égalité enveloppé qui lève une erreur.
// Un assert "fait main" : rien de plus qu'un if + un throw.
function assert(passe, message) {
if (!passe) {
throw new Error(message || 'Assertion échouée');
}
}
assert(1 + 1 === 2); // ne fait rien : tout va bien
assert(checkHand(['2-H', '3-C', '4-D', '5-H', '2-C']) === 'pair', 'la main devrait être une paire'); C'est tout le secret : une assertion, c'est du code qui jette une erreur quand une condition est fausse. Les bibliothèques élaborées n'ajoutent que du confort, surtout de meilleurs messages.
Astuce
Burchard se méfie des syntaxes d'assertion alambiquées. assert.equal(foo, 'bar'), expect(foo).to.equal('bar'), foo.should.equal('bar') : trois façons de dire la même chose. JavaScript possède déjà un test d'égalité parfait. Tout ce qu'il faut, c'est l'envelopper. C'est pourquoi le livre adopte wish, qui produit un message clair sans second paramètre : Expected "foo" to be equal(===) to "'bar'".
Passer à un vrai framework
Tant qu'on a deux ou trois assertions, le fichier brut suffit. Mais dès qu'on descend en niveaux de tests, avec plusieurs échecs simultanés persistant un moment, les assert nus deviennent confus. C'est le moment de s'outiller. Mocha apporte deux fonctions structurantes : describe, qui indique quoi on teste, et it, qui contient les assertions d'un cas. Les tests sont les mêmes — seule leur organisation change.
// ❌ Avant : assertions à plat, sans contexte ni regroupement.
var wish = require('wish');
wish(checkHand(['2-H', '3-C', '4-D', '5-H', '2-C']) === 'pair');
wish(checkHand(['3-H', '3-C', '3-D',
'5-H', '2-H']) === 'three of a kind');
// ✅ Après : structuré, lisible, exécutable par `mocha`.
var wish = require('wish');
describe('checkHand()', function () {
it('handles pairs', function () {
var result = checkHand(['2-H', '3-C', '4-D', '5-H', '2-C']);
wish(result === 'pair');
});
it('handles three of a kind', function () {
var result = checkHand(['3-H', '3-C', '3-D', '5-H', '2-H']);
wish(result === 'three of a kind');
});
}); On lance la suite avec mocha check-hand.js, et la sortie est nette. Mieux : mocha -w surveille les fichiers et relance la suite à chaque sauvegarde — la boucle de rétroaction tombe à quelques secondes.
Astuce
Plusieurs assertions peuvent cohabiter dans un même bloc it ; si l'une échoue, tout le bloc échoue. Et si vous nommez votre fichier test.js (ou le placez dans un dossier test), un simple mocha le trouvera tout seul.
La boucle rouge-vert-refactor
Le développement piloté par les tests (test-driven development, TDD) inverse l'ordre habituel : on écrit le test avant le code. Sa colonne vertébrale est le cycle rouge-vert-refactor.
ROUGE → VERT → REFACTOR
écrire un test écrire le minimum améliorer la structure
qui échoue pour le faire passer sans changer le comportement
↑___________________________________________________| - Rouge : écrire un test qui échoue. L'échec prouve que le test teste réellement quelque chose, et indique exactement quoi faire ensuite.
- Vert : écrire le minimum de code pour faire passer le test. Pas plus. Si TDD est suivi strictement, aucune ligne n'est écrite sans être couverte — donc aucune ligne ne peut échapper au refactoring.
- Refactor : maintenant que les tests sont au vert, améliorer la structure à l'abri. C'est ici, et nulle part ailleurs, qu'on paie la dette technique accumulée.
La grande vertu de cette boucle, résume Burchard : votre premier jet n'a pas à être votre meilleur jet. Une fois les tests verts, vous avez toute liberté de rendre le code meilleur ensuite. Une métaphore du livre l'illustre bien : le TDD, c'est faire de la soupe — on goûte au fur et à mesure (boucle courte) — pas de la pâtisserie, où l'on attend 45 minutes avant de savoir si c'est raté.
Note
Créer un nouveau test rouge juste après un vert n'est conseillé qu'après avoir au moins envisagé une phase de refactoring. Si vous enchaînez tout de suite, rien ne signale qu'il restait du travail à soigner.
Un cycle TDD complet, pas à pas
Suivons le déroulé du livre sur la fonction checkHand, qui nomme une main de poker. Rouge d'abord : on écrit le test, le code n'existe pas encore.
// ❌ ROUGE : checkHand n'existe pas.
var wish = require('wish');
wish(checkHand(['2-H', '3-C', '4-D', '5-H', '2-C']) === 'pair');
// → ReferenceError: checkHand is not defined L'erreur nous dit exactement quoi faire : créer la fonction. Vert : on écrit le strict minimum pour passer. Une seule assertion en jeu (une paire), donc le minimum est… de retourner la bonne chaîne.
// ✅ VERT : le minimum pour passer l'unique test.
var checkHand = function () {
return 'pair';
};
var wish = require('wish');
wish(checkHand(['2-H', '3-C', '4-D', '5-H', '2-C']) === 'pair'); Cela paraît absurde, mais c'est précisément l'esprit du TDD : ne pas écrire de code que les tests n'exigent pas encore. On ajoute alors un second test (un brelan), qui passe au rouge et nous force à généraliser. La tentation est d'écrire du code « pas touche, t'as pas le droit » :
// ❌ Code "fragile" (brittle) : il triche pour passer le test.
var checkHand = function (hand) {
if (hand[0] === '2-H' && hand[1] === '3-C'
&& hand[2] === '4-D' && hand[3] === '5-H'
&& hand[4] === '2-C') {
return 'pair';
} else {
return 'three of a kind';
}
}; Techniquement, il passe ; mais il casse à la moindre nouvelle main. C'est du code fragile (brittle). Pour le casser pour de bon — et donc justifier une vraie implémentation — on ajoute une autre paire dans le test. Le rouge réapparaît, et l'on conçoit enfin l'interface souhaitée, en s'appuyant sur des fonctions qu'on implémentera ensuite à leur tour.
// ✅ VERT (par étapes) : on dessine l'interface idéale.
function checkHand(hand) {
if (isPair(hand)) {
return 'pair';
} else {
return 'three of a kind';
}
}
function isPair(hand) {
return multiplesIn(hand) === 2; // une fonction qu'on testera
}
function multiplesIn(hand) {
return highestCount(valuesFromHand(hand));
} Chaque nouvelle fonction non définie provoque un nouvel échec — un nouveau rouge — qui guide l'étape suivante. On descend ainsi de checkHand vers multiplesIn, valuesFromHand, highestCount, en écrivant à chaque fois un test qui décrit l'interface voulue avant de coder. Le code obtenu est souvent laid, et c'est très bien :
// ✅ VERT : moche mais correct. Le refactoring viendra après.
function highestCount(values) {
var counts = {};
values.forEach(function (value) {
counts[value] = 0;
if (value == values[0]) counts[value] += 1;
if (value == values[1]) counts[value] += 1;
if (value == values[2]) counts[value] += 1;
if (value == values[3]) counts[value] += 1;
if (value == values[4]) counts[value] += 1;
});
var totals = Object.keys(counts).map(function (k) {
return counts[k];
});
return totals.sort(function (a, b) { return b - a; })[0];
} Piège courant
Il n'y a rien de mal à écrire du code terrible — temporairement. Dupliquer est souvent le pas le plus petit et le plus sûr : mieux vaut copier-coller que tenter une extraction qui casse dix choses à la fois (surtout si vous vous êtes éloigné de votre dernier commit). Le vrai problème, c'est de laisser la duplication. On la traite dans la phase refactor, jamais pendant qu'on cherche à passer au vert.
Quand le test révèle un bug : la régression
Pendant ce cycle, on introduit un bug : la vérification de couleur (flush) renvoie true à tort. La réaction TDD est limpide. On reproduit d'abord le bug avec un test — qui doit échouer — avant de toucher au code.
// ❌ ROUGE volontaire : ce test reproduit le bug.
it('reports false if elements are not the same', function () {
var result = allTheSameSuit(['D', 'H', 'D', 'D', 'D']);
wish(!result); // attendu : false, or on obtient true
}); L'échec confirme qu'on a bien capturé le bug. La cause : un return false à l'intérieur d'un forEach ne sort que du callback, pas de la fonction. On corrige, et les deux tests repassent au vert.
// ❌ Avant : le return ne sort que du callback.
function allTheSameSuit(suits) {
suits.forEach(function (suit) {
if (suit !== suits[0]) {
return false; // ne quitte pas allTheSameSuit !
}
});
return true;
}
// ✅ Après : on accumule, puis on retourne.
function allTheSameSuit(suits) {
var toReturn = true;
suits.forEach(function (suit) {
if (suit !== suits[0]) {
toReturn = false;
}
});
return toReturn;
} Le test de régression reste désormais dans la suite : ce bug-là ne reviendra plus sans être immédiatement signalé.
Trois niveaux à garder en tête
Le livre rappelle qu'au-delà de la boucle, les tests se déclinent en niveaux complémentaires.
- Unitaire : une fonction isolée, entrées/sorties. Rapide, on y simule librement (
isPair,valuesFromHand). - Intégration / bout-en-bout : plusieurs unités en collaboration. Plus lent, peu de simulation (
checkHandqui orchestre tout). - Acceptation (acceptance) : du point de vue de l'utilisateur final, souvent via des API de haut niveau. Burchard met en garde contre les frameworks qui prétendent transformer « magiquement » des exigences en code accepté — l'ingénierie logicielle, c'est avant tout des humains qui se parlent.
Pour le refactoring, peu importe la méthodologie (TDD, BDD…) : ce qui compte, c'est une bonne couverture, idéalement par tests unitaires et bout-en-bout. Qu'ils aient été écrits « test d'abord » est un plus, pas une obligation.
À retenir
- Pas de tests, pas de refactoring. Les tests sont ce qui prouve que le comportement n'a pas changé ; sans eux, on modifie du code à l'aveugle. Le vrai but n'est pas la couverture mais la confiance transmissible.
- Montez en sophistication : du test manuel (lent, oubliable, non répétable) au test manuel documenté, puis aux assertions automatisées. Visez des tests déterministes, rapides, isolés et lisibles.
- Une assertion n'a rien de magique : c'est du code qui lève une erreur quand une condition est fausse. Un framework (
describe/it,wish/expect) n'ajoute que structure et messages clairs. - Boucle rouge-vert-refactor : écrire un test qui échoue, écrire le minimum pour le faire passer, puis refactorer à l'abri. Votre premier jet n'a pas à être le meilleur.
- Dupliquer pour avancer, factoriser pour finir : le code laid est acceptable dans la phase verte ; on le nettoie dans la phase refactor, jamais avant.
- Un bug ? Un test de régression d'abord : reproduisez-le par un test rouge, corrigez, gardez le test. Pensez en trois niveaux : unitaire, intégration/bout-en-bout, acceptation.