jeudi 15 mai 2014

Critique du TDD

En ce moment, ça buzze pas mal sur le TDD. Pour certains, ce ne serait pas la méthode idéale de développement. Elle irait même jusqu'à complexifier inutilement le design ! Il faut choisir son camp : tests unitaires isolés des parties ou tests système de bout en bout (pas forcément en test first d'ailleurs) ?

Tout a commencé je crois avec ce post de DHH qu'il a défendu avec délice sur Twitter. De ce que j'ai compris, pour lui, le TDD basé sur les tests unitaires complique le design car il implique une volonté de tout découpler. Le design dépend alors d'abord du fait qu'on veuille tester toutes nos parties isolément avant de servir le besoin. Il préfère donc tester après et avec des tests systèmes de bout en bout. Apparemment Rails est pratique pour ça (c'est normal, c'est son bébé).

J'ai également eu une discussion avec un collègue qui me soutenait que les tests unitaires ne sont pas nécessaires. Les tests systèmes sont suffisants pour tous les cas. Il m'a dit que je faisais des tests U par peur. Lol.

Pour moi, les tests de bout en bout comme les tests unitaires sont nécessaires. Leur utilisation est à pondérer par un seul principe : Ils doivent être rapides à écrire, exécuter et à maintenir tout en assurant un niveau de qualité suffisant du livrable.

Les tests de bout en bout, ça fait rêver. Ils testent : Le besoin au travers de scénarios avec un sens métier L'architecture * Peuvent permettre de bencher l'application

J'y vois trois inconvénients. D'une part, le temps de mise en place : il y a de la configuration à faire. Créer un utilisateur, insérer des données en base, nettoyer les données après le test etc. Le fait est qu'il est souvent difficile de se mettre dans une situation initiale complètement contrôlée. J'ai fait ça sur mon projet précédent, où je testais des traitements sur un data warehouse en initialisant toutes mes données, en lançant le code PLSQL depuis Java et en vérifiant les données en sortie avec des requêtes SQL. Je peux vous dire qu'en terme de setup, ce n'était pas une partie de plaisir.

L'autre inconvénient est la vitesse d'exécution. Appeler une base, ouvrir un fichier réel sur disque ou appeler un web service prend du temps et ralentit les tests. Les tests lents sont à terme lancés moins souvent, on a moins la dynamique du TDD (regardez les katacasts, vous verrez de quoi je parle). Cette semaine j'ai codé une appli lisant des fichiers XML de deux formats différent. Le premier était simple, j'en ai extrait une partie que j'ai mis en dur dans une constante du test. Les tests s'exécutent en moins d'une seconde. Pour le second, il s'agissait d'une wsdl. J'ai eu la flemme et ai choisi d'ouvrir directement le fichier. Mes tests passent en 2 secondes. C'est long.

Enfin, toutes vos ressources externes ne sont pas forcément utilisables à volonté : vous utilisez peut-être une API payante ou appelez une appli en cours de développement ou de maintenance. Vous devez alors au moins en partie sacrifier votre bout en bout.

Dans certains cas, comme ce que remonte DHH, ces aspects sont acceptables. Par exemple, avec Django, j'utilise l'outil de test intégré, avec création d'une base de données en mémoire. La base de données est complètement recrée à chaque lancement ! Je n'ai pas de configuration à faire dans ce cas de figure et les tests sont pour l'instant plutôt rapides alors ça ne me gène pas. Lui il crâne parce qu'il est dans cette situation, mais c'est plutôt rare pour la plupart d'entre nous.

Avec du pur TDD et des tests unitaires, vous n'avez que votre code en local et vous faites des doublures pour toutes vos dépendances. Ça va super vite. Mais comment bouchonner les dépendances ?

C'est là que les mocks entrent en scène. Encore une pratique décriée. Vous lirez cet article de Uncle Bob à ce propos. Utiliser un outil de création de mock, comme Mockito en Java ou Rhino Mock en C#, permet d'implémenter une interface rapidement et de contrôler son utilisation. Implémenter une interface rapidement, c'est super dans certains cas. Une fois, je voulais tester le comportement d'une appli Java lorsqu'une base renvoyait un timeout. Je me rappelle avoir implémenté à la main l'interface de la SqlConnexion et SqlStatement ! Ça fonctionnait bien, mais c'était peu lisible. Avec un outil de mock, j'aurais fait ça en deux coups de cuiller à pot !

Toutefois j'ai aussi eu l'occasion de perdre plein de temps avec ces foutus mocks. Ils ont le défaut de permettre de pousser le bouchon trop loin (rires). Tester le comportement d'une méthode en vérifiant de quelle façon le mock a été appelé est une fonctionnalité séduisante mais elle peut impliquer elle aussi beaucoup de configuration. Qu'est-ce que le mock doit renvoyer ? Comment doit-il contrôler les paramètres de chque appel ? Arriverez-vous à comprendre pourquoi il vous renvoie une erreur alors que vous pensiez que votre config est OK ? Et que ce passe-t-il si vous changez votre implémentation, votre mock continuera-t-il à fonctionner ou faudra-t-il le reconfigurer ? Parfois, se contenter d'un bouchon ou d'une implémentation ad hoc de l'interface sera peut-être plus rapide à écrire et plus lisible tout en conservant un couplage léger avec le code d'implémentation. Ça peut rester un mal nécessaire si vous testez des méthodes qui ne renvoient rien, comme sauver un objet dans un repository.

La conclusion pour ce long article est la même que sur tout ce qui fait débat dans le monde de l'informatique : restez pragmatique. Décidez-vous sur des principes directeurs pour vos tests, ils vous guiderons dans vos choix. Le mien est que les tests doivent rester rapides à configurer et à exécuter. Selon le cas, je me contente des tests systèmes. Si ceux-ci sont trop long à configurer ou à exécuter, je vire ce qui me ralentit ou j'en fait moins pour privilégier une approche unitaire. S'il y a beaucoup de cas à tester, je vais découper le traitement et tester chaque partie isolément parce que ça me fait moins d'efforts d'écriture de tests système. Méfiez-vous des solutions toutes faites des tiers qui parlent en fonction de leurs expériences et sachez vous adapter à votre contexte.