Quality Code - Software Testing Principles, Practices, and Patters, un livre de Stephen Vance, couvre les différents aspects d'un cycle de développement en se focalisant sur la livraison de produits de qualité. Dans son livre, Stephen examine les pratiques de test dans l'édition de logiciel. Il parle de techniques de conception telles que la séparation entre l'implémentation et le besoin grâce à de simples exemples de code. Certains des principes de test examinés dans le livre incluent ce qui suit :
- Vérifier l'objectif plutôt que l'implémentation
- Préférer les environnements minimaux, éphémères et renouvelés
- Ecrire des tests courts
- Séparer les objectifs
Stephen a également couvert des sujets tels que les modèles de testabilité et les techniques de tests parallèles pour vérifier des conditions comme les situations de compétition ou d'étreinte fatale.
InfoQ a discuté avec l'auteur à propos de son livre et des meilleures pratiques pour tester du code applicatif.
InfoQ : Premièrement, pouvez-vous définir, pour nos lecteurs, le terme de "Code de qualité" ? Cela peut avoir différents sens selon les différentes audiences. Qu'est-ce qu'un code de qualité ?
Vance : En quelques mots, je dirais que le code de qualité est un code qui, par ordre d'importance, fait ce qu'il est censé faire, ne contient pas de bug, et est bien conçu. Pensez à un code qui est prêt pour aujourd'hui, demain, et l'année prochaine. Du code qui fait ce qui est supposé satisfaire le métier et l'utilisateur. Le code qui ne contient pas de bug se tient à part de ce monde imparfait, et gère les choses avec grâce quand il interagit inévitablement avec ce monde imparfait. Un code bien conçu peut être réparé, modifié, amélioré loin dans le futur et, avec chance, vient briser le traditionnel cycle de réécriture sans valeur qui a lieu tous les deux ans.
En partant du principe que vous savez ce que votre code est censé faire, j'essaie d'aborder la question de comment transformer fidèlement cela en code. Plus précisément, je veux être sûr que les développeurs sachent à quoi ressemble un code testable et qu'ils puissent monter en compétence dans la mise en oeuvre de modèles de code de tests.
InfoQ : Pouvez-vous nous parler un peu de "l'Objectif du Code" ? Si les IDE proposaient une fonctionnalité pour gérer l'Objectif du Code, comment fonctionnerait-elle ?
Vance : Vous avez relevé ma référence au mythique Vérificateur d'Objectif dans le Chapitre 2, n'est-ce pas ?
Trop souvent, nous écrivons notre code puis nous déterminons s'il fait réellement ce que nous voulons qu'il fasse. Nous n'avions pas d'objectif déterminé quand nous l'avons écrit. Il n'est peut-être pas si loin de la cible, mais comporte probablement des bugs et sûrement beaucoup d'éléments superflus. La programmation objectivée consiste à savoir exactement pourquoi vous écrivez chaque déclaration et à s'assurer que chacune d'entre elles a un but.
Je me concentre beaucoup là-dessus dans le livre, car un test devrait être une vraie déclaration d'objectif. Adopter cette sensibilité vous aide à affiner vos compétences de test et à produire du code qui fait ce qu'il est censé faire.
En conséquence, je suis un grand fan du test-driven development. Ce n'est pas une question de test. C'est une question d'écrire un test qui exprime votre objectif pour guider votre conception.
Comme pour les outils, si j'avais la réponse, je me plais à penser que tout le monde de l'édition de logiciel le saurait. Transformer un objectif en code est vraiment le coeur de la programmation. Nous avons joué avec les exigences, la traçabilité, la programmation graphique, les usines de développement, la logique formelle, les modèles génératifs et plus encore, et nous ne sommes toujours pas allés très loin en représentation de la sémantique. Nous développons lentement des constructions de niveaux de plus en plus élevés qui nous emmènent de plus en plus loin du métal, mais le niveau de détails nécessaire à l'écriture d'un logiciel est tellement éloigné de la granularité dans laquelle la plupart des gens vivent que cela reste un processus spécialisé et source d'erreurs.
InfoQ : Quelles sont les considérations et les pièges de la gestion des erreurs dans la programmation d'applications ?
Vance : Après avoir correctement implémenté les fonctionnalités de base, la façon dont votre logiciel gère les erreurs est le plus grand déterminant de sa réputation. J'ai lu que cela peut prendre entre quatre et dix critiques positives pour contre-balancer une critique négative lorsque les gens évaluent s'il faut ou non acheter ou utiliser quelque chose.
Alors, pourquoi la gestion des erreurs est-elle souvent traitée comme une réflexion après coup, ayant le moins d'importance dans la conception et la couverture de test ? Pourquoi tant d'erreurs visibiles par l'utilisateur apparaissent comme des messages incompréhensibles ou inutiles ? Un des éléments de réponse est que les cas d'erreur n'ont pas été considérés ou testés intentionnellement.
Au niveau du code, même les langages comme Java, qui offrent la possibilité de déclarer des exceptions, ne nécessitent pas que vous les déclariez toutes. Les exceptions non vérifiées de Java, que les développeurs utilisent de plus en plus exclusivement, permettent d'omettre toute indication explicite de l'objectif. De nombreux langages ne supportent même pas la déclaration d'erreurs. La plupart des librairies et des frameworks ont fait un piètre travail quant à la documentation de leurs erreurs, ce qui signifie que même si vous voulez bien couvrir votre logiciel, vous êtes dépendants de votre capacité à décortiquer les outils que vous utilisez.
InfoQ : Vous exposez différents aspects de test, comme le Test d'Etat ou de Comportement. Pouvez-vous nous en dire plus sur ceux-ci et sur la manière dont les développeurs devraient profiter des différentes techniques de test ?
Vance : Le test d'état se concentre sur les changements de données résultant des opérations dans votre logiciel. Les exemples classiques sont les valeurs de retour qui sont complètement déterminées par les valeurs des paramètres d'entrée ou les changements de valeur d'attributs après l'exécution d'une méthode sur un objet.
Le test de comportement se concentre sur ce qui est exécuté lorsque vous faites tourner votre application, tel que les méthodes des autres objets ou les appels de services. Par exemple, dans une méthode dont l'unique but est d'appeler d'autres méthodes dans un ordre précis et avec les bons paramètres, vous vérifieriez que ces appels sont exécutés comme prévu.
Certaines fonctions peuvent être des exemples purs de l'un ou l'autre, mais la plupart contiennent des éléments des deux. Les approches de test qui traitent exclusivement l'un de ces deux aspects peuvent vous entraver voire même vous porter préjudice. Vous pourriez ne pas avoir tous les états, et vous focaliser uniquement sur les états peut vous faire manquer des comportements critiques. La focalisation sur le comportement engendre le risque de coupler vos tests aux détails de l'implémentation et de surcharger votre application pour qu'elle supporte ce type de vérifications.
Guider vos tests grâce aux objectifs, utiliser la technique de test qui répond au mieux à votre objectif et coupler le moins possible vos tests à l'implémentation vous permet d'utiliser le meilleur des deux aspects. La distinction devrait alimenter votre modèle mental, et non polariser votre utilisation de ce modèle.
InfoQ : Quel est l'équilibre entre créer suffisamment de méthodes de test et aller trop loin ?
Vance : Il y a de nombreuses dimensions dans cette question. Un des aspects est de savoir si vous écrivez des tests redondants. Un autre est le temps de réponse de votre boucle de tests, i.e, si vous en avez tellement que votre batterie de tests ne tourne pas assez vite. Un autre encore dépend du niveau de vos tests, comme l'unité/isolation, l'intégration, l'API et le système.
La dimension sur laquelle je vais mettre l'accent, celle dont je pense vous parlez et qui est au centre de la discussion sur les tests sur de nombreux points, est l'optimisation de votre investissement. Juste en le formulant de cette façon, on comprend qu'il s'agit d'une réponse de type "cela dépend".
Si les tests guident votre développement, alors la réponse sera différente de si vous essayez simplement d'avoir une couverture de test, au moins pour la première étape du développement. Le TDD est une approche de création d'application beaucoup plus que de test d'application. C'est une approche disciplinée qui consiste à construire avec qualité en exprimant votre objectif dans un test avant d'écrire le code et en faisant évoluer graduellement votre application.
En termes de couverture de test, tout dépend de l'analyse de risque. Imaginons que vous soyez dans une start-up essayant de trouver son modèle d'affaires. Combien êtes-vous prêts à investir dans un logiciel blindé que vous avez de bonnes chances de jeter la semaine prochaine ? D'un autre côté, pouvez-vous amener, sans test, votre logiciel à une qualité suffisante pour que les bugs n'effraient pas les potentiels clients ? Si vous avez développé votre application sans beaucoup de tests et que vous pensez avoir trouvé votre modèle d'affaires, réécrivez-vous le logiciel avec une approche plus disciplinée, essayez-vous de remblayer la couverture de test manquante pour apporter une situation plus stable, ou solidifiez-vous le tout en espérant avoir fait de l'assez bon travail ? La tolérance de risques personnelle et celle de l'entreprise doivent vous guider.
Comme autre exemple, que se passe-t-il si vous avez hérité d'un projet d'application qui n'a pas de test, mais qui nécessite un petit changement ou qui sera rapidemment remplacé ? Vous ne voudrez probablement pas investir pour couvrir le système entier de tests. Il y a peut-être des domaines fonctionnels critiques ou des changements majeurs qui le garantissent cependant.
Votre application typique qui soutient un modèle d'affaires que vous espérez voir grandir dans les années à venir nécessite des tests. Pour l'anecdote, je me suis rendu compte que les tests unitaires ne sont pas rentables avant 50% de couverture d'état et vous ne verrez pas de bénéfices importants avant 70-80% de couverture d'état. J'ai appliqué des schémas de tests pour un logiciel de haute fiabilité et de sécurité critique où 100% de couverture d'état, de branche et de condition n'était qu'une étape de tests approfondis, mais les conséquences d'un échec du système étaient considérables.
Une partie de la thèse tacite du livre est qu'obtenir une grande couverture de test n'est pas aussi difficile qu'on le pense généralement. En fait, un des premiers titres proposés était "Une Haute Couverture de Tests Unitaires". L'obstacle auquel beaucoup font face est la compréhension des mécanismes de testabilité.
InfoQ : Vous parlez de l'archéologie logicielle dans le dernier chapitre. Pouvez-vous nous en dire plus sur le sujet et sur la façon dont cela favorise la qualité de l'application ?
Vance : Quand vous n'avez ni test ni documentation, vous devez retrouver l'objectif pour pouvoir tester l'application. Vous rencontrerez des moments où votre capacité à raisonner sur votre logiciel vous fera défaut. Parfois, c'est la limite de vos compétences, de votre vision, ou de votre concentration. Généralement, ce sera du au code résiduel, aux changements de contexte, aux comportements obsolètes, aux solutions étranges et autres.
Dans la section que vous mentionnez, je n'arrivais pas à déterminer à quel moment une exception était générée. Le code faisait des choses documentées comme douteuses d'après le contenu du message de l'exception. J'aurais pu injecter une exception qui semblait correspondre aux modèles attendus, mais j'aurais alors testé l'implémentation, et non l'objectif.
L'investigation permit de découvrir que ce comportement étrange venait d'une fonction appelée depuis une version antérieure, réécrite depuis longtemps. Le code de l'exception avait été copié avec elle, mais la copie originale avait elle aussi été laissée là. En conséquence, il y avait du code inutile et perturbant, et c'était difficile à tester. Tous ces éléments sont des anti-modèles d'application de qualité qui requièrent de l'archéologie pour être nettoyés.
La capacité de rechercher l'évolution du code au fil du temps nous permet de rendre le code fin, propre et testable lorsque nous avons besoin de remblayer les tests.
Stephen mentionna aussi ce qui suit à propos du fait que les limitations de test sont généralement dues à une mauvaise conception plutôt qu'à des problèmes avec la testabilité elle-même.
"Une de mes citations favorites est d'Henry Ford : "Que vous pensiez pouvoir, ou pensiez ne pas pouvoir - vous avez raison". Cela s'applique aussi aux tests. Comme je le montre dans le Chapitre 3, même l'un des problèmes les plus compliqués, qui consiste à reproduire les situations de compétition, peut être apprivoisé grâce à une grande collection de situations. La difficulté dans le test est plus souvent de constater que le code qui est testé a des problèmes plutôt qu'un problème inhérent à la testabilité."
A propos de l'auteur du livre
Stephen Vance a travaillé à quasiment tous les postes liés au développement de logiciel au cours des 20 dernières années. Il s'est attaqué à des problèmes tels que la réalité augmentée, la robotique industrielle, les infrastructures internet, le commerce d'entreprise et de logiciels en tant que service dans différents secteurs industriels. Il est intervenu, a formé et présenté à l'échelle internationale le processus de développement de logiciels et la gestion de configuration. Il est actuellement Coach Agile/Lean en Edition de logiciel à Boston.