Que tester lorsqu'on débute avec BDD?

Publié par Pascal Roy le mardi 5 mars 2013 à 15:27

Tout récemment on me demandait conseil sur la façon de mettre en oeuvre des essais de type BDD ou Behavior-Driven Development dans une équipe de développement. En gros, la personne voulait savoir comment partir du bon pied et quels étaient les pièges à éviter. Voici un extrait de son message:

Pour le BDD, j'aimerais connaître votre opinion et votre vécu. Par exemple:

  • On passe par l'interface?
  • On passe au niveau des contrôleurs (ou couche juste en bas de l'interface si vous préférez)?
  • On passe par les services directement?

J'ai pensé partager ma réponse sous forme d'un billet puisque je trouve que la question est pertinente pour la majorité des équipes qui souhaitent débuter avec le BDD. Voici donc...

Il fut un temps ou le message était de ne pas passer par l'interface utilisateur parce que c'était excessivement coûteux et vraiment difficile à faire. Il y a beaucoup de choses qu'on ne connaissait pas à l'époque et les outils étaient somme toute pas mal misérables... Il y a eu beaucoup d'améliorations des outils mais il y a également eu beaucoup d'avancement au niveau de notre compréhension pour faire face à la complexité accrue de passer par l'interface utilisateur. Pour l'avoir fait dans quelques projets, je peux dire qu'il y a moyen de gérer cette complexité, et de ne pas rendre cela significativement plus complexe que de passer par un API de plus bas niveau.

Soyons clair: si on ne passe pas par l'interface usager, on ne peut être certain à 100% qu'un User Story est "Done Done". Peu importe que l'erreur soit banale parce que la couche UI est très mince et simple (ce qui par ailleurs est plutôt rare dans la vrai vie). Une erreur banale dans cette couche peut être quand même suffisante pour faire en sorte que le user story n'est tout simplement pas fonctionnel. Cela peut être aussi bête qu'un champ qui n'est pas lié au bon contrôle. Du point de vue du client, peu importe la profondeur de l'anomalie, le user story n'est pas fonctionnel et la valeur est nulle…

La décision de ne pas faire d'essais au niveau de la couche graphique est donc laissée à l'appréciation de l'équipe selon le retour sur l'investissement anticipé. De ma propre expérience, je crois qu'il y a moyen de faire ça pour que ce ne soit pas énormément plus coûteux que de passer sous la couche UI. Voici tout de même quelques trucs à garder en tête.

Utiliser le langage d'affaires

Premièrement, les tests doivent être écrits en langage d'affaires et non pas au niveau du l'interface utilisateur. Le langage doit en être un d'intention et ne pas tomber dans le détail d'implémentation. Le détail d'un user story peut changer, mais l'intention reste généralement la même, ce qui fait que la définition des tests eux-mêmes reste assez stable. Ce qui peut changer, ce sont les fixtures (ce qui lie le texte BDD et manipule l'application sous test). Celles-ci devront tenir compte des changements dans les détails de l'application (au niveau de l'interface entre autre).

Voici un cas typique fourni par mon collègue Louis-Philippe Carignan et qui illustre très bien ce que je veux dire.

Mauvais exemple
Étant donné que je suis sur la page de création d'un nouveau projet
Et que ma liste de projets existante est vide
Quand je tape le nom du projet dans le textbox "Nom"
Alors j'appuie sur le bouton "Créer"
Et j'ai une confirmation à la page suivante

Bon exemple
Étant donné que je veux créer un nouveau projet
Et que mon portefeuille de projets est vide
Quand je crée un projet avec le nom "Nouveau projet"
Alors je peux entrer les détails de ce projet

Utiliser un langage de programmation connu de votre équipe

Idéalement, le langage des fixtures devrait être le langage de programmation de l'application. C'est une erreur d'introduire "Yet Another Language" (par exemple Ruby). Oui, les gens peuvent apprendre l'autre langage, mais de façon réaliste, ils en ont déjà bien assez de se garder à jour avec les complexités du domaine, du langage de l'application, de l'OO, des design patterns, etc...  Ajoutons aussi, et c'est beaucoup plus important que ce que les gens croient habituellement, qu'utiliser le langage de l'application permet de se servir des mêmes outils de réingénierie (refactoring) pour maintenir les fixtures, ce qui est essentiel pour que ce code soit aussi facilement modifiable que le code de l'application.

Penser globalement, agir localement

La plus grosse erreur que les gens font, c'est de penser que tous les tests d'acceptation doivent absolument passer par l'interface utilisateur lorsque la décision est prise de procéder de cette façon. C'est tout simplement faux. Par exemple, pour tester un user story qui calculerait un montant selon plusieurs paramètres avec une règle d'affaires complexe, nul besoin de faire tous les cas de tests par le UI. On peut avoir un test qui valide l'interface, comme la mise à jour des champs après avoir lancé le calcul par exemple. Mais tous les cas de tests possibles spécifiés par le client qui concerne la règle d'affaires soujacente pourraient être testés directement au niveau de la classe qui implémente la règle elle-même. Ces tests, qui sont parfois aussi des tests unitaires, peuvent tout simplement être rendus disponibles dans la batterie de tests d'acceptation. Après tout, si le client a pris la peine de spécifier ces cas de tests, il va s'attendre à les voir dans les résultats des tests d'acceptation. Dans la mesure où on a un ou plusieurs tests qui assurent que l'interface permet l'exécution des cas d'utilisation, il est inutile de passer tous les cas de tests par elle si ils peuvent être testés autrement.

Il y a donc lieu, en créant chaque test, de se demander à quel niveau il devrait être implémenté. Il ne s'agit pas de couper les coins ronds, mais bien d'éliminer des charges indirectes inutiles.

Organisation des fixtures

Une des choses qu'une équipe réalisera avec le temps est que l'organisation des fixtures est très importante. Il faut que les développeurs sachent instinctivement où regarder quand il y a une nouvelle fonctionnalité à tester. Plus le projet est gros, plus cela a de l'importance. Une approche par écran (ou contexte d'interaction) est  plus facile qu'une approche par cas d'utilisation car elle permet de créer des fixtures qui sont des abstractions d'écran. C'est un peu la même philosophie qu'on retrouve au niveau du pattern MVP (Model View Presenter) ou le Presenter représente une abstraction de plus haut niveau de l'interface. 

Par exemple, si on a un user story qui modifie l'écran de login, on ne doit pas se poser la question sur l'endroit où se trouve la fixture à mettre à jour. En créant une LoginFixture, on évite que les développeurs répartissent du code de contrôle de l'interface un peu partout parce qu'ils ne savent pas que ça existe ailleurs (un problème qu'on a vécu lors de projets chez certains clients). J'ai déjà parlé de cette approche en plus de détail dans une présentation sur l'ATDD (disponible ici) que j'ai faite pour Agile Québec et Agile Montréal en compagnie de Nicolas Desjardins.

Isoler la complexité des fixtures

N'ayez pas peur d'ajouter une couche d'aide qui cache la complexité d'accéder à l'interface afin de faciliter la vie aux développeurs qui doivent modifier les fixtures. Un exemple serait de la création d'un utilitaire qui facilite l'écriture des fixtures pour contrôler des opérations qui sont asynchrones. On peut aussi isoler les choses qui changent souvent dans l'application pour limiter les endroits où il faudra les changer dans les fixtures.

Quand ç'est bien fait, ça rend le code de tests beaucoup plus stable au point où le changement n'est pas vraiment si pénible que ça. Enfin, pas plus que pour l'application elle-même...

Conclusion

En résumé, pour répondre à la question du départ:

  • Les services peuvent être entièrement testés sans passer par l'interface utilisateur.
  • Si un user story utilise un service, ne testez pas ce dernier par l'interface (il devrait déjà avoir été testé). Assurez-vous plutôt que la couche graphique est capable de le manipuler correctement (entrée/sortie) au niveau de l'interface. Cela limite beaucoup la quantité des tests qui doivent passer par l'interface.
  • Passer par l'interface utilisateur est vraiment la seule façon d'être certain que le user story fonctionne ("Done Done" plutôt que "Most likeky done"). Vous pouvez décider de ne pas le faire, mais faites le en connaissance de cause et ne sous-estimez pas ce que vous perdez à ne pas le faire.
  • L'important est que tous les cas de tests spécifiés par le client soient inclus la suite de tests d'acceptation, mais les tests peuvent être à différents niveaux, quitte à réutiliser certains tests unitaires pour valider certains cas comme les règles d'affaires par exemple.
  • Si un user story utilise plusieurs services, ne validez pas les services eux-mêmes, mais bien que l'interface les pilotent correctement. S'il y a une interaction entre les services, il vaut probablement mieux encapsuler cette interaction dans un service composé de plus haut niveau et tester celui-ci directement.

Comme toujours, il faut être pragmatique. Il n'y a pas qu'une seule "bonne" approche. Ça dépend encore beaucoup du contexte, des technologies utilisées et des compétences de l'équipe. Il y a peut-être encore des environnements qui rendent les tests par l'interface utilisateur excessivement dispendieux. Plusieurs IDE, qui ont un objectif fort louable de rendre le développement d'interface utilisateur plus facile et rapide pour tous, imposent toutefois une architecture qui rend les tests beaucoup plus difficiles à implémenter et maintenir dans une approche BDD.

blog comments powered by Disqus

0 Comments:

Post a comment

Comments have been closed for this post.