Aller au contenu principal

Tests et TDD : sortir de la peur de toucher au code qui marche

Tu as livré une fonctionnalité la semaine dernière. Aujourd'hui, on te demande une petite modification dessus. Et tu hésites. Pas parce que tu ne sais pas faire, mais parce que tu n'as aucun moyen de vérifier que tu ne casses rien d'autre. C'est exactement ce problème que les tests automatisés résolvent.

Guides & tutoriels · ·
Adel LATIBI
Adel LATIBI

Le Briefing Dev - les ressources et actus de la semaine, droit dans ta boîte chaque vendredi gratuitement.

En vous inscrivant, vous acceptez de recevoir notre newsletter. Désinscription possible à tout moment.

La plupart des développeurs juniors écrivent du code sans tests. Pas par paresse, par habitude. Les tutoriels n'en parlent presque jamais, les écoles les survolent, et les premiers projets perso fonctionnent très bien sans. La facture arrive plus tard, quand le projet grossit, quand quelqu'un d'autre y touche, quand il faut revenir sur son propre code six mois après.

Les termes circulent partout : tests unitaires, TDD, couverture de code, tests d'intégration. La frontière entre les quatre reste floue pour beaucoup de gens qui apprennent à coder, et l'idée que tout ça serait réservé aux gros projets ou aux développeurs séniors a la vie dure.

Les tests ont été présentés pendant des années comme une discipline d'expert, une case à cocher pour équipes matures, alors qu'ils servent d'abord au confort de la personne qui écrit le code. Cet article explique ce qu'il faut comprendre pour commencer à tester sans avoir l'impression de perdre son temps, ce qu'est le TDD une fois débarrassé de son folklore, et comment déléguer la partie répétitive à un outil quand la base de code est déjà écrite.

Pyramide des tests : tests unitaires à la base, tests d'intégration au milieu, tests end-to-end au sommet

Le problème : la peur de modifier son propre code

La scène revient dans presque tous les projets sans tests. Tu reprends un fichier écrit il y a trois mois. Il fait 200 lignes. Tu dois ajouter une option, juste un petit paramètre supplémentaire. Tu fais la modification, tu relances l'application, tu testes à la main le cas que tu viens d'ajouter. Ça marche. Tu pushes.

Deux jours après, un collègue ou un utilisateur signale qu'une autre fonctionnalité ne marche plus. Une fonctionnalité que personne n'avait touchée, en apparence. En réalité, la fonction modifiée était utilisée à six endroits, et un seul de ces endroits a été vérifié.

La peur de casser quelque chose finit par bloquer toute amélioration, et c'est le vrai coût de l'absence de tests, bien plus que les bugs eux-mêmes. On préfère dupliquer du code plutôt que toucher à l'existant. On laisse pourrir les vieux bouts du projet parce que personne n'ose y revenir. La dette technique s'installe exactement sur ce terrain.

Le principe : un test, c'est du code qui vérifie du code

Un test n'a rien de magique. C'est une petite fonction qui appelle ton code avec des entrées précises et qui vérifie que la sortie est celle attendue. Rien de plus.

Quand tu écris un test, tu transformes une vérification manuelle (ouvrir le navigateur, taper trois valeurs, regarder le résultat) en une vérification automatique que la machine rejoue en une fraction de seconde, autant de fois que tu veux. Toute la promesse des tests tient là : remplacer la fatigue du test manuel par une routine fiable.

Les familles de tests à distinguer

Les noms varient selon les communautés, mais on retrouve toujours les mêmes niveaux :

  • Tests unitaires. Tu vérifies une seule fonction ou une seule méthode, isolée du reste. Rapides à écrire, rapides à exécuter, ils forment la majorité de tes tests.
  • Tests d'intégration. Tu vérifies que plusieurs morceaux fonctionnent ensemble : une route API avec sa base de données, par exemple. Plus lents, mais ils attrapent les bugs que les tests unitaires laissent passer.
  • Tests end-to-end. Tu simules un utilisateur qui clique dans ton application. Très lents, fragiles, utiles sur les parcours critiques comme le paiement ou la connexion.

Pour commencer, oublie les deux derniers niveaux. Les tests unitaires représentent l'essentiel de l'apprentissage et l'essentiel du bénéfice quotidien.

Ce que recouvre le TDD

Cycle TDD Red Green Refactor : écrire un test qui échoue, écrire le code minimum, nettoyer

Test Driven Development veut dire "développement piloté par les tests". La pratique a été popularisée par Kent Beck au début des années 2000, dans le sillage de l'Extreme Programming. L'idée tient en une ligne : le test s'écrit avant le code. Pas après. Pas en même temps. Avant.

Le cycle s'appelle "Red, Green, Refactor". Trois étapes courtes que tu répètes :

  1. Red. Tu écris un test qui décrit ce que tu veux. Tu le lances, il échoue puisque le code n'existe pas encore.
  2. Green. Tu écris le code minimum pour faire passer le test. Pas plus.
  3. Refactor. Tu nettoies le code maintenant que tu sais qu'il marche. Le test reste vert, c'est ton filet de sécurité.

Le TDD n'est pas une religion, et beaucoup de développeurs séniors ne l'appliquent pas à la lettre sur l'ensemble de leur code. Comprendre la mécanique rend meilleur même quand on choisit de ne pas la suivre.

Deux exemples : JavaScript et Python

Passons du discours au code, avec une fonction facile à tester : un calcul de prix avec remise.

Avec Vitest en JavaScript

// prix.js
export function calculerPrix(montant, remise) {
  if (remise < 0 || remise > 100) {
    throw new Error("Remise invalide");
  }
  return montant * (1 - remise / 100);
}

// prix.test.js
import { describe, it, expect } from "vitest";
import { calculerPrix } from "./prix.js";

describe("calculerPrix", () => {
  it("applique une remise de 20 %", () => {
    expect(calculerPrix(100, 20)).toBe(80);
  });

  it("retourne le montant initial si remise nulle", () => {
    expect(calculerPrix(100, 0)).toBe(100);
  });

  it("rejette une remise négative", () => {
    expect(() => calculerPrix(100, -5)).toThrow();
  });
});

Tu lances npx vitest, tu vois passer trois lignes vertes. À la prochaine modification de calculerPrix, ces trois assertions seront rejouées automatiquement.

Avec pytest en Python

# prix.py
def calculer_prix(montant, remise):
    if remise < 0 or remise > 100:
        raise ValueError("Remise invalide")
    return montant * (1 - remise / 100)

# test_prix.py
import pytest
from prix import calculer_prix

def test_remise_de_vingt_pour_cent():
    assert calculer_prix(100, 20) == 80

def test_remise_nulle():
    assert calculer_prix(100, 0) == 100

def test_remise_negative_leve_une_erreur():
    with pytest.raises(ValueError):
        calculer_prix(100, -5)

Tu lances pytest, et tu obtiens la même boucle de sécurité. Le vocabulaire change un peu d'un langage à l'autre, le principe reste rigoureusement identique. Si tu travailles surtout côté Python, la formation Créer une API REST avec Python et FastAPI pousse ces tests jusqu'au niveau des routes HTTP.

La règle Arrange-Act-Assert

Un bon test se lit en trois temps. Tu prépares les données dont tu as besoin (Arrange). Tu exécutes la fonction testée (Act). Tu vérifies le résultat (Assert). Cette structure rend un test compréhensible même six mois plus tard, et elle évite le test fourre-tout qui vérifie quinze choses à la fois sans qu'on sache laquelle a cassé quand il devient rouge.

def test_panier_calcule_le_total():
    # Arrange
    panier = Panier()
    panier.ajouter("livre", 15)
    panier.ajouter("stylo", 3)

    # Act
    total = panier.total()

    # Assert
    assert total == 18

Les pièges fréquents quand on débute avec les tests

Vouloir tout tester d'un coup

Lire un article comme celui-ci, se motiver, décider de couvrir 100 % d'un projet existant : trois heures plus tard, l'élan retombe. Commence par tester une seule fonction utilitaire. Puis une autre. La couverture monte par accumulation, jamais par sprint forcé.

Tester l'implémentation au lieu du comportement

Un test qui vérifie "la fonction appelle bien tel module interne" se casse dès que l'organisation du code change. Un test qui vérifie "pour cette entrée, j'obtiens cette sortie" survit aux refactorings. Teste ce que le code fait, pas comment il le fait.

Confondre couverture de code et qualité des tests

Atteindre 90 % de couverture ne veut pas dire que le code est protégé. Un test peut exécuter toute une fonction sans jamais vérifier le résultat. La couverture est un indicateur faible, pas un objectif. Dix tests pertinents valent mieux que cinquante tests décoratifs.

Écrire des tests dépendants les uns des autres

Si le test B échoue parce que le test A a modifié une variable globale, tu as un problème. Chaque test doit pouvoir s'exécuter seul, dans n'importe quel ordre. Réinitialise les états dans des fixtures ou des hooks dédiés.

Penser que le TDD ralentit

Au début, oui, tu vas perdre du temps. Au bout de quelques semaines, tu en gagnes : moins de débogage, des refactorings sans stress, des mises en production plus calmes. Le gain est invisible sur un projet d'une semaine, il devient énorme sur un projet d'un an. C'est aussi ce genre de réflexe que cherchent les recruteurs, comme le détaille l'article sur l'entretien technique junior.

Automatiser l'écriture des tests avec Claude Code

Une fois le réflexe acquis, la question devient pratique : comment limiter le temps passé à écrire des tests sans renoncer à leur qualité ? Sur un projet existant sans couverture, rattraper la dette à la main décourage rapidement. Claude Code, l'outil en ligne de commande d'Anthropic, génère des tests directement depuis le terminal en s'appuyant sur le contexte du projet.

Génération à la demande

La commande la plus directe, depuis la racine du projet :

claude "génère les tests unitaires pour src/services/user.service.ts en suivant les conventions du projet"

Claude Code lit le fichier ciblé, identifie le framework de test utilisé (Jest, Vitest, pytest, PHPUnit) et calque le style sur les tests existants. Pour un résultat exploitable, au moins un ou deux fichiers de tests doivent déjà servir de référence.

Le fichier CLAUDE.md comme socle

Pour industrialiser, le fichier CLAUDE.md placé à la racine définit les règles à respecter à chaque génération :

## Tests

- Framework : Vitest
- Localisation : à côté du fichier source, suffixe .test.ts
- Pattern : AAA (Arrange, Act, Assert)
- Couverture minimum : branches principales + cas d'erreur
- Mocks : utiliser vi.mock, jamais de vrais appels réseau
- Nommage : describe('NomClasse') puis it('should ... when ...')

## Commandes utiles
- Lancer les tests : pnpm test
- Coverage : pnpm test:coverage

Une fois ce fichier en place, une simple commande claude "ajoute les tests manquants pour ce fichier" produit un résultat aligné sans répéter les consignes à chaque appel.

Commandes personnalisées

Dans le dossier .claude/commands/, des fichiers Markdown deviennent des commandes réutilisables. Exemple avec .claude/commands/test-file.md :

Génère les tests pour le fichier $ARGUMENTS.

Règles :
1. Lis d'abord un test existant du projet pour calquer le style
2. Couvre les cas nominaux et les cas d'erreur
3. Mocke toutes les dépendances externes
4. Lance les tests après génération et corrige si échec

Utilisation : /test-file src/services/auth.service.ts

Boucle agentique : génération puis correction

Claude Code peut exécuter les tests qu'il vient d'écrire et corriger les échecs en boucle. Le prompt type :

claude "écris les tests pour le module facturation, exécute-les avec pnpm test, et corrige jusqu'à ce que tout passe"

Cette approche fonctionne bien sur du code pur, les calculs et les transformations de données. Sur du code avec beaucoup d'entrées/sorties, il faut surveiller les mocks générés, qui ont tendance à devenir excessifs.

Limites à connaître

Un modèle génère des tests qui passent, pas forcément des tests qui détectent les bugs. La relecture reste nécessaire pour vérifier que les assertions portent sur le bon comportement. Sur du code legacy mal découpé, la génération produit souvent des mocks excessifs qui rendent les tests fragiles, et dans ce cas mieux vaut refactorer d'abord, tester ensuite.

L'objectif n'est pas que l'outil écrive tous les tests à ta place, mais qu'il prenne en charge la partie répétitive pour te laisser le jugement sur ce qui compte.

Par où commencer

Le piège classique consiste à attendre "le bon projet" ou "le bon moment". Comme le décrit l'article sur le syndrome du tutoriel infini, ce moment n'arrive jamais s'il n'est pas provoqué. Voici la séquence minimale :

  1. Installe le framework de test natif de ton langage : Vitest ou Jest pour JavaScript, pytest pour Python, PHPUnit pour PHP, JUnit pour Java.
  2. Repère dans ton projet une fonction pure, qui prend des paramètres et retourne un résultat sans effet de bord. Écris trois tests dessus.
  3. Configure ton éditeur pour relancer automatiquement les tests à chaque sauvegarde. La boucle de feedback rapide est ce qui rend la pratique addictive.
  4. Prends le prochain bug signalé sur ton projet et écris d'abord un test qui le reproduit, avant de le corriger. C'est le meilleur exercice de TDD qui existe, parce que le résultat attendu est déjà connu.
  5. Branche tes tests sur ta chaîne d'intégration. Si tu héberges ton code sur GitHub, la formation CI/CD avec GitHub Actions montre comment automatiser cette étape proprement.

Un code facile à tester est presque toujours un code bien découpé. Si tu veux creuser ce qui rend une fonction testable au départ, l'article sur le principe CQS et le hub des 20 principes de code sont des compléments directs. Une fonction qui lit sans modifier se teste en trois lignes, une fonction qui fait les deux demande une usine à gaz.

Ce que ça change au quotidien

Un projet sans tests devient progressivement intouchable. Un projet avec tests reste modifiable, y compris par quelqu'un qui découvre le code. Ce bénéfice compte plus que le nombre de bugs attrapés, qui est la métrique que tout le monde regarde en premier.

Le TDD appliqué à la lettre sur une base de code entière, je n'y crois pas, et peu d'équipes le tiennent plus de quelques mois. Sur les corrections de bugs, en revanche, écrire d'abord le test qui reproduit le problème est la seule méthode qui garantit qu'il ne reviendra pas dans six mois. Commence par là si tu ne dois retenir qu'une chose.

Trois tests utiles valent mieux que trente tests décoratifs. La maîtrise vient avec la répétition, et personne n'écrit de bons tests dès la première semaine, y compris les gens qui en parlent le mieux sur LinkedIn.

Questions fréquentes

Faut-il tester tout son code ?

Non. Concentre-toi sur la logique métier, les fonctions utilitaires, les calculs, les transformations de données. Ne perds pas de temps à tester du code purement structurel comme un simple getter ou une ligne de configuration. La règle pratique : teste ce qui pourrait casser silencieusement.

Quelle est la différence entre Vitest, Jest et Mocha ?

Ce sont trois frameworks de tests pour JavaScript. Jest est le plus ancien et reste très utilisé. Vitest est plus récent, plus rapide, et s'intègre nativement avec Vite. Mocha est minimaliste et demande de choisir séparément une bibliothèque d'assertions. Pour un nouveau projet en 2026, Vitest est en général le choix le plus simple.

Le TDD est-il tenable en entreprise ?

Oui, mais rarement à 100 %. La plupart des équipes pratiquent un TDD partiel : sur les fonctions critiques, les bugs identifiés, les nouveaux modules. Le TDD complet sur l'ensemble du code reste une exception. L'important est d'avoir une couverture automatisée, peu importe l'ordre dans lequel les tests ont été écrits.

Quel pourcentage de couverture viser ?

Entre 70 et 85 % sur la logique métier est un objectif raisonnable. Au-delà, tu commences à tester des choses sans valeur ajoutée. La couverture n'est pas un objectif en soi, c'est un indicateur. Mieux vaut 60 % de tests qui vérifient des comportements réels que 95 % de tests décoratifs.

Peut-on apprendre les tests sans avoir de projet en cours ?

Oui, mais c'est moins efficace. Les tests prennent leur sens quand ils protègent du vrai code, dans un vrai projet. Pour démarrer, prends une petite application personnelle (une todo list, un convertisseur, une calculatrice) et ajoute des tests dessus. L'apprentissage par projet réel reste supérieur aux exercices isolés.

Peut-on faire écrire ses tests par une IA quand on débute ?

Oui, à condition de savoir lire ce qui sort. Un test généré qui passe du premier coup peut ne rien vérifier d'utile, ou vérifier l'implémentation plutôt que le comportement. La méthode qui marche pour apprendre : écris toi-même les deux ou trois premiers tests d'un fichier, puis laisse l'outil compléter les cas restants en calquant ton style. Tu gardes le contrôle sur ce qui est réellement vérifié.

Faut-il connaître la POO pour faire du TDD ?

Non. Le TDD s'applique à des fonctions, à des classes, à des modules. Tu peux en faire avec des fonctions pures sans jamais écrire de classe. Cela dit, la POO facilite certains patterns de tests comme l'injection de dépendances. Pour débuter, teste des fonctions simples, la POO viendra ensuite.
Crédits images : vector4stock sur Magnific Tous les crédits

Vous êtes expert ?

Partagez votre expertise sur notre blog

Tutoriel, retour d'expérience, analyse - publiez un article invité et gagnez en visibilité.

Écrire pour nous