Affichage des articles dont le libellé est extrem programming. Afficher tous les articles
Affichage des articles dont le libellé est extrem programming. Afficher tous les articles

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.

samedi 23 juillet 2011

Les bonnes pratiques n'ont pas de cadre

Dans mon article précédent, j'ai essayé de convaincre qu'il fallait arrêter d'essayer d'adopter une conduite agile de projet dans un contexte de forfait. Cela vous attriste : vous déprimez à l'idée de travailler à l'ancienne. Vous voulez quand même de l'agilité dans votre vie de développeur. Bien.

Scrum met peu l'accent sur les pratiques de développement pourtant décrites par extreme programming. Il y en a peu mais toutes sont intéressantes.

Tout d'abord, le développement dirigé par les tests (TDD). C'est un point difficile. Si on suit les principe d'XP, aucune ligne de code ne doit passer en production si elle n'a pas d'abord fait l'objet d'un test unitaire en échec. On est dans la discipline pure. Pourtant, cette pratique permet d'avoir un contrôle quasi total du code en frôlant 100% de couverture par les tests. L'intérêt est d'autant plus grand lorsque cette pratique est couplée à de l'intégration continue (voir plus bas). Le paradigme qu'elle met en place est que la première nécessité d'un module développé est de pouvoir être testé rapidement de façon automatisée. Ce point surclasse le principe d'encapsulation cher à la programmation orientée objet. De prime abord, on peut penser qu'une telle pratique engage un surcout, surtout si on n'est pas habitué à automatiser les tests. D'habitude, on commence par rédiger le code de production et on s'assure manuellement de son bon fonctionnement. De plus, les tests représentent du code supplémentaire à maintenir ce qui n'est pas compris dans le budget... D'autorité, je dis que les avantages dépassent le surcout. Les tests sont maintenus car lancés et mis à jour avant chaque retouche de code. Le surcout en temps doit être pris en compte dans les chiffrages.
La vraie difficulté est de dépasser le barrage culturel et de former l'équipe à cette pratique.

Une deuxième pratique intéressante est l'intégration continue. L'IC consiste à faire tourner la construction du projet régulièrement, en la déclenchant par exemple sur la validation de fichiers dans le système de gestion de code source. Le point fort de cette pratique est qu'elle est peu couteuse à mettre en place, même longtemps après le début de développement, comme dans le cadre d'une tierce maintenance applicative. Les intérêts sont multiples.
Tout d'abord, vous pouvez contrôler que les sources compilent toujours après une validation. C'est un peu le niveau zéro de l'intégration[1].
Ensuite, le système permet d'installer une plateforme de test automatiquement, même vierge. Vous contrôlez ainsi qu'il n'y a pas de problème d'installation et vous disposez d'une plateforme "propre" pour faire des tests manuels.
Enfin, lorsque vous avez une bonne couverture de tests unitaires, vous pouvez les lancer à chaque mise à jour du dépôt de code source. Avec la pratique du TDD, vous mettez ainsi en place une non régression permanente pour un coût nul.
Les systèmes populaires d'intégration continue aujourd'hui sont Jenkins et Hudson (c'est le même système) mais si vous ne voulez pas (ou ne pouvez pas) utiliser Java sur votre plateforme, vous pouvez très bien développer un système simple avec des scripts et cron.

Dernière pratique d'ingénierie, le développement en binôme (pair programming). Pour moi, elle est la plus difficile à mettre en place mais ses bénéfices sont intéressants.
Le plus évident est le transfert de connaissance, l'apprentissage. C'est efficace pour montrer une technique comme le TDD, l'intérêt d'une règle de développement, des raccourcis clavier ou autre fonctionnalité de l'IDE pour aider le binôme à augmenter sa productivité. Cet aspect n'est malheureusement intéressant que pour le moins expérimenté[2].
Un autre aspect est la relecture de code, l'un pouvant voir des problèmes dans le code que l'autre ne saisit pas au premier abord ; même si la personne au clavier est la plus expérimenté, elle bénéficiera d'un autre point de vue sur son code. La discussion sera également bénéfique pour les deux parties.
Enfin, en mélangeant les binômes sur différents aspect du produit, on améliore la connaissance globale du code par l'équipe.
Il y a effectivement de bonnes choses mais il est compliqué de mettre cette pratique en place de façon systématique : elle a un côté "socialisation forcée" qui peut ne pas convenir à tout le monde. Elle est en outre difficile à argumenter avec la hiérarchie qu'on veut mettre deux personnes au lieu d'une sur une tâche. Ne prétendez pas que la tâche sera terminée deux fois plus vite : c'est faux même s'il y a un gain de productivité. C'est en terme de qualité de code et de capacité d'apprentissage de l'équipe que cela se joue.

Pour conclure ce deuxième article, disons que ces bonnes pratiques d'ingénierie ont été promue aux travers des valeurs de l'agilité. Elles sont adaptables à n'importe quel type de projet car elles mènent à une meilleure qualité du produit livré[3], avec moins de bogues mais aussi une architecture plus flexible, que l'on peut remodeler sans crainte grâce au tests. Elles visent à donner une meilleur maîtrise du processus de livraison. Soyez honnêtes, intégrez le surcout de ces pratiques dans vos chiffrages. Ne badinez pas avec la qualité du produit, votre réputation et celle de votre entreprise est en jeu.

Cet article aborde des techniques connues avec un point de vue candide mais quand je vois les gens travailler autour de moi, je pense que ça valait le cout de l'écrire.

Pour la suite, j'aborderais les pratiques de conduite de projet issues de l'agilité bénéfique même dans un cadre forfaitaire.


[1] si le code en gestion de source ne compile même pas, il va falloir en discuter avec l'équipe
[2] je me demande toutefois si la personne qui a posture de mentor dans cette situation ne va pas vouloir "briller" devant le n00b et faire une performance :).
[3] un contrat de TMA est plus rentable quand le produit n'a pas de bogue.

lundi 18 janvier 2010

I wish I had TDD.

Has you know, I was recently on a PLSQL package to produce a 60 colums report. And it is still buggy. Two lessons for me:

1. Never believe that a code work because you are told so.
2. Begin the job by creating an automated test (...suite).

Not so long ago, I though the idea of Test Driven Development was kinda weird. Why starting by writing tests ? Where is the value ???
Step by step I gave it a chance. It's the only reasonable way to do refactoring. By refactoring, I mean changing large blocks of code, not renaiming functions or deleting unuseful comments.

Now I've figured out that it is the only way to perform as stressfree as possible firefighting (that we all eventually do, don't we ?).

Try to manually check results of a 60 columns sql report 30' before delivery to validate some bug fix. No, don't even try: you can't.

Now I'll let the machine do it for me. EVER. Spending 2 hours to write an automated test suite is not a waste of time, after all.

For Kent Beck fan boys oblidged to develop in PLSQL: a unit test framework ALREADY exists : utplsql (I've never used it yet though).