Non-opinion : comment aborder la programmation défensive ?

Un sujet qui revient régulièrement quand on manipule des données issues d’une source extérieure au programme (comme un fichier local, accessible sur un partage réseau, ou encore des données stockées sur une base de données…) est la gestion des erreurs.

La gestion des erreurs est essentielle pour réagir en fonction du motif d’échec, sachant que chaque motif peut entraîner des réactions différentes : impossibilité de localiser la ressource, d’y accéder en lecture, de la parcourir intégralement, données non conformes au format attendu, etc. Les possibilités sont – malheureusement – nombreuses.
Dans la plupart des cas, il est d’ailleurs impossible d’être absolument et totalement robuste avec des manipulations telles que celles sur les fichiers.

C’est un vaste sujet mais je vais ici m’attarder sur ce qui se passe après un échec. Admettons qu’une ressource n’ait pas été lue, nous laissant avec une donnée dite « nulle » (que ce soit un Nil, un None, un NULL, un nullptr, une structure ou un objet avec des membres aux valeurs par défaut, etc… peu importe).
Comment gérer cela dans un programme ?

Je vais commencer par illustrer la mauvaise façon de procéder (cela peut paraître un peu direct, mais c’est un sujet où il n’y a pas vraiment de débat, juste des bonnes et des mauvaises pratiques). Nous avons un algorithme de calcul scientifique qui doit, au démarrage, récupérer une structure de données contenant des informations sur un climat météorologique. Pour cause d’erreur, cette structure est nulle. L’exécution du programme est néanmoins poursuivie, l’algorithme reçoit cette structure, elle est passée de fonction en fonction, toujours nulle, et chaque fonction vérifie l’état de ladite structure.
Donc, chaque fonction implémente une fonction si, n’effectuant que les calculs ne nécessitant pas cette structure lorsqu’elle est nulle… c’est-à-dire parfois aucun.
Le programme fait même appel aux GPUs disponibles dans la machine afin de paralléliser au maximum ces calculs scientifiques. Le processeur est actif, les puces graphiques sont actives, et à la fin, les résultats sont obtenus : une belle grille de 0.
Du temps de perdu, de l’électricité consommée pour rien, et un programme parsemé de fonctions si qui traitent toutes le même cas.
Nous sommes ici dans le cas typique d’une structure de données absolument primordiale pour la bonne exécution du programme. Les fonctions qui en ont besoin devraient donc toujours recevoir des données exploitables et non une structure nulle.

Alors comment faire ?
La réponse vient généralement assez naturellement. Ne vérifier qu’une fois l’état de cette structure, et surtout, le plus tôt possible. Dès que la tentative de lecture a été faite, en cas d’erreur, le programme est interrompu. Cela peut se faire par le biais d’une assertion, une exception, ou d’une boite de dialogue en avertissant l’utilisateur si l’application dispose d’une interface graphique.
Si les données ont été lues avec succès, le programme peut continuer, et les portions de code nécessitant cette structure n’auront pas à vérifier sa présence.

Bien évidemment, cela ne s’applique pas dans le cadre d’une application jugée critique et qui pourrait être exposée, par exemple, à des bitflips à cause de conditions extrêmes (température, pression, accélération…), typiquement dans le monde de l’embarqué dans les transports, où il est alors indispensable d’implémenter des mécanismes de contrôle de l’intégrité des données et des robustesses aux valeurs hors des intervalles prévus.
Ne cherchons pas la petite bête 🙂

Opinion : les tests servent-ils à contrôler que l’implémentation est correcte ?

Une méthode de travail semble être à la mode dans certains secteurs de nos jours : le développement piloté par les tests. Concrètement, cela consiste à élaborer en premier lieu des tests, munis d’entrées fournies et des sorties attendues, avant de passer à l’implémentation.
Mais même si l’on ne « pilote » pas son développement par les tests, on peut tout à fait en élaborer ; c’est d’ailleurs encouragé.

C’est peut-être une mode, mais il y a des secteurs – comme l’avionique – où les tests font partie du quotidien des développeurs depuis longtemps, très longtemps. Bref !

La question est la suivante : les tests servent-ils à contrôler que l’implémentation est faite sans erreur ?

Il faut bien sûr nuancer, mais en très résumée, mon opinion très personnelle se présente en 2 points.

  • on peut tout à fait élaborer des tests après l’implémentation, et c’est très bien ainsi.
  • non, les tests ne servent pas vraiment à contrôler que l’implémentation est correcte.

Commençons par ce second point. Quand on parle de tests, entre développeurs, on fait généralement référence à des tests qu’on qualifie dans l’avionique de « tests bas niveau ». Un petit bout de code de test, avec des entrées et des sorties, appelle un petit bout de code de l’application, pour vérifier que les résultats sont corrects.
C’est à différencier des tests dits « de haut niveau », qui consistent plutôt à préparer l’ensemble des entrées, faire tourner l’application, et contrôler l’ensemble des sorties.

Cette nuance est très importante : il y a peu de chances que vous ayez besoin de quelqu’un pour vérifier si vous avez implémenté une petite fonction de la bonne façon. En revanche, il y a très certainement des chercheurs qui sauront vous dire si le programme s’est comporté correctement en ayant subi le scénario de test qu’ils auront préalablement préparé dans un simulateur spécialisé.
D’ailleurs, pour que les tests en général aient un rôle de contrôle de l’implémentation, il faut qu’ils soient élaborés par une personne qui n’est pas celle qui a réalité ladite implémentation. Si l’on écrit dans la même journée les tests et le code, il y a de bonnes chances qu’on reproduise une éventuelle erreur des deux côtés, la faisant passer inaperçue. Il est d’ailleurs très courant que cette méthode de travail soit obligatoire dans l’avionique.

Je considère donc que oui, les tests de haut niveau peuvent servir à contrôler, par l’intermédiaire de personnes hautement qualifiées sur la partie « métier », qu’un programme (ou un pan du programme) est implémenté correctement.
Mais en dehors de cette particularité, les tests, de haut comme de bas niveau, ont surtout un rôle de protection plus que de contrôle : ils nous permettent de détecter les régressions.

Ce qui m’amène donc au premier point. Implémenter un algorithme, tester soi-même le programme, contrôler les entrées et sorties… cela permet souvent de détecter bien des erreurs d’implémentation, sans même passer par des cas de test. Et, quand le sujet est bien frais, qu’on est encore plongé dedans, c’est assez peu coûteux de déboguer et détecter où se situe le problème. Est-ce vraiment strictement nécessaire de commencer par le test ?
En revanche, des semaines, des mois, voire des années plus tard, lorsqu’une autre partie du code sera altérée (probablement par quelqu’un d’autre), il sera beaucoup plus difficile de détecter les éventuels effets de bord de cette modification sur notre algorithme originel. Et il sera bien plus difficile de déboguer à travers l’application pour finalement aboutir au point de rupture.
Les tests, eux, nous permettent de nous prémunir de ce genre de situation. Aussitôt les modifications effectuées, tout effet de bord sera détecté par le biais de l’exécution des tests, ce qui permettra de détecter immédiatement quelles sont les fonctionnalités qui ont régressé.

Un bon test peut tout à fait vérifier, par des cas bien identifiés que l’implémentation est correcte. C’est généralement le but initial.
Mais un bon test va surtout nous assurer qu’elle va le rester durablement.