Modifier en toute sécurité une application métier legacy sans tests — Tests de caractérisation et refactorisation en pratique

· · Technologie legacy, Valorisation des actifs existants, Refactorisation, Conception de tests, Test de caractérisation, C#, .NET, Maintenance, Tableau de décision, Conseil technique

« Je sais exactement ce qu’il faudrait corriger. Mais rien que l’idée qu’en y touchant, je risque de casser autre chose ailleurs, ça me bloque » — voilà une phrase que l’on entend souvent de la part de personnes qui ont hérité d’une application métier sans aucun test.

Beaucoup d’applications métier écrites en VB6, en .NET Framework ou avec Access n’ont aucun test automatisé. Le cahier des charges, lui aussi, a cessé d’être mis à jour depuis longtemps : « le code est la seule spécification qui reste ». L’activité continue pourtant, et les demandes de modification — changement du taux de TVA, correction de la mise en page d’un état imprimé, ajout d’un nouveau partenaire commercial — n’attendent personne.

Sur ce blog, dans l’article « Jusqu’à quand une application VB6 continuera-t-elle de fonctionner ? — Une démarche réaliste de migration vers .NET », nous avions présenté l’idée de traiter l’ancien système comme une « spécification vivante » et de faire avancer une migration en comparant les sorties produites. Cet article applique cette même idée à une situation différente : non pas migrer, mais modifier sur place le code qui tourne actuellement. L’outil central est le test de caractérisation (characterization test). Même pour du code totalement dépourvu de tests, si l’on commence par figer dans un test le comportement que l’on risque de casser, la refactorisation comme l’ajout de fonctionnalités deviennent nettement plus sûrs.

1. L’essentiel d’abord

  • Ne corrigez pas tout de suite. Commencez par figer le comportement actuel dans un test. Même sans cahier des charges, la sortie du code tel qu’il tourne actuellement est en elle-même la spécification.
  • L’outil pour cela est le test de caractérisation. Il ne s’agit pas de vérifier un « comportement correct », mais d’enregistrer le « comportement actuel » : on sauvegarde tel quel, comme valeur attendue, la sortie produite (états imprimés, CSV, résultats de calcul, etc.), puis on compare les différences avant et après une modification (méthode du golden master).
  • Pour les structures dans lesquelles on ne peut pas insérer de test (logique écrite directement dans un gestionnaire d’événement d’interface, référence directe à DateTime.Now ou à un chemin de fichier), on crée un « point de couture » (seam) avec le changement minimal que constituent l’extraction de méthode et l’injection d’interface. Une refonte majeure n’est pas nécessaire.
  • Ne mélangez pas refactorisation et ajout de fonctionnalités dans le même commit. Le critère de réussite d’une refactorisation est « zéro différence », celui d’un ajout de fonctionnalité est « seulement la différence voulue » ; en les mélangeant, on perd la capacité de savoir ce que signifie une différence constatée.
  • Jusqu’où pousser la mise en place des tests se décide en croisant l’ampleur de la modification, la durée de vie restante du système et l’impact en cas de dysfonctionnement. Couvrir absolument tout par des tests unitaires n’est pas toujours la bonne réponse ; il y a des cas où « seulement des tests de caractérisation » ou même « ne pas y toucher » est la bonne décision.
  • On peut commencer sans intégration continue (CI). Un seul projet de tests et un dossier de fichiers de valeurs attendues suffisent : même exécutés uniquement en local, ils changent radicalement le niveau de sécurité.

2. Pourquoi le code legacy « casse dès qu’on y touche »

Modifier du code legacy fait peur non pas parce que le code est ancien, mais parce qu’il n’existe aucun moyen de vérifier si le résultat de la modification est correct.

Dans son ouvrage Working Effectively with Legacy Code, Michael Feathers définit le code legacy non pas comme « du code simplement ancien », mais comme « du code sans tests ».1 La raison qu’il avance est que, sans tests, il n’existe aucun moyen rapide de vérifier, à chaque modification, si le code s’améliore ou se dégrade. Selon cette définition, même du code écrit hier est du code legacy s’il n’a pas de tests.

Le code sans tests enclenche le cercle vicieux suivant :

  1. Sans tests, on ne connaît pas le périmètre d’impact d’une modification, ce qui fait peur
  2. Par peur, on évite de toucher à la structure existante et l’on se contente d’un copier-coller minimal accompagné d’une condition supplémentaire
  3. Ces correctifs de fortune s’accumulent, et le code devient encore plus illisible et encore plus fragile
  4. Plus fragile, il fait encore plus peur (retour au point 1)

La porte de sortie de ce cercle vicieux n’est pas de « rassembler son courage pour une grande refactorisation ». C’est l’inverse : on tend d’abord un filet de sécurité (les tests), on élimine la source de la peur, et ensuite seulement on corrige. Mais on se heurte alors à un problème de l’œuf et la poule : écrire un test exige une structure testable ; or, pour rendre la structure testable, il faut modifier (refactoriser) le code. On se retrouve donc à devoir modifier, sans tests, du code qui n’a pas de tests.

Pour résoudre cette contradiction, la modification de code legacy se déroule dans l’ordre suivant :1

  1. Uniquement autour de la zone à modifier, figer de l’extérieur le comportement actuel (test de caractérisation)
  2. À l’intérieur de ce filet de sécurité, effectuer le changement minimal présentant un risque de casse extrêmement faible (comme l’extraction de méthode) afin de créer un point d’insertion pour les tests
  3. Une fois que la structure permet d’écrire des tests fins, entamer le changement réellement souhaité (refactorisation ou ajout de fonctionnalité)

Les chapitres suivants détaillent concrètement les points 1 et 2.

3. Le test de caractérisation — enregistrer le « comportement actuel »

3.1 En quoi diffère-t-il d’un test classique ?

Un test classique vérifie le comportement correct — « c’est ainsi que cela devrait fonctionner d’après la spécification ». Le test de caractérisation, lui, est différent : il enregistre la manière dont le code se comporte réellement aujourd’hui, en suspendant tout jugement sur son exactitude.

Supposons par exemple que le cahier des charges ne précise pas si l’arrondi des fractions doit se faire par arrondi, par arrondi supérieur ou par troncature. Si le code actuel tronque, et que l’activité tourne ainsi depuis dix ans, alors « il tronque » constitue, au minimum, la spécification de fait. Le test de caractérisation fige cela exactement tel quel, sous la forme « la sortie actuelle est telle ou telle chose ». Même s’il s’agit d’un bogue, on commence par le figer. Changer le comportement (corriger le bogue) se fait séparément, comme une modification volontaire, une fois le filet de sécurité en place.

3.2 La méthode du golden master, étape par étape

Pour du code legacy dont la granularité de sortie est grossière, la méthode du golden master est la forme de test de caractérisation offrant le meilleur rapport coût-efficacité. La procédure est simple :

  1. Identifier la sortie produite par la fonctionnalité à modifier (texte d’un état imprimé, CSV, liste de résultats de calcul, etc.)
  2. Préparer des données d’entrée représentatives, exécuter le code actuel et récupérer la sortie
  3. Sauvegarder cette sortie telle quelle comme fichier de valeur attendue (le golden master) et l’intégrer au dépôt
  4. Ensuite, exécuter le test à chaque modification du code, en vérifiant que la différence entre la sortie et le fichier de valeur attendue est nulle

Une implémentation en C# n’a besoin d’aucune bibliothèque particulière : quelque chose d’aussi simple que ce qui suit suffit.

[Fact]
public void RecapFacturationMensuelle_GoldenMaster()
{
    // 1. Lire une entrée représentative (par ex. des données extraites de la production et anonymisées)
    var input = File.ReadAllLines(TestDataPath("billing-input-202606.csv"));

    // 2. Appeler la logique existante telle quelle et récupérer la chaîne de sortie
    string actual = BillingReport.Generate(input);

    // 3. Un fichier de valeur attendue manquant signifie soit que "l'environnement de test est cassé"
    //    soit qu'il s'agit de "la première exécution". Dans les deux cas, ne pas laisser passer
    //    silencieusement : enregistrer puis faire échouer le test sans condition
    string expectedPath = TestDataPath("billing-expected-202606.txt");
    if (!File.Exists(expectedPath))
    {
        File.WriteAllText(expectedPath + ".candidate", actual);
        Assert.Fail("Le fichier de valeur attendue est introuvable. Relisez le contenu du fichier " +
                    ".candidate et, s'il est correct, validez-le comme valeur attendue.");
    }

    // 4. Vérifier une correspondance exacte avec le comportement sauvegardé
    string expected = File.ReadAllText(expectedPath);
    Assert.Equal(expected, actual);
}

Évitez une implémentation qui, lorsque le fichier de valeur attendue est introuvable, sauvegarde la sortie actuelle telle quelle comme valeur attendue et laisse le test réussir. Si la valeur attendue n’a pas été commise, ou si l’environnement de test a été mal configuré, la CI passerait au vert sans jamais détecter de régression. Pour le premier enregistrement, procédez comme ci-dessus : produire un fichier candidat (.candidate) et faire échouer le test explicitement, de sorte que le flux soit strictement à sens unique — une personne relit, puis seulement alors on le valide comme valeur attendue.

Comme le seul message de Assert.Equal est difficile à exploiter lorsqu’une différence apparaît, en pratique il est utile, en cas d’échec, d’écrire aussi la sortie réelle dans un fichier séparé, par exemple billing-actual-202606.txt, afin de pouvoir la comparer à la valeur attendue avec un outil de diff comme WinMerge — cela accélère nettement l’investigation.

3.3 Choisir les entrées et normaliser la sortie

Choisissez les entrées selon le principe « cas représentatifs + cas limites ». Prenez un ou deux cas ordinaires, puis ajoutez des entrées qui font passer par les branches repérées en lisant le code : clôture de fin de mois, zéro enregistrement, valeurs négatives, traitement d’exception pour un partenaire commercial particulier, etc. Si vous pouvez utiliser des données de production anonymisées, ce sont elles qui couvriront le plus fidèlement les branches réelles.

Normalisez avant comparaison toute valeur non déterministe qui s’invite dans la sortie. L’horodatage d’impression, la durée de traitement, un GUID, un numéro auto-incrémenté, etc., changent à chaque exécution et produiraient donc une différence à chaque fois. Après avoir généré la sortie, insérez une étape de prétraitement — par exemple une substitution par expression régulière remplaçant Date d'impression : 2026/07/17 16:00 par Date d'impression : <DATE> — avant de procéder à la comparaison.

Voici un aperçu du type de sortie qui se prête bien à un golden master :

Type de sortie Adéquation Remarques
CSV / fichiers à largeur fixe Excellent Peuvent être sauvegardés et comparés tels quels. La première cible à viser
États imprimés (texte, ou données source de l’aperçu avant impression) Excellent Capturer la chaîne juste avant la génération du PDF ; éviter de comparer des binaires PDF
Liste de résultats de calcul (montants, quantités en stock, etc.) Excellent Il est possible d’ajouter une méthode réservée aux tests qui exporte les résultats en CSV
Contenu écrit en base de données Bon Effectuer un SELECT sur la table après écriture, l’exporter en CSV et comparer
L’affichage écran lui-même Limité Possible si ramené à une chaîne de caractères. Automatiser les manipulations à l’écran demande un autre outillage, traité dans « Tests UI automatisés pour applications de bureau Windows »
Transmissions vers un système externe Limité Nécessite un point de couture (chapitre suivant) pour capturer les données juste avant l’envoi

4. Créer le « point de couture » (seam) qui permet d’insérer un test

Vouloir écrire un golden master se heurte souvent à un mur dans le code legacy : la logique est écrite directement dans un gestionnaire d’événement d’interface, et on ne peut pas l’exécuter sans lancer l’écran. Ce qu’il faut ici, c’est un endroit où le code de test peut substituer et observer le comportement — ce que Feathers appelle un point de couture (seam).1

4.1 Détacher la logique de l’interface par extraction de méthode

Voici à quoi ressemble typiquement l’état « avant » : calcul, accès à la base de données, dépendance à l’heure et mise à jour de l’écran cohabitent dans un seul gestionnaire d’événement.

// Before : tout est écrit directement dans le gestionnaire d'événement
private void btnCalc_Click(object sender, EventArgs e)
{
    var rows = LoadRowsFromDb();                          // Accès direct à la base de données
    var now = DateTime.Now;                               // Dépend de l'heure actuelle
    decimal total = 0;
    foreach (var row in rows)
    {
        if (row.SalesDate.Year == now.Year &&
            row.SalesDate.Month == now.Month)              // N'agréger que le mois en cours
        {
            total += Math.Floor(row.Amount * 1.1m);        // Une règle métier d'arrondi
        }
    }
    lblTotal.Text = total.ToString("N0");                  // Répercuté directement sur l'écran
}

En l’état, tester la logique d’agrégation du mois en cours exige l’écran, la base de données et « la date du jour ». La technique standard pour la rendre testable avec un changement minimal consiste à extraire uniquement le calcul dans une méthode, en transformant les dépendances externes (le résultat de la base de données et l’heure actuelle) en paramètres. La refactorisation d’extraction de méthode de Visual Studio (Ctrl+R, M) permet aussi de réduire les erreurs de réécriture manuelle.2

// After : seul le calcul est extrait, en recevant "le résultat de la base de données"
// et "l'heure actuelle" comme paramètres
internal static decimal CalcMonthlyTotal(IEnumerable<SalesRow> rows, DateTime now)
{
    decimal total = 0;
    foreach (var row in rows)
    {
        if (row.SalesDate.Year == now.Year &&
            row.SalesDate.Month == now.Month)
        {
            total += Math.Floor(row.Amount * 1.1m);
        }
    }
    return total;
}

private void btnCalc_Click(object sender, EventArgs e)
{
    var rows = LoadRowsFromDb();
    lblTotal.Text = CalcMonthlyTotal(rows, DateTime.Now).ToString("N0");
}

Côté gestionnaire d’événement, il ne reste plus que trois lignes — charger, calculer, afficher — et la méthode extraite peut être testée avec n’importe quelles données de lignes et n’importe quelle date. Les cas limites liés au temps, comme la fin de mois, le début de mois ou une année bissextile, se reproduisent eux aussi simplement en passant une date telle que new DateTime(2028, 2, 29).

4.2 Rendre les dépendances substituables par injection d’interface

Pour les dépendances trop répandues pour être résolues par simple paramétrage (DateTime.Now référencé un peu partout, un chemin de fichier codé en dur, etc.), on enveloppe la dépendance dans une interface que l’on injecte. Les bonnes pratiques de test unitaire .NET de Microsoft Learn citent elles aussi la dépendance directe à DateTime.Now comme un exemple classique de dépendance impossible à contrôler depuis un test, et proposent de l’envelopper dans une interface pour introduire un point de couture (seam).3

public interface IClock
{
    DateTime Now { get; }
}

public sealed class SystemClock : IClock
{
    public DateTime Now => DateTime.Now;
}

// Côté test, on injecte une implémentation qui renvoie une heure fixe
public sealed class FixedClock : IClock
{
    private readonly DateTime _fixed;
    public FixedClock(DateTime value) => _fixed = value;
    public DateTime Now => _fixed;
}

Ajouter IClock au constructeur d’une classe existante oblige à corriger tous les appelants, aussi est-il réaliste, pendant la période de transition, d’ajouter en parallèle un constructeur avec valeur par défaut — « le constructeur sans argument utilise SystemClock » — et de corriger les appelants progressivement. Les chemins de fichiers ou chaînes de connexion à la base de données codés en dur se traitent de la même façon : on les enveloppe dans une petite interface qui n’expose que les opérations de « lecture/écriture ».

Notez que Visual Studio intègre également une refactorisation d’extraction d’interface à partir d’une classe existante (Extract Interface), qui permet d’effectuer ce type de changement de façon mécanique.2

Il n’y a qu’un seul principe à respecter lors de la création d’un point de couture : le changement qui crée le point de couture ne doit modifier le comportement d’absolument aucune façon. L’extraction de méthode et l’injection d’interface sont toutes deux des opérations à haute préservation du comportement, réalisables mécaniquement avec l’aide du compilateur et de l’IDE. On est tenté, à ce stade, de « profiter de l’occasion » pour corriger la logique — mais c’est un travail réservé à l’après mise en place du filet de sécurité.

5. Un tableau de décision pour savoir jusqu’où aller

Le test de caractérisation comme la création de points de couture ont eux aussi un coût. Amener tout le code legacy au même niveau de test n’est réaliste ni pour une équipe petite ou moyenne, ni même nécessaire. Trois axes guident la décision :

  • L’ampleur de la modification : une correction de bug de quelques lignes, un ajout de fonctionnalité, ou quelque chose impliquant un changement de structure
  • La durée de vie restante du système : migration ou retrait prévu dans un an ou deux, ou utilisation prévue pendant cinq ans ou plus
  • L’impact en cas de dysfonctionnement : simple déformation visuelle d’un état imprimé, ou erreur sur un montant facturé ou une quantité en stock
Ampleur de la modification Durée de vie restante Impact en cas de dysfonctionnement Niveau recommandé
Mineure (quelques lignes, changement d’un paramètre) Courte (jusqu’à ~2 ans) Faible (déformation d’affichage) Tests de caractérisation uniquement. Figer la sortie concernée, modifier, vérifier une différence nulle, et s’arrêter là
Mineure à moyenne Courte Fort (traite des montants ou du stock) Tests de caractérisation uniquement, mais plus étoffés. Multiplier les schémas d’entrée, y compris les cas limites
Moyenne (ajout de fonctionnalité, changement de logique) Longue (5 ans ou plus) Faible à moyen Tests de caractérisation + tests unitaires limités à la zone modifiée (créer un point de couture)
Moyenne à grande Longue Fort Tests de caractérisation + mise en place de tests unitaires + découpage plus fin des unités de mise en production
Grande (une refonte structurelle est nécessaire) Courte Ne pas y toucher. Contourner le problème sur le plan opérationnel sans le modifier, et consacrer l’effort à la migration ou au remplacement
— (aucune demande de modification) Ne pas y toucher. Ne pas refactoriser préventivement du code qui fonctionne déjà

Le « ne pas y toucher » des deux dernières lignes n’est pas un choix passif, mais une décision active. Investir dans la qualité interne d’un système dont la durée de vie restante est courte ne sera pas rentabilisé. Cet effort doit plutôt être consacré à la décision de migration présentée dans « Tableau de décision pour prolonger ou migrer une application métier VB6 / Access », ainsi qu’à la conception de la solution cible.

Par ailleurs, si l’on va jusqu’à « mettre en place des tests unitaires », il faut aussi tracer la frontière entre ce qui relève du test unitaire et ce qui doit rester dans un test d’intégration (un test qui touche une vraie base de données ou de vrais fichiers). Cette frontière est présentée sous forme de tableau de décision dans « Où tracer la frontière entre tests unitaires et tests d’intégration », à consulter en complément. Étant donné qu’un test unitaire est censé être fast, isolated et repeatable,3 il est prudent de séparer les tests de caractérisation qui touchent une base de données ou des fichiers dans un projet et une unité d’exécution distincts des tests unitaires.

6. Règles opérationnelles — pour ne pas rompre le filet de sécurité

Un test de caractérisation perd facilement tout son sens si l’on se trompe ensuite dans son exploitation. Voici les trois règles minimales à respecter.

6.1 Ne pas mélanger refactorisation et ajout de fonctionnalités dans le même commit

La refactorisation est un changement qui rend le code plus facile à comprendre et à maintenir sans en modifier le comportement.4 Son critère de réussite est donc zéro différence par rapport au golden master. L’ajout de fonctionnalité ou la correction de bug, à l’inverse, a pour critère de réussite seule la différence voulue apparaît. Mélanger les deux dans un même commit fait perdre, dès qu’une différence apparaît, la capacité de déterminer s’il s’agit du « changement voulu » ou de « quelque chose de cassé ».

Type de changement Traitement du golden master Critère de réussite
Refactorisation (changement de structure) Ne pas mettre à jour Zéro différence
Correction de bug / ajout de fonctionnalité (changement de comportement) Mettre à jour après relecture de la différence Uniquement la différence voulue
Création de point de couture (extraction de méthode, injection d’interface) Ne pas mettre à jour Zéro différence
Changement de la règle de normalisation des valeurs attendues Régénérer Indiquer clairement la raison du changement dans le message de commit

Il en va de même au niveau des mises en production. Une « mise en production contenant uniquement de la refactorisation » ne devrait entraîner aucun changement de comportement ; en cas d’incident, on peut donc immédiatement suspecter la refactorisation. En mélangeant les deux, cette capacité à isoler la cause ne fonctionne plus.

6.2 Mettre à jour les valeurs attendues dans l’ordre « relecture de la différence → écrasement »

Lorsqu’on change volontairement le comportement, il faut aussi mettre à jour le golden master. Fixez la procédure :

  1. Générer la sortie après modification et relire visuellement la différence avec la valeur attendue actuelle
  2. Vérifier que la différence ne contient que le changement voulu (si ne serait-ce qu’une ligne non voulue a changé, investiguer)
  3. Écraser le fichier de valeur attendue avec la nouvelle sortie, et l’inclure dans le même commit que le code pour qu’il reste dans l’historique

La pratique dangereuse consiste à « écraser la valeur attendue pour repasser le test au vert parce qu’il est devenu rouge ». Faire cela revient à enregistrer une régression telle quelle comme étant « correcte », et le filet de sécurité cesse d’en être un.

6.3 Mettre en place, même sans CI, la configuration minimale exécutable en local

Même sur un terrain sans serveur de CI, la configuration minimale suivante permet de commencer dès aujourd’hui.

  • Ajouter un seul projet de tests à la solution (MSTest, NUnit et xUnit fonctionnent tous, même en restant sur .NET Framework)
  • Placer les fichiers de valeurs attendues et les données d’entrée dans un dossier TestData, et les gérer en version avec le code
  • Faire de l’exécution manuelle de dotnet test (ou de l’Explorateur de tests de Visual Studio) avant chaque commit une règle d’équipe
  • Ajouter une ligne « exécuter les tests et vérifier une différence nulle » dans la procédure de mise en production, pour ne pas oublier de vérifier le résultat

Lors du rapprochement entre les résultats des tests et la sortie propre de l’application, mieux les journaux sont tenus, plus vite l’investigation avance. Ce qu’il faut consigner dans les journaux est traité dans « Exigences minimales pour un journal applicatif maison et liste de contrôle pour les tests d’intégration ».

7. En résumé

  • Le code legacy, c’est « du code sans tests »,1 et la véritable raison pour laquelle il casse dès qu’on y touche est qu’il n’existe aucun moyen de vérifier le résultat d’une modification. Avant de corriger quoi que ce soit, figez le comportement actuel dans un test.
  • Le test de caractérisation est un test qui enregistre le « comportement actuel », et non le « comportement correct ». Avec la méthode du golden master — sauvegarder tels quels des états imprimés, des CSV ou des résultats de calcul dans des fichiers de valeurs attendues et comparer les différences — on peut commencer avec du simple code C#.
  • Pour les structures où l’on ne peut pas insérer de test, créez un point de couture (seam) par extraction de méthode et injection d’interface. Envelopper une dépendance comme DateTime.Now est une technique standard, présentée elle aussi dans les recommandations de test unitaire de Microsoft.32
  • Jusqu’où pousser la mise en place des tests se décide en croisant l’ampleur de la modification, la durée de vie restante et l’impact en cas de dysfonctionnement. « Seulement des tests de caractérisation » ou « ne pas y toucher » sont eux aussi des décisions parfaitement légitimes.
  • Sur le plan opérationnel, respectez la règle de ne pas mélanger la refactorisation (zéro différence pour réussir) et l’ajout de fonctionnalités (seule la différence voulue pour réussir),4 et faites toujours passer la mise à jour des valeurs attendues par une relecture de la différence. Même sans CI, le simple engagement d’équipe à exécuter les tests en local change radicalement le niveau de sécurité.

Articles associés

Domaines de conseil associés

Chez Komura Software LLC, nous prenons en charge l’introduction de tests de caractérisation dans des applications métier existantes dépourvues de tests, la refactorisation progressive vers une structure testable, ainsi que l’aide à trancher entre investir dans la modification ou dans la migration.

Références

  1. Michael C. Feathers, Working Effectively with Legacy Code (Prentice Hall, 2004). Sur la définition du code legacy comme « code sans tests », sur la procédure consistant à enregistrer le comportement actuel au moyen d’un test de caractérisation avant d’entamer une modification, et sur le concept de point de couture (seam) permettant d’insérer un test.  2 3 4

  2. Microsoft Learn, Extract and inline refactorings (Visual Studio). Sur les étapes des refactorisations Extract Method (Ctrl+R, M) et Extract Interface de Visual Studio pour C# / Visual Basic.  2 3

  3. Microsoft Learn, Unit testing best practices for .NET. Sur les propriétés d’un bon test unitaire (fast / isolated / repeatable / self-checking / timely), sur la technique consistant à envelopper dans une interface une dépendance incontrôlable comme DateTime.Now pour introduire un point de couture (seam), et sur le fait de garder les dépendances d’infrastructure hors des tests unitaires, en les reportant vers les tests d’intégration.  2 3

  4. Microsoft Learn, Refactor code (Visual Studio). Sur la définition de la refactorisation comme le processus consistant à modifier le code pour le rendre plus facile à maintenir, à comprendre et à étendre, sans en changer le comportement.  2

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Qu'est-ce qu'un test de caractérisation (characterization test) ?
C'est un test qui enregistre tel quel le « comportement actuel », et non le « comportement correct ». Dans du code legacy dont le cahier des charges a disparu depuis longtemps, il n'existe souvent aucun moyen de vérifier ce qui serait correct : on commence donc par sauvegarder, comme valeur attendue, la sortie du code tel qu'il tourne actuellement (un état imprimé, un CSV, un résultat de calcul, etc.), puis on vérifie mécaniquement que cette sortie ne change pas avant et après une modification. La pratique de base consiste à mettre en place ce filet de sécurité au comportement figé, puis seulement ensuite à passer à la refactorisation ou à l'ajout de fonctionnalités.
Par où commencer avec du code legacy qui n'a absolument aucun test ?
L'approche réaliste consiste à écrire des tests de caractérisation limités à la seule zone que l'on s'apprête à modifier. Couvrir tout le système par des tests n'est presque jamais tenable en termes de charge de travail, et ce n'est de toute façon pas nécessaire. Il faut d'abord identifier la sortie que produit la fonctionnalité à modifier (un état imprimé, un CSV, ce qui est écrit en base de données, etc.), puis sauvegarder dans un fichier la sortie obtenue pour des entrées représentatives afin de la figer. À l'intérieur de ce filet de sécurité, on effectue de petites refactorisations qui préservent le comportement, comme l'extraction de méthode, pour faire émerger une logique testable, avant de s'attaquer au changement réellement souhaité.
Pourquoi ne faut-il pas mélanger refactorisation et ajout de fonctionnalités dans le même commit ?
Parce que dès qu'une différence apparaît dans la sortie, on perd la capacité d'en identifier la cause. La refactorisation se vérifie en confirmant que « le comportement n'a pas changé », tandis que l'ajout de fonctionnalité se vérifie en confirmant que « seul l'endroit voulu a changé de comportement » : les critères de réussite sont opposés. En mélangeant les deux, on ne peut plus déterminer si une différence par rapport au golden master correspond à « un changement voulu » ou à « quelque chose de cassé ». Il est plus sûr de séparer les deux : zéro différence pour un commit de refactorisation, uniquement la différence voulue pour un commit d'ajout de fonctionnalité.
Quand faut-il mettre à jour le golden master (le fichier de valeur attendue) ?
Uniquement au moment où l'on change délibérément le comportement, c'est-à-dire lors d'un commit d'ajout de fonctionnalité ou de correction de bug. Lors de la mise à jour, il faut relire visuellement la différence entre l'ancienne et la nouvelle sortie, confirmer qu'elle ne contient que le changement voulu, et ce n'est qu'ensuite que l'on remplace la valeur attendue par la nouvelle sortie. Si l'on écrase mécaniquement la valeur attendue simplement parce que le test est passé au rouge, on finit par intégrer une régression (un changement de comportement non voulu) en la faisant passer pour « correcte », ce qui retire tout son sens au filet de sécurité.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog