Aller au contenu principal

Mettre en place des tests automatisés sur un projet legacy

Photo d'Emmanuel BALLERY, fondateur de x10
Emmanuel BALLERY
CTO freelance & Architecte logiciel
calendar_today 26/03/2026
schedule 14 min lecture
Un développeur ajoute des tests automatisés sur un projet existant affiché à l'écran

Personne ne lance un projet en se disant « on ne mettra pas de tests ». Mais les contraintes s'accumulent : des deadlines serrées, un MVP a livrer, une equipe reduite, un framework mal maitrise. Et un jour, on se retrouve avec une application en production, des centaines de fichiers, zero test automatise — et la peur au ventre a chaque mise en production.

C'est une situation courante. Et contrairement a ce qu'on pourrait croire, ce n'est pas une impasse. Il existe des strategies eprouvees pour introduire des tests automatises sur un projet existant, sans tout reecrire et sans bloquer les developpements en cours.

Pourquoi tester un legacy est plus urgent qu'un projet neuf

Sur un projet neuf, le code est frais, l'architecture est connue, les decisions techniques sont documentees. L'equipe sait ce que fait chaque module parce qu'elle vient de l'ecrire. Les tests sont utiles, mais le risque de regression est faible.

Sur un projet legacy, c'est l'inverse. Le code a ete ecrit par des developpeurs qui ne sont plus la. Certains modules n'ont pas ete touches depuis des annees. D'autres ont ete modifies des dizaines de fois sans que personne ne comprenne l'ensemble des effets de bord.

Dans ce contexte, chaque changement est un risque. Modifier une requete SQL, refactorer un service, mettre a jour une dependance — tout peut casser quelque chose d'inattendu. Et sans tests, ces regressions sont invisibles jusqu'a ce qu'un utilisateur les remonte en production.

Le paradoxe est cruel : c'est precisement la ou il n'y a pas de tests qu'on en a le plus besoin. Et c'est precisement la ou le code est le moins testable qu'il faut trouver un moyen de le tester.

La peur du refactoring

Sans filet de securite, les equipes developpent un reflexe naturel : ne toucher a rien. On contourne les problemes plutot que de les resoudre. On duplique du code plutot que d'extraire une abstraction. On ajoute des conditions plutot que de simplifier la logique.

Ce comportement est rationnel — mais il accelere la degradation du code. C'est un cercle vicieux : moins on teste, moins on refactore ; moins on refactore, plus le code devient difficile a tester. Briser ce cercle est la premiere etape pour reprendre le controle d'un projet legacy.

Les tests de caracterisation : documenter avant de changer

Michael Feathers, dans Working Effectively with Legacy Code, definit le code legacy comme « du code sans tests ». Pas du vieux code. Pas du code mal ecrit. Du code dont on ne peut pas verifier le comportement de maniere automatisee.

Sa premiere recommandation est contre-intuitive : avant de corriger quoi que ce soit, ecrire des tests qui capturent le comportement actuel. Meme si ce comportement contient des bugs. Meme si la logique semble incorrecte.

Le principe

Un test de caracterisation ne verifie pas que le code fait ce qu'il devrait. Il verifie que le code fait ce qu'il fait. C'est une photo instantanee du comportement reel.

La methode est simple :

Si le test passe, vous avez documente un comportement. Si vous modifiez le code et que le test echoue, vous savez exactement ce qui a change — et vous pouvez decider si ce changement est intentionnel ou non.

Le golden master testing

Une variante puissante des tests de caracterisation est le golden master testing (aussi appele approval testing). Le principe : capturer la sortie complete d'une operation — une reponse HTTP, un rendu HTML, un export CSV — et la stocker comme reference.

A chaque execution, le test compare la sortie actuelle au fichier de reference. Toute difference est signalee. Si le changement est voulu, on met a jour le golden master. Sinon, on a detecte une regression.

C'est une technique particulierement efficace sur les projets legacy ou la logique est trop complexe pour etre testee unitairement : on teste la sortie globale sans avoir besoin de comprendre chaque ligne de code.

Rendre le code testable sans le reecrire

Le principal obstacle aux tests sur un projet legacy n'est pas le manque de temps. C'est que le code n'est pas structurellement testable. Des dependances codees en dur, des methodes statiques partout, des classes de 2 000 lignes, des appels directs a la base de donnees dans les controleurs — le code resiste aux tests.

La bonne nouvelle : il n'est pas necessaire de tout refactorer pour rendre le code testable. Il suffit de creer des coutures.

Le concept de coutures (seams)

Michael Feathers definit une couture comme un point du code ou l'on peut changer le comportement sans modifier le code lui-meme. C'est un endroit ou l'on peut « brancher » un test en substituant une dependance, en interceptant un appel ou en injectant un double de test.

En PHP, les coutures les plus courantes sont :

Extract and override

C'est la technique la plus rapide pour rendre un morceau de code testable sans le restructurer en profondeur. Le principe :

Ce n'est pas elegant. Ce n'est pas l'architecture cible. Mais ca permet d'ecrire un premier test en vingt minutes sur du code qui semblait intestable. Et un test imparfait vaut infiniment mieux que pas de test du tout.

Wrap and delegate

Quand une classe fait trop de choses et qu'on ne peut pas la modifier (dependance externe, classe generee, code critique en production), on peut la wrapper : creer une nouvelle classe qui delegue au code existant, mais expose une interface testable.

Le wrapper ne change rien au comportement. Il ajoute simplement un point d'injection qui permet de substituer la dependance dans les tests. C'est une technique classique pour isoler les appels a des APIs tierces ou a des composants systeme.

L'injection de dependance minimale

Sur un projet legacy, passer a un container d'injection de dependance complet peut etre un chantier enorme. Mais on n'en a pas besoin pour commencer a tester.

Il suffit souvent d'appliquer une regle simple : toute dependance externe doit etre passee en parametre du constructeur. Pas besoin d'interface. Pas besoin de container. Juste un parametre qu'on peut remplacer par un mock dans le test.

Cette approche incrementale transforme progressivement le code vers une architecture plus testable, sans jamais imposer un arret complet des developpements.

La strategie par couche

Une erreur frequente est de vouloir commencer par des tests unitaires. C'est l'approche naturelle — tester les petites briques en isolation — mais sur un projet legacy, c'est souvent la plus difficile. Le code n'est pas decoupe en unites testables. Les dependances sont entrelacees. L'effort pour isoler une classe peut etre disproportionne par rapport a la valeur du test.

Une strategie plus efficace consiste a commencer par le haut et descendre progressivement.

Tests E2E d'abord : le filet de securite large

Les tests end-to-end (E2E) verifient le comportement de l'application du point de vue de l'utilisateur. Ils traversent toute la pile : controleur, service, base de donnees, rendu.

Leur avantage sur un legacy : ils ne necessitent aucune modification du code. On teste l'application telle qu'elle est, via son interface HTTP ou son interface graphique. Avec un outil comme Playwright ou Codeception, on peut couvrir les parcours critiques en quelques jours.

Les tests E2E sont lents et fragiles, c'est vrai. Mais sur un projet sans aucun test, ils offrent un ratio effort/securite imbattable. Dix tests E2E bien cibles protegent mieux que cent tests unitaires sur du code non critique.

Tests d'integration sur les modules critiques

Une fois le filet de securite E2E en place, on peut descendre d'un cran et cibler les modules qui concentrent la logique metier. Les tests d'integration verifient qu'un module fonctionne correctement avec ses dependances reelles (base de donnees, systeme de fichiers, APIs).

Sur un projet Symfony, cela signifie typiquement :

Tests unitaires sur la logique metier

Les tests unitaires viennent en dernier — mais ils sont les plus precieux a long terme. Rapides, fiables, precis, ils documentent le comportement attendu de chaque regle metier.

Pour qu'ils soient possibles, il faut que le code ait ete rendu testable grace aux techniques decrites plus haut (coutures, extract and override, injection). C'est pourquoi cette couche arrive en dernier : elle beneficie du travail de refactoring progressif realise lors de l'ajout des tests d'integration.

Prioriser : la matrice risque x frequence de changement

On ne peut pas tout tester d'un coup. Sur un projet legacy de taille significative, atteindre une couverture de 80 % peut prendre des mois. Il faut donc choisir ou investir en premier.

La matrice risque x frequence de changement est un outil simple pour guider cette decision :

Cette matrice permet de concentrer les efforts la ou ils ont le plus d'impact. Elle evite le piege classique de tester du code trivial pendant que les modules critiques restent sans protection.

Outillage PHP concret

L'ecosysteme PHP dispose aujourd'hui d'outils matures pour chaque niveau de la pyramide de tests. Voici les incontournables pour un projet legacy.

PHPUnit

Le standard de facto. PHPUnit couvre les tests unitaires et les tests d'integration. Sa configuration est simple, sa documentation exhaustive, et son integration avec les CI/CD (GitHub Actions, GitLab CI) est native.

Sur un projet Symfony, le composant symfony/test-pack fournit tout le necessaire : WebTestCase pour les tests HTTP, KernelTestCase pour les tests de services, et les outils de debug associes.

Codeception

Pour les tests E2E et les tests d'acceptance, Codeception offre une syntaxe expressive et des modules preconfigures pour les applications web. Il se branche directement sur Symfony et permet de tester les parcours utilisateur complets.

DAMA DoctrineTestBundle

L'isolation des tests est un probleme recurrent sur les projets qui utilisent une base de donnees. DAMA DoctrineTestBundle resout ce probleme elegamment : chaque test s'execute dans une transaction qui est annulee a la fin. La base revient a son etat initial apres chaque test, sans jamais ecrire sur le disque.

Le gain est double : isolation parfaite et performance accrue (pas de purge ni de rechargement de fixtures entre chaque test).

PHPStan comme filet complementaire

PHPStan n'est pas un outil de test au sens strict, mais c'est un complement indispensable. L'analyse statique detecte une categorie de bugs que les tests ne couvrent pas toujours : types incorrects, variables indefinies, appels de methodes inexistantes, violations de contrat.

Sur un projet legacy, activer PHPStan au niveau 1 et monter progressivement est une victoire rapide. Chaque niveau supplementaire elimine une classe de bugs potentiels sans ecrire une seule ligne de test.

Plan d'adoption en 4 sprints

Introduire les tests automatises sur un projet legacy ne se fait pas en un jour. Mais il ne faut pas non plus en faire un projet a part. L'objectif est d'integrer la pratique dans le flux de travail existant, sprint apres sprint.

Sprint 1 : la fondation

L'objectif du premier sprint est de poser l'infrastructure.

A la fin de ce sprint, l'equipe a un filet de securite minimal et une CI qui valide chaque changement. C'est le socle sur lequel tout le reste s'appuie.

Sprint 2 : couvrir le module critique

En utilisant la matrice risque x frequence, identifier le module le plus critique de l'application. Typiquement : le paiement, la gestion des droits, le calcul de tarification, le workflow de commande.

Sprint 3 : tests unitaires et logique metier

Maintenant que le module critique est couvert par des tests d'integration, il est possible d'extraire la logique metier pure et de la tester unitairement.

Sprint 4 : mutation testing et consolidation

Le mutation testing est une technique avancee qui verifie la qualite des tests eux-memes. Un outil comme Infection modifie le code source (remplace un > par un >=, supprime une condition, inverse un booleen) et verifie que les tests detectent le changement.

A la fin de ces quatre sprints, l'equipe a transforme sa posture. Le projet n'est plus un legacy sans tests — c'est un projet qui a des tests et une strategie pour en ajouter. La difference est fondamentale.

Les erreurs a eviter

L'introduction de tests sur un legacy peut echouer si l'approche est mal calibree. Voici les pieges les plus courants.

Viser une couverture de 100 %

C'est un objectif contre-productif. La couverture de code est un indicateur, pas un objectif. Un projet avec 60 % de couverture sur le code critique est bien mieux protege qu'un projet avec 90 % de couverture concentree sur des getters et des setters.

Ecrire des tests fragiles

Un test qui casse a chaque modification de l'interface graphique ou a chaque changement de libelle n'est pas un filet de securite — c'est un frein. Les tests doivent verifier le comportement, pas les details d'implementation.

Reporter les tests a « quand on aura le temps »

Ce moment n'arrivera jamais. Les tests doivent etre integres dans le flux de developpement courant, pas traites comme un chantier a part. La regle la plus efficace : tout nouveau code et toute correction de bug s'accompagnent d'un test.

Reprendre le controle

Mettre en place des tests automatises sur un projet legacy n'est pas un luxe reserve aux grandes equipes. C'est un investissement qui se rentabilise en quelques semaines : moins de regressions, plus de confiance dans les mises en production, capacite retrouvee a refactorer et a faire evoluer le produit.

La cle est de ne pas chercher la perfection. Commencer petit, cibler les zones critiques, automatiser progressivement. Chaque test ajoute reduit le risque. Chaque refactoring securise par un test ameliore la qualite du code pour la suite.

Chez x10, nous accompagnons regulierement des equipes dans cette demarche. Notre audit technique permet d'identifier les zones critiques et de definir une strategie de test adaptee. Et notre service de maintenance applicative inclut la mise en place progressive de tests automatises pour securiser les evolutions futures. Si votre projet legacy vous inquiete, parlons-en.

Photo d'Emmanuel BALLERY, fondateur de x10

À propos de l'auteur

Emmanuel BALLERY est le fondateur de x10. Expert en architecture logicielle et passionné par la qualité du code (Software Craftsmanship), il aide les entreprises à transformer leur dette technique en actifs durables.

Voir plus arrow_forward