Blog / Modernisation legacy

Écrire les tests manquants avant de refactorer : les agents IA comme filet de sécurité

Keltio · Guide · 7 min de lecture

Refactorer un code sans tests, c'est modifier un comportement que personne ne sait décrire précisément. Les tests de caractérisation règlent ce problème : ils figent ce que le logiciel fait aujourd'hui, avant qu'on y touche. Longtemps trop coûteux à écrire à la main, ils deviennent réalistes avec des agents de code. L'agent les écrit vite ; l'expert choisit où et vérifie ce qu'ils figent.

Ce qu'est un test de caractérisation

Le terme vient de Michael Feathers (Working Effectively with Legacy Code). Un test de caractérisation ne vérifie pas que le code fait ce qu'il devrait faire. Il vérifie que le code fait ce qu'il fait aujourd'hui.

La différence est fondamentale :

  • un test classique part d'une spécification ;
  • un test de caractérisation part du code existant, l'exécute sur des entrées, enregistre les sorties et échoue si elles changent.

Il fige donc aussi les bugs. C'est voulu : pendant un refactoring, on ne veut aucun changement de comportement. Les anomalies découvertes sont notées, puis corrigées plus tard, dans un changement séparé et assumé.

Deux formes coexistent :

  • Tests unitaires de caractérisation : une fonction, des entrées choisies, des assertions sur les sorties observées.
  • Golden master (ou tests d'approbation) : on fait passer un grand nombre d'entrées réelles dans un traitement complet et on compare la sortie à un instantané de référence. C'est la forme la plus efficace sur les calculs métier lourds.

Choisir les zones à couvrir

On ne caractérise pas tout. Le choix des zones est la décision d'expert qui conditionne tout le reste.

Les critères, par ordre de priorité :

  1. Le périmètre du prochain refactoring. On couvre ce qu'on va toucher, et ses appelants directs.
  2. Les calculs à conséquence financière ou réglementaire : paie, commissions, facturation, calculs d'émissions ou énergétiques, marges, risques, approvisionnement. Une régression y coûte cher et se voit tard.
  3. Les zones à risque : code complexe, souvent modifié, connu d'une seule personne. Si une cartographie du code existe, elle donne directement cette liste.
  4. Les frontières : API exposées aux clients, formats de fichiers échangés, flux de facturation électronique, exports comptables. Un changement de format casse les intégrations des clients.

À l'inverse, on laisse de côté le code mort, les écrans purement décoratifs et les modules qui seront remplacés entièrement sans reprise de logique.

Un bon découpage ressemble à une liste courte : « le calcul des commissions et ses trois appelants », « l'export de facture au format structuré », « la valorisation des stocks en fin de mois ».

Générer avec des agents, sur des données réelles

L'agent de code est très bon pour la partie laborieuse : lire la fonction, identifier les branches, construire les entrées qui les exercent, écrire le code de test, faire tourner, ajuster. Voici le déroulé que nous suivons.

  1. Donner le contexte : la zone choisie, les conventions de test du projet, et l'instruction explicite de ne pas modifier le code de production.
  2. Faire lister les comportements avant d'écrire : branches, cas limites, valeurs en dur, effets de bord (écriture en base, fichiers, horloge).
  3. Construire les entrées à partir de cas réels. Les meilleurs jeux de données sont des plans de commission réels, des bilans réels, des historiques de commandes réels, anonymisés. Ils contiennent les combinaisons que personne n'aurait imaginées.
  4. Isoler le non-déterminisme : dates, identifiants générés, ordre de tri non garanti, arrondis flottants. L'agent injecte une horloge fixe et normalise les sorties.
  5. Exécuter et enregistrer les sorties comme référence.

Un golden master minimal ressemble à ceci :

import json, pathlib, pytest
from legacy.commissions import calculer_commissions

CAS = sorted(pathlib.Path("tests/caracterisation/commissions/entrees").glob("*.json"))

def normaliser(resultat):
    # arrondi explicite et tri stable pour éviter les faux positifs
    return sorted(
        ({**l, "montant": round(l["montant"], 2)} for l in resultat),
        key=lambda l: (l["vendeur"], l["periode"]),
    )

@pytest.mark.parametrize("cas", CAS, ids=lambda p: p.stem)
def test_commissions_inchangees(cas, snapshot, horloge_fixe):
    entree = json.loads(cas.read_text())
    assert normaliser(calculer_commissions(**entree)) == snapshot

La consigne donnée à l'agent tient en quelques lignes :

Objectif : tests de caractérisation pour legacy/commissions.
- Ne modifie aucun fichier hors de tests/.
- Liste d'abord les branches et cas limites de calculer_commissions.
- Utilise les entrées anonymisées de tests/caracterisation/commissions/entrees.
- Ajoute des entrées synthétiques uniquement pour les branches non couvertes.
- Fige les sorties actuelles, même si elles te semblent fausses.
- Signale dans ANOMALIES.md tout comportement suspect, sans le corriger.

La dernière consigne est importante. Un agent a tendance à « réparer » ce qui lui paraît incohérent. Ici, il doit documenter, pas corriger.

La revue humaine : ce que l'expert vérifie

L'agent écrit vite, mais un test peut passer sans rien protéger. La revue porte sur quatre points :

  • Le test exerce-t-il vraiment le code ? Un mock trop large peut remplacer la logique qu'on voulait figer.
  • Les assertions sont-elles assez précises ? Vérifier qu'un résultat « n'est pas vide » ne caractérise rien.
  • Les cas choisis sont-ils représentatifs ? L'expert métier connaît les cas pathologiques (le client avec un plan de commission atypique, l'établissement avec un paramétrage particulier).
  • Les anomalies signalées sont-elles de vrais bugs ? Parfois, un comportement étrange est une règle métier voulue.

Relire un lot de tests prend beaucoup moins de temps que l'écrire. C'est cet écart qui rend l'approche rentable : le temps de l'expert va au choix des zones et à la revue, pas à l'écriture.

Couverture utile contre couverture vanity

Un taux de couverture global de 80 % peut ne rien protéger. Des tests qui exécutent chaque ligne sans rien vérifier font monter la couverture ; ils ne détectent aucune régression. C'est la couverture vanity, facile à obtenir avec un agent si on lui donne le pourcentage comme objectif.

Ce que nous mesurons à la place :

  • Couverture des branches sur les zones choisies, pas la couverture globale en lignes.
  • Score de mutation : un outil modifie volontairement le code (inverse une condition, change un opérateur) et vérifie qu'au moins un test échoue. Des outils existent pour la plupart des écosystèmes, comme PIT pour Java, Stryker pour JavaScript et .NET, mutmut pour Python. Une mutation qui survit signale un comportement que les tests ne protègent pas.
  • Couverture des cas réels : part des configurations clients ou des types de dossiers représentés dans les entrées du golden master.

Donnez à l'agent un objectif de score de mutation sur la zone choisie, pas un pourcentage de lignes.

Une référence de non-régression pour les migrations

Les tests de caractérisation servent aussi au-delà du refactoring. Quand un éditeur réécrit un produit (nouvelle génération web, convergence de plusieurs gammes, reprise d'un module dans une nouvelle plateforme), les clients migrés attendent les mêmes résultats qu'avant.

Le principe du test différentiel :

  1. Caractériser l'ancien logiciel sur des entrées réelles : il devient l'oracle.
  2. Faire passer les mêmes entrées dans le nouveau.
  3. Comparer les sorties, module par module ou écran par écran, et traiter chaque écart : bug du nouveau, bug de l'ancien assumé, ou changement voulu et documenté.

C'est particulièrement utile pour les résultats financiers (écritures comptables, montants de paie, états réglementaires), où « presque pareil » n'est pas acceptable.

Même logique pour un produit livré en plusieurs modes, SaaS et on-premise par exemple : le même jeu de caractérisation, paramétré par mode de déploiement, couvre les deux sans doubler l'effort d'écriture.

L'intégration en CI

Un filet de sécurité qui ne tourne pas à chaque changement ne protège rien.

  • À chaque merge request : les tests de caractérisation des zones touchées, en quelques minutes.
  • Chaque nuit : le golden master complet sur l'ensemble des cas réels.
  • Mise à jour des instantanés : jamais automatique. Un changement de référence passe par une revue explicite, avec un label dédié ou un fichier de justification. Sinon, le premier refactoring qui casse un comportement mettra aussi à jour le test.
  • Tests instables : mis en quarantaine et corrigés vite. Un golden master qui échoue au hasard est vite ignoré.
  • Données : les entrées réelles sont anonymisées et stockées dans un espace à accès contrôlé.
# extrait GitLab CI
caracterisation:rapide:
  stage: test
  script:
    - pytest tests/caracterisation -m "zone_modifiee"
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

caracterisation:complete:
  stage: test
  script:
    - pytest tests/caracterisation
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"

Un agent de revue en CI peut en plus signaler toute modification d'un fichier d'instantané dans une merge request, pour qu'un humain la valide.

Encadré : checklist avant le premier refactoring

  • Zones choisies par un expert, liste écrite et courte.
  • Entrées réelles anonymisées disponibles pour chaque zone.
  • Consigne à l'agent : aucune modification hors du répertoire de tests, anomalies signalées et non corrigées.
  • Non-déterminisme neutralisé (horloge, identifiants, tri, arrondis).
  • Revue humaine faite sur chaque lot de tests générés.
  • Score de mutation mesuré sur les zones choisies.
  • Tests rapides en merge request, golden master complet chaque nuit.
  • Mise à jour des instantanés soumise à revue explicite.

Par où commencer

  1. Choisissez le prochain refactoring prévu et délimitez sa zone avec l'expert du module.
  2. Rassemblez 50 à 200 cas réels anonymisés pour cette zone.
  3. Faites générer les tests de caractérisation par un agent avec la consigne ci-dessus, puis relisez-les.
  4. Mesurez le score de mutation et complétez les cas jusqu'à un niveau jugé suffisant.
  5. Branchez les tests en CI avant d'écrire la première ligne du refactoring.

Nous accompagnons les éditeurs et les équipes de développement dans la sécurisation de leurs refactorings et migrations avec des agents IA. Si vous voulez en parler : [email protected]

Un sujet similaire chez vous ?

Nous en parlons volontiers, sans engagement : 30 minutes pour faire le point sur votre contexte.

Une conversation pour résoudre vos enjeux