La recherche d’un bug se résume à une enquête policière. Sherlock Holmes aurait été un excellent programmeur grâce à sa célèbre « méthode de déduction » : « Lorsque vous avez éliminé l’impossible, tout ce qui reste, aussi improbable soit-il, doit être la vérité. » – Sherlock Holmes, dans Le signe des Quatre.

I – Traquer les bugs

Une fois une fonctionnalité développée, nous avons hâte de la tester pour voir son fonctionnement. Mais personne n’est parfait et un comportement étrange, des résultats inattendus ou un crash de l’application peuvent apparaître. Dans ce type de situation, c’est qu’un morceau de code produit présente une anomalie, autrement appelé bug.

Comme dans une enquête policière il faut savoir déduire les éléments inutiles et ceux utiles à l’enquête. Le meilleur moyen de traquer un bug est de déterminer ce qui fonctionne, identifier ce qui ne fonctionne pas, simplifier le problème, générer des hypothèses et tester nos hypothèses. Il nous restera le ou les bogues rencontrés.

Durant notre enquête policière, nous allons rechercher le code coupable dans le programme suivant.
Le crime : à l’affichage de notre liste de voitures, la voiture Renault Clio a disparu, mais nous avons toujours le témoin de la voiture Renault Megane.

class Program
    {
        static void Main(string[] args)
        {
            List<Voiture> listVoitures = new List<Voiture>()
            {
                new Voiture()
                {
                    Id= 1,
                    Marque = "Ford",
                    Modele = "Fiesta",
                    Prix = 25000
                },
 
                new Voiture()
                {
                    Id= 2,
                    Marque = "Renault",
                    Modele = "Clio",
                    Prix = 15000
                },
 
                new Voiture()
                {
                    Id= 3,
                    Marque = "Renault",
                    Modele = "Megane",
                    Prix = 15000
                },
 
                new Voiture()
                {
                    Id= 2,
                    Marque = "Ford",
                    Modele = "C-Max",
                    Prix = 20000
                },
            };
 
 
            AfficheListeVoiture("Premier affichage de la liste", listVoitures);
 
            Voiture voitureDelete = listVoitures.FirstOrDefault(x => "Renault".Equals(x.Marque) && "Megane".Equals(x.Modele));
 
            listVoitures.Remove(voitureDelete);
 
            AfficheListeVoiture("Second affichage de la liste", listVoitures);
 
            Console.ReadLine();
        }
 
        private static void AfficheListeVoiture(string nameList, ICollection<Voiture> voitures)
        {
            Console.WriteLine(nameList);
 
            foreach (Voiture voiture in voitures)
            {
                Console.WriteLine($"Marque : {voiture.Marque}, Modèle : {voiture.Modele}, Prix : {voiture.Prix} euros");
            }
        }
    }
 
    internal class Voiture
    {
        public int Id { get; set; }
        public string Marque { get; set; }
        public string Modele { get; set; }
        public double Prix { get; set; }
 
        public override bool Equals(object obj)
        {
            return obj != null && obj is Voiture voiture &&
                   voiture.Marque == this.Marque;
        }
    }

Résultat :

Déterminer les parties fonctionnelles

Dans un premier temps, il est important de déterminer les parties de code qui ne présentent pas de problème. Durant l’exécution de notre programme, nous voyons que la fonction affichant la liste des véhicules affiche correctement la liste à la première exécution. Nous pouvons faire l’hypothèse que le second affichage de la liste affiche correctement l’ensemble des éléments présents dans la liste.

Nous pouvons voir qu’il y a une suppression qui est effectuée, ce qui permet dans un premier temps de mettre de côté la fonction supprimant une voiture dans la liste.

Une simple analyse visuelle du résultat de l’application nous permet de mettre en évidence les parties fonctionnelles de l’application.

Comme sur une scène de crime, il est plus difficile de trouver des indices sur le coupable dans une pièce en désordre que dans une pièce bien rangée. Si votre code est correctement structuré, il est plus simple d’identifier chacune des parties du code. Il faut aussi penser que le code produit peut-être repris ou maintenu par une autre personne.

De ce fait, la documentation du code, au travers des commentaires ou d’une documentation externe, peut aider à se repérer dans les différentes fonctionnalités du code.

Identifier les parties erronées

Après avoir écarté une bonne partie de lignes de code qui nous semblent fonctionnelles, nous avons une partie de code mise en évidence. Dans notre enquête, nous pouvons émettre l’hypothèse que celle-ci peut contenir la partie incriminée.

Notre premier suspect est la fonction récupérant la première voiture dans la liste correspondant au critère suivant : Marque égale à Renault et Modèle égal à Clio.

La plupart des IDE nous permettent de mettre des points d’arrêt durant l’exécution de notre application. Les points d’arrêt permettent d’arrêter le programme à un endroit précis dans le code, afin de vérifier que l’application, dans son exécution, à cet endroit précis correctement. Ces IDE permettent de visualiser l’état des variables au moment de l’arrêt de l’application. Sur le même principe, ils permettent d’ajouter des espions durant l’exécution, afin de voir l’état d’une variable ou d’une expression.

En vérifiant le contenu de la variable, nous voyons que la voiture sélectionnée correspond à la bonne voiture.

De ce fait, notre premier suspect ne peut-être incriminé pour ce délit. Notre enquête va s’orienter vers notre deuxième suspect : la fonctionnalité de suppression du véhicule.

D’autres outils analysant le code permettent de mettre en évidence des parties à risque. Les analyseurs de code permettent la mise en place de bonnes pratiques au sein d’une équipe, afin de faciliter la maintenance du code. Certains analyseurs de code poussent leur analyse en incluant la détection de bogue. Même si cette détection n’est pas équivalente à une recette, ces remontées d’informations permettent de mettre en évidence des erreurs de transtypage ou d’atteintes de limitation d’index par exemple.

La plupart des analyseurs de code permettent de voir l’évolution de la qualité du code, des vulnérabilités et des bogues mis en avant durant les analyses. Au fil des analyses et des remontées de bogues, nous pouvons voir sur ces analyseurs une fiabilisation de l’application.

Extraits de l’analyseur de code Sonar Cube mettant en évidence deux bogues majeurs dans une application

Générer des hypothèses

La génération d’hypothèses va nous permettre d’identifier la cause du problème. Dans une enquête policière, cette phase correspond au moment des interrogatoires des suspects et de la formulation du mobile du crime.

En informatique la méthode la plus simple pour effectuer des hypothèses est d’utiliser la méthode dite du canard en plastique. Cette méthode consiste à expliquer ligne par ligne le code à un objet ou une personne.

Dans notre exemple nous voyons que la partie suspecte est la suivante :

Voiture voitureDelete = listVoitures.FirstOrDefault(x => "Renault".Equals(x.Marque) && "Megane".Equals(x.Modele));

Si j’applique la méthode du canard en plastique, voici l’explication du code :

Dans un premier temps, je récupère le premier élément dans la liste des voitures ayant pour critère de marque Renault et pour critère de modèle Megane.

La seconde ligne permet de supprimer une voiture ayant les caractéristiques récupérées dans la première phase. Pour réaliser cette modification, la méthode Remove du Framework .Net va comparer avec la méthode Equals de la classe de la liste (ici la classe Voiture) les éléments présents dans la liste des voitures et supprimer le premier élément pour lequel la fonction Equals retournera vraie. La classe Voiture surcharge la fonction Equals et la réécrit. Pour déterminer si deux véhicules sont identiques à partir de la méthode Equals, la comparaison des objets voiture se fait sur la marque. Si deux voitures ont la même marque, alors celles-ci sont identiques.

De cette formulation, nous pouvons en déduire que nous obtenons bien une voiture de marque Renault, et de modèle Megane quand nous sélectionnons le premier élément dans la liste des voitures. Mais quand nous passons en paramètre cette voiture à la méthode Remove, la comparaison pour supprimer la voiture va se faire sur la marque puisque la réécriture de la méthode Equals indique que si deux objets voiture ont la même marque, alors elles sont identiques. Donc la méthode Remove applique la suppression sur le premier objet voiture de marque Renault trouvé, mais de modèle Clio.

Ici, nous sommes dans un cas simple où le programme comporte peu de fonctionnalités et peu de lignes. Sur des programmes plus complexes, l’écriture de tests unitaires nous permettra de formuler nos hypothèses ou de mettre en évidence la partie de code présentant un problème.

Correctif

Une fois le problème identifié et la partie de code à corriger mise en avant, il est temps d’appliquer un correctif. Dans l’exemple de cet article, l’une des solutions pour pouvoir supprimer le bon véhicule est de réécrire la méthode Equals en incluant plus de critères d’égalité.

Exemple de correctif possible de la méthode Equals :

public override bool Equals(object obj)
        {
            return obj is Voiture voiture && 
                   voiture.Marque == this.Marque &&
                   voiture.Modele == this.Modele &&
                   voiture.Id  == this.Id;
        }

Une fois le correctif appliqué, il est souhaitable d’indiquer en commentaire la raison du correctif, afin d’informer les futurs développeurs de la sensibilité de la partie.

Une autre façon de partager la connaissance du problème et d’effectuer les sessions de débogage en pair-programming. Cette méthode a pour avantage de faciliter la montée en compétence de chacun et d’utiliser l’expérience de plusieurs personnes pour investiguer et corriger le problème. Avec le pair-programming, la circulation de l’information dans une équipe est facilitée, mais il reste important de modifier la documentation de l’application avec le correctif appliqué afin de pérenniser dans le temps la modification.

Cette pérennisation du code corrigé peut être réalisée aussi à travers de tests unitaires.

Les tests unitaires permettent de vérifier le bon fonctionnement d’une petite partie bien précise (unité ou module) d’une application. Ils s’assurent qu’une méthode exposée à la manipulation par un utilisateur fonctionne bien tel qu’elle a initialement été conçue.

Voici quelques conseils pour écrire des tests unitaires :

  • Choix de l’unité testée. Pour optimiser les tests unitaires au maximum, il est important de ne tester que les éléments les plus petits possibles dans votre application.
  • L’écriture de tests unitaires est peut-être l’un des seuls domaines où être un indépendantiste est socialement acceptable 😆 Veillez à isoler vos tests unitaires au maximum et à les rendre totalement indépendants les uns des autres. Ne faites jamais appel à une base de données ou à une API externe, même si votre classe en dépend : utilisez toujours des données de test les plus proches possible des données réelles.
  • Gardez vos tests unitaires très rapides
  • Avant de réparer un bug, écrivez ou modifiez un test unitaire pour exposer ce bug
  • Utilisez le template AAA pour améliorer la lisibilité de votre test : Arrange (création des objets, des données de test et définition des attentes), Act (invocation de la méthode testée), Assert (test du résultat)
  • Testez toujours et tout le temps !

Microsoft fait un très bon résumé des bonnes pratiques à appliquer en .net core et .net standard.

Publication

Une fois le problème corrigé et la solution testée, il est temps de déployer l’application pour mettre à disposition le correctif auprès des utilisateurs.

Avant de déployer une application, il est recommandé d’incrémenter la version de l’application. Le versioning sémantique est une méthode très utilisée pour l’attribution des numéros de version.

En règle générale dans le versionnage sémantique, la version se compose en trois parties X.Y.Z:

  • X correspond à la version majeure : changements non rétro compatibles. Les évolutions majeures apportent de nouvelles fonctionnalités, en changeant radicalement l’apparence ou l’architecture du logiciel.
  • Y correspond à la version mineure : ajouts de fonctionnalités rétro compatibles, principalement des corrections de bugs, ajouts de quelques fonctionnalités.
  • Z correspond au correctif : corrections d’anomalies rétro compatibles, failles de sécurité

Explications et détails sur le versionnage sémantique sur SemVer.org

II – Identification et enquête sur les bogues en production

Les remontées des utilisateurs

Le premier indicateur sur la fiabilité d’une application est la remontée des bogues par les utilisateurs. Afin de centraliser ces remontées, de faciliter le traçage et de voir l’avancement du traitement de l’erreur remontée par l’utilisateur, il est souhaitable d’utiliser un outil de gestion des services d’assistance.

Interface de déclaration d’incident de l’application GLPI

Ces outils permettent de gérer le niveau de criticité d’une erreur, d’inscrire le parcours de l’utilisateur sur l’application…

Afin de corriger le problème rencontré par l’utilisateur, il faut pouvoir reproduire son cheminement sur une autre machine afin de déterminer l’origine du problème (le plus souvent un utilisateur interagit avec l’application sur l’environnement de production). Afin de refaire facilement le cas remonté par l’utilisateur, une copie des données présentes sur l’environnement de production peut être nécessaire. Si une copie des données de production doit être réalisée, il est important de garantir la sécurité de ces données. Le premier niveau de sécurité est l’anonymisation des données.

Les journaux applicatifs

Malgré les informations remontées par l’utilisateur sur le cheminement effectué pour rencontrer le bug, nous avons aussi besoin de connaitre d’autres informations qui ne sont pas directement visibles par celui-ci.

Afin de tracer l’activité de l’utilisateur sur une application, des log (journaux) peuvent être mise en place. 

Ces logs sont composés d’éléments appelés traces. Ces traces sont catégorisables en plusieurs niveaux de criticité. Nous pouvons mettre en évidence 4 niveaux, même si d’autres niveaux peuvent être ajoutés:

  • Error : c’est le niveau de criticité le plus élevé. Il est à utiliser pour les erreurs rencontrées par le programme qui ne sont pas gérées.
  • Warning : moins élevé que la trace de niveau Error, il est à utiliser pour les erreurs rencontrées et gérées par le programme.
  • Information : ce niveau de criticité est utilisé pour informer sur le parcours de l’utilisateur
  • Verbose : cette criticité est plutôt utilisée par le développeur au moment de la conception de l’application ou de la recette, pour tracer la valeur d’une variable par exemple, ou pour connaitre le cheminement dans le code d’une action utilisateur.

Afin de sauvegarder et d’exploiter les logs, il est normal de les sauvegarder à un endroit.

  • Cela peut être réalisé dans une table d’une base de données, mais un grand nombre de traces entraine rapidement un accroissement de la taille de la base de données si aucune purge n’est effectuée.
  • Utilisation de l’observateur d’événements Windows : celui-ci a l’avantage d’avoir une purge automatique des logs, mais il est local à chaque machine. Par exemple, sur une application lourde les logs seront enregistrés sur la machine de l’utilisateur, contrairement à une application web qui va enregistrer ces logs sur le serveur où il est exécuté.
  • Il existe aussi des packages de gestion de logs qui peuvent directement être intégrés à l’application. Ces systèmes ont l’avantage d’être paramétrables, de définir le niveau de trace à logger, de définir où enregistrer les logs et de gérer la purge des données.

Une bonne trace dans un log se doit d’être courte, avec des informations pertinentes comme le nom de la méthode où le bogue a été rencontré. On peut être plus précis avec la ligne de code de la valeur incriminée.

Plus d’articles :

Le paiement mobile

Le paiement mobile

Quels sont les pros et les cons de ce nouveau mode de paiement ? Quelles sont les technologies d’intelligences artificielles derrière cette révolution de paiement ? Quels enjeux envisage-t-on dans un futur proche ?