I. Introduction▲
La correction de bogues occupe une place importante dans la vie d'un développeur. Entre les bogues qui remontent durant la phase de développement et la phase de pure maintenance, nous sommes littéralement infestés.
Il faut bien reconnaître que les bogues ont un certain talent pour apparaître sous des formes très variées (erreur système, problème fonctionnel, soucis de performance…) et avoir des causes tout aussi diverses : mauvaise configuration, mauvais choix d'algorithme, mauvais format de données, cas particulier non pris en compte…
Néanmoins, il est surprenant de constater que malgré le temps qu'on y consacre, le « bug fixing » est un sujet parmi les moins discutés dans la communauté des développeurs, un peu comme si le simple fait d'évoquer le mot « bug » accroissait le risque de passer sa journée à s'en occuper.
« La peur d'un nom ne fait qu'accroître la peur de la chose elle-même. » - Albus Dumbledore
Par conséquent il s'agit peut-être de l'un des domaines d'activité du développeur parmi les moins matures, où la plupart y vont un peu « au feeling ». L'objectif de ce billet va donc être de remédier à ça en proposant une méthode pour la résolution de bogues.
II. La démarche▲
Rentrons tout de suite dans le vif du sujet, avec la délicatesse d'un troupeau d'éléphants.
La démarche de résolution de bogues est constituée de deux grandes phases, d'abord identifier le bogue, et enfin le corriger. (Là déjà, vous vous dites que vous avez bien fait de venir !).
Ce qui fait la particularité et la difficulté de la résolution de bogues est presque toujours la première phase. En effet, une fois le problème clairement posé, corriger un bogue est extrêmement similaire aux autres activités de développement, le développeur se retrouve alors de nouveau en terrain connu. Tout l'enjeu de la résolution du bogue réside dans l'analyse du problème.
« If I had an hour to solve a problem, I would spend the first 55 mns thinking about the problem and 5 minutes thinking about the solution» - Albert Einstein (en fait non, il semblerait qu'il n'ait jamais dit ça, mais tant pis, même si elle a été inventée par un sombre crétin, la « citation » est bien quand même).
III. L'identification du bogue▲
Tout d'abord, c'est quoi un bogue ? Un bogue est la différence entre le résultat attendu (tel que décrit dans les spécifications dans le meilleur cas, tel que pensait l'avoir pourtant clairement demandé le client lorsqu'il a discuté de sa vision avec le commercial à la machine à café sinon) et le résultat effectivement implémenté dans le produit. La première chose à faire est de chercher à observer le bogue, et à en comprendre les contours.
Pour cela, il faut :
Étape 1 : être capable de reproduire le bogue
La toute première chose à faire est de réussir à reproduire le bogue. Vous ne pourrez jamais être sûr que le bogue a disparu si vous n'avez pas été capable de le reproduire de manière fiable lorsqu'il était là. Vous n'aurez pas non plus de moyen à votre disposition pour pouvoir mieux le cerner.
En cas de difficultés à reproduire le bogue, il apparaît nécessaire d'aller chercher un maximum d'informations sur l'état du système au moment du bogue. Vos deux principales sources d'informations sont :
- la personne qui a remonté l'incident ;
- les logs.
Étape 2 : comprendre le comportement attendu
Le comportement attendu peut être parfois simple à comprendre, mais dans certaines applications complexes, déterminer le comportement du système lorsqu'il reçoit tel ensemble de données peut nécessiter une fine compréhension du métier. Ce savoir est néanmoins indispensable, il faut donc aller le chercher là où il est (documentation, spécifications, chef de produit/expert fonctionnel…).
Dans les cas les plus complexes, il est utile de valider le comportement attendu auprès du chef de produit. Il apparaît parfois que le comportement souhaité ne soit pas clair et/ou que fixer ce bogue soit finalement loin d'être aussi prioritaire qu'on ne le pensait. Parfois même, il n'y a finalement pas de bogue.
« It's not a bug, it's a feature» - Bill Gates
Étape 3 : circonscrire le problème
Une fois que l'on est capable de montrer ce qui ne va pas (étape 1), et que l'on sait où on va (étape 2), il va falloir circonscrire le problème afin de trouver l'endroit exact où se trouve l'erreur.
Dans certains cas, on a la chance d'avoir un message d'erreur explicite qui nous pointe directement au bon endroit, mais dans d'autres, il va falloir trouver par nous-mêmes.
Cette étape est itérative, on va d'abord déterminer quel module est fautif, puis quel composant du module, jusqu'à arriver à la fonction qui pose problème.
Prenons un exemple :
Imaginons que notre bogue concerne une application RH. Un clic sur un bouton de Daisy, la charmante assistante RH chargée de la paie, devrait permettre d'envoyer à Roger, salarié de MyPoney.Inc, son bulletin de salaire par mail, mais Daisy est désespérée, car le mail n'arrive jamais à Roger.
C'est à ce moment-là que
, que vous commencez à regarder dans le code. Le use-case est implémenté de la façon suivante dans le logiciel :La première étape est de trouver lequel des modules ne fonctionne pas comme il faudrait. Une fois que l'on a déterminé que le module RécupérationDesDonnéesDuSalarié est fautif, nous reprenons la même démarche et nous allons décomposer ce module. Celui-ci est constitué de trois sous-modules, un composant qui filtre les données auxquelles l'utilisateur à le droit d'accéder, un composant qui va chercher les données en base, et un composant qui les met en forme pour celles qui nécessitent des traitements… Nous allons ensuite chercher le responsable parmi ces sous-modules, etc.
« Et ça continue, encore et encore» - Françis, Lead Dev.
Comment détermine-t-on qu'un composant est fautif ?
Pour déterminer quel module ne fait pas correctement son travail, toujours la même démarche, comprendre ce qu'il est censé faire, observer son comportement. C'est dans cette phase d'observation du comportement que l'on utilise les techniques classiques :
- utilisation des logs (ceux déjà présents & ajouts de nouveaux si besoin) ;
- ajout de tests unitaires ;
- utilisation d'un débogueur ;
- refactoring pour clarifier le code ;
- remplacement de blocs de code complexe par le résultat attendu pour les données d'entrée jusqu'à ce que ça marche… (long, mais utile en dernier recours pour demander de l'aide sur un forum ou pour prouver la responsabilité d'un composant tierce que vous ne connaissez pas en détail).
Et si je m'aperçois qu'aucun des modules n'est fautif, que chacun fait ce qu'il lui est demandé de faire ?
« Sorry Mario, but you bug is in another castle» - Un champignon qui parle
Alors c'est sans doute un problème d'architecture. Deux cas possibles : gros souci de conception, et gros travail en perspective (ci-dessus, remplacer un module par un autre), ou alors il s'agit d'un souci (heureusement plus classique) d'interface entre deux des modules (l'un s'attend à recevoir le format A et l'autre envoie le format B).
Étape 4 : auditer le code autour
Une fois le problème à l'origine de notre bogue bien défini, il va falloir auditer le code autour. Cette phase a deux objectifs : trouver la présence d'autres erreurs connexes, déjà identifiées ou non, ayant la même cause, et vérifier qu'on ne fera pas de dégâts collatéraux lors de la phase de correction.
IV. La correction du bogue▲
Ça y est. La cause de notre bogue est identifiée. On sait également ce qu'il y a autour du bogue. Bien que le problème puisse être un gros souci de design global nécessitant trois mois de développement, dans 99 % des cas, il s'agit d'une correction de taille relativement modeste. Rien qui ne soit à la portée d'un développeur tel que vous.
Alors c'est enfin bon, me direz-vous ? On fonce et on lui règle son compte à ce maudit bogue ? Pas si vite. Le problème d'un bogue, ce n'est pas seulement que « ça ne marche pas ». Cela prouve bien souvent deux autres choses : les tests ne couvraient pas ce cas (sinon ils auraient planté, et le code ne serait jamais passé en production), et le code n'était pas suffisamment clair (sinon votre collègue n'aurait pas pu se planter. Sauf si c'est une grosse buse. Mais une grosse buse ne produit pas du code suffisamment clair, CQFD.).
Étape 5 : ajouter des tests
Certes, il est illusoire d'espérer couvrir tous les cas d'usages avec des tests. Pour rappel, même un test coverage de 100 % ne couvre pas tous les cas possibles.
Cependant, l'objectif des tests est de couvrir les cas ayant le plus de valeur pour l'utilisateur, et ceux qui sont les plus susceptibles de poser problème. Si un utilisateur a remonté un bogue, c'est qu'il a eu un problème (ça avait de la valeur pour lui, sinon il n'aurait pas pris le temps -et le risque - de créer un ticket auprès du support), et que c'est susceptible de planter (puisque ça a planté). C'est donc un cas que nos tests auraient dû couvrir.
Dans cette étape, on va donc ajouter des tests. Idéalement, pour chacun des niveaux qu'on a examinés lors de la troisième étape, le use-case global, au niveau du module, au niveau du sous-module… (notons qu'il aurait été possible de le faire en parallèle de l'étape 3, pour aider à localiser le bogue). A minima, couvrir le use-case global et les tests unitaires. Ensuite, on va rajouter les cas collatéraux que nous avons identifiés dans l'étape 4.
Enfin, si nécessaire, on va rajouter les cas qui fonctionnent bien, mais qui risquent d'être victimes de dommages collatéraux lors de la correction (dans le jargon, on appelle ça protéger ses arrières. Si vous voulez savoir pourquoi, ne le faites pas, et après vous saurez.)
Étape 6 : faire passer les tests
Vous êtes des développeurs, non ? Donc j'imagine que vous savez ce que vous avez à faire. C'est un peu dans le nom de votre métier tout de même : développe - eur.
Étape 7 : nettoyer le code
Quoi, mais on vient de régler le problème là, non ? Oui et non. Comme indiqué plus haut, si le code était limpide, votre collègue ne se serait pas planté. Vous avez passé un certain temps à comprendre les exigences, comprendre le code, son environnement. Ce serait dommage de ne pas en profiter pour améliorer la qualité du code, et graver votre compréhension dans le marbre du code. C'est l'étape de refactoring.
En réalité, vous pouvez déjà avoir commencé le refactoring dans l'étape 3, ce qui aide à comprendre l'existant (référence). De plus, selon l'importance de l'étape 6, il est tout à fait possible de suivre un cycle itératif (commencer à nettoyer, corriger un premier problème, nettoyer, corriger, nettoyer…), mais « corrigez votre bogue en sept étapes », ça a du potentiel pour faire le buzz, alors je l'ai mis là.
Vous avez peut-être remarqué que la description de la « correction du bogue » ressemble étrangement aux bonnes pratiques de développement habituelles (Test-driven development, refactoring…). C'est normal. Selon moi, c'est le meilleur moment pour les appliquer :
- les délais sont souvent moins contraints que lors de la création d'une nouvelle fonctionnalité (peu de monde se risque à donner/demander une date précise pour la correction d'un bogue qu'on ne comprend pas) ;
- un bogue étant un problème de qualité, ce n'est pas le moment le plus confortable pour vous demander de renier sur la qualité.
V. La morale de cette histoire▲
La principale étape du « bug fixing » (en terme de temps à consacrer et d'importance) est l'identification du bogue. C'est lorsqu'on ne prend pas le temps de faire cette étape que le pire devient possible : qui n'a jamais vu un junior taper son message d'erreur sur Google, prendre la première configuration/bout de code qui fait disparaître le message et considérer le problème comme résolu sans avoir compris ce qui s'est passé (et je suis bien placé pour le savoir, je l'ai fait aussi lorsque je débutais). Une fois l'étape d'identification faite, la correction devient très simple, et est l'occasion ou jamais d'appliquer les meilleures pratiques de programmation et d'améliorer la qualité du code.
Dans un sens, la démarche est extrêmement similaire à celle d'une enquête policière, d'un audit financier, ou d'un diagnostic médical : là où le médecin, l'auditeur et le policier cherchent la cause/le coupable d'un dysfonctionnement, dans un système biologique, une société humaine ou une entreprise, nous cherchons le bogue dans notre propre système complexe : une application informatique, ses milliers de lignes de code, ses centaines de dépendances.
VI. Remerciements▲
Merci à Francis Walter pour sa gabarisation de l'article et à milkoseck pour ses relectures.