TDD & intégration continue
Nous avons eu la semaine dernière avec l'équipe une formation en intra de 4 jours sur 2 sujets très intéressants : le TDD et l'intégration continue. Cette formation avait pour objectif de nous mettre un pied à l'étrier sur la base du TDD et de l'intégration continue. Ces 2 modules demanderont certainement par la suite un investissement personnel non négligeable mais tellement positif et passionnant, en droite ligne avec les méthodes agiles (agile dans le sens XP).
TDD
La traduction littérale donnerait : "développements pilotés par les tests", autrement dit inverser la méthode traditionnelle de programmer : coder un test puis produire le code, au lieu de coder sa logique pour ensuite, peut-être, la tester.
L'écriture d'un test se déroule selon le modèle suivant :
- écriture d'un test sur une méthode d'une classe métier (sans code métier associé), cela ne compile pas (puisqu'il n'y a pas de ... code métier),
- écriture d'un bout de code métier pour que cela compile, la classe et la méthode à tester, le test doit échouer, (l'indicateur est ROUGE / RED)
- remaniement du code métier pour que le test implémenté passe, (l'indicateur est VERT / GREEN)
- refactoring si besoin : factorisations, ... (REFACTOR)
voir le cycle sur Wikipédia.
L'écriture de tests s'effectue sous forme d'itération très courte. Chaque test ne teste qu'une seule chose à la fois (une intention, avec un et un seul but précis), reste simple (si possible sans config., ...) et rapide à exécuter, et doit rester sans état (toute modification dans une base de test devra être réinitialisée dans son état initial).
Tests avec MbUnit
Le framework utilisé pour l'implémentation des tests était MbUnit. Celui-ci évolue rapidement avec une forte communauté et semble dépasser NUnit en termes de fonctionnalités. MbUnit s'utilise à l'aide d'attributs qui décoreront la classe de test.
On trouvera pêle-mêle les attributs :
- [TestFixture] (sur la classe),
- [Test] (sur la méthode de test),
- [Setup] / [TearDown] (pour initialiser l'instance ou effectuer le ménage lors de sa destruction),
- [Rollback2] (si la DAL sous-jacente est compatible avec TransactionScope, apparue en .NET2),
- [Row] et [RowTest] (pour des jeux de tests),
- et [Ignore], [TestFixtureSetup] / [TestFixtureTearDown],
voir la documentation MbUnit, une version téléchargeable est disponible au format chm.
Chaque méthode test est décorée par [Test], dans laquelle on trouvera des assertions, celle-ci sont envoyées par la classe statique Assert.assertion(resultatattendu, resultatencours[,messageerreur]), dans laquelle on puisera parmi les dizaines de méthodes utilitaires :
Assert.AreEqual(expected,actual[,message]), Assert.IsNull(), ...
Les tests seront lancés soit par l'interface MbUnit (client dédié), soit (recommandé) par un composant installé dans Visual Studio, un add-in de type TestDriven.Net (payant, 135 €/utilisateur), un wrapper de frameworks du type MbUnit, NUnit, NCover, ...ce qui permet de lancer plus rapidement les tests et donc plus souvent.
Tests doubles
Lorsque l'on développe des classes ou des modules / composants, il est fréquent pour ne pas dire habituel, d'utiliser d'autres composants / objets. Durant les tests, on souhaite tester notre module qui utilise d'autres objets, ceux-là ne sont pas forcément développés, ou trop compliqués (DB, configurations, ...) ou tout simplement difficilement personnalisables pour être facilement intégrés à nos composants qui sont en train d'être testés.
Les Mocks sont nos sauveurs pour ces cas d'utilisation : ils vont simuler un objet externe (grâce à une interface qui définit la classe externe) ainsi que son comportement qui sera utilisé par la suite dans notre composant à tester. Le mock enregistre un scénario d'un objet A, celui-ci sera ensuite injecté dans notre objet en cours de test, et c'est là, la magie, et la puissance des mocks : pouvoir tester une multitudes de scenarii (comportements / cas d'utilisation) d'objets externes à injecter dans notre objet qui les utilise.
Le framework retenu est RhinoMocks, développé par le fameux Ayende. Celui-ci (Rhino pas Ayende) permet de simuler les différents types de Mocks : strick mock (via StrictMock<T>(), CreateMock<T>() est déprécié dans la version RH 3.5), non-strick mock (via DynamicMock<T>()) et les stubs (via Stub<T>()) selon ce que l'on souhaite simuler et le degré d'acceptation sur l'objet simulé (du point de vue des retours, appels de méthodes/propriétés, ...) (voir l'explication sur les différences). Très puissant, RhinoMocks supporte un bon nombre d'autres fonctionnalités : generics, lambda expression, délégués / événements, exceptions, écriture possible avec un style fluent.
L'usage du framework s'effectue de la façon suivante : enregistrement (via Record()) d'un scénario (appel de telle méthode avec un retour attendu précis), puis déroulement (via Playback()) du scénario avec notre objet qui s'appuie sur l'objet scénarisé. Des exemples assez clairs d'utilisation de Stubs / Mocks sont disponibles sur ce billet.
RhinoMock est très puissant. Les frameworks de type Mocks permettent de s'affranchir de tout ou partie de codes qui seraient utilisés uniquement pour les tests.
Conclusion
Quels avantages à dépenser plus de temps (car cela prend du temps de tester) pour produire un code ?
- le test sert de spécification : le développeur réfléchira un peu plus les cas d'utilisation que s'il codait directement,
- le test sert de documentation : les méthodes de test doivent être libellés de façon explicite, en lisant les tests, il devient plus facile de savoir ce que doit faire le composant testé,
- le test réduit le risque de bugs,
- le test contraint à produire un code mieux conçu pour pouvoir être testé (design patterns, IoC, refactoring, DRY, YAGNI, encapsulation / abstraction, ...)
On a pu également manipuler ReSharper, qui est un add-in très astucieux et surtout intelligent sous Visual Studio.NET. Payant, il rend à l'évidence quelques services qui aident : au refactoring, à la compréhension ou à l'écriture du code.
Ce sujet du TDD est passionnant car au delà du test, il fait intervenir bon nombre de bonnes pratiques en conception objet. Tout cela reste à creuser et surtout à pratiquer bien entendu, mais cela donne des idées et une bonne base de départ. Les frameworks MBUnit, mais surtout RhinoMocks, sont des composants denses, et promettent de bonnes journées (et soirées) à réfléchir sur ces très sérieuses problématiques de tests.
Un bouquin à lire : The art of unit testing
, toujours en cours d'écriture.
Intégration Continue
La 2ème partie fut consacrée à l'intégration continue. L'outil retenu est CruiseControl.NET, le portage iso-fonctionnel de CruiseControl en Java, nous le nommerons par la suite par son petit nom CC.
L'intégration continue s'apparente à une usine dans laquelle on aurait plusieurs types de chaînes à exécuter pour un produit : compilation, tests, couverture des tests, qualité du code, ...l'objectif est d'appeler un ensemble d'outils afin d'avoir un produit qui compile (et a fortiori qui s'exécute, enfin, en théorie), et d'agréger leur résultat dans l'usine pour l'afficher aux membres de l'équipe, tel est la mission de CC. CC est installé sur un serveur dédié.
On veillera à installer CCTray sur chacun des postes de l'équipe de développement. CCTray est un petit programme qui se loge dans la barre de tâches Windows, et indique l'état de l'usine : vert si tout est ok, ou rouge si une compilation ne passe pas, il faut alors la réparer ASAP. A partir de ce client, chacun peut forcer un build sur l'usine, après avoir mis un fix par exemple.
Parmi les outils utilisés, on trouvera :
- MbUnit ou NCover (prendre la version Discontinued Versions of NCover pour la licence gratuite, sinon cela vous en coûtera au moins 150 $) et NCoverExplorer. NCover encapsulera l'appel à MbUnit pour les tests, et s'occupera également de voir si le code est suffisamment couvert par les tests (couverture des tests),
- NAnt associé à NAntContrib (qui apporte un grand nombre de tâches supplémentaires). Nant est un makefile like mais bien plus puissant : permettra de lancer les compilations souhaitées, ou toute tâche à exécuter par l'usine (compilations, tests, packages, ...) sur la solution en cours,
- SVN : un gestionnaire de sources est nécessaire : CC détectera tout commit sur les sources, et recompilera automatiquement les sources (grâce notamment à MSBuild) : l'intégration des différents modules d'un projet s'effectue en temps réel sur l'usine, toute erreur sera alors détectée au plus tôt, et sa correction facilitée,
Chaque tâche (NAnt, MBUnit, NCover, MSBuild) produit un rapport qui sera affiché par CC après s'être exécutée. Le rapport est mis en forme grâce à une feuille de style XSL, souvent fournie par l'outil concerné.
On pourrait adjoindre à CC d'autres chaînes ou tâches, telles que des tests fonctionnels avec FitNesse, voire des tests d' IHM avec WatiN, mais c'est déjà une autre histoire.
Conclusion
Sujet également passionnant, et qui est tout sauf trivial dans sa mise en application. Cela demande la connaissance d'une multitude d'outils selon ce que l'on souhaite analyser. On pourrait aller plus loin en rajoutant FXCop pour l'analyse de la qualité du code. On pourra également s'intéresser à la technique du lapin pour s'assurer que son usine fonctionne bien.
Là encore, un modèle a été mis en place, il reste encore beaucoup à faire, mais en partant simplement, on va pouvoir in fine améliorer notre gestion de projets et de leurs évolutions, tout en s'appuyant sur des spécificités du SCM Subversion, notamment grâce aux externals (ou encore ici) pour référencer des composants tiers.
Ces 2 techniques (TDD / intégration continue) font partie de la panoplie des méthodes agiles, d'un point de vue de l'ingénierie du code (on trouvera Scrum pour la partie gestion de projets). L'objectif de tout ça étant d'apporter de la qualité dans le code, et de façon plus large aux projets.
L'intervenant était Vincent LABATUT de Winwise, que je remercie pour sa prestation.
Commentaires
Le compte rendu est très intéressant. Par curiosité, comment en êtes-vous arrivés à retenir les outils dont tu parles ?
Pourquoi MBUnit, Svn, NAnt ou CC au lieu de MSTest, MSBuild et TFS ? Et non, je ne demande pas ça à cause de Winwise
Etrange en effet de la part de Winwise hein ?
Non, en fait, dès notre 1ère rencontre, leur proposition fût bien entendu de nous orienter vers l'artillerie (trop lourde ?) des outils Microsoft : TFS, MSTests, ...Je souhaitais pour ma part axer l'intervention sur une utilisation de produits Opensource (MBUnit, RhinoMocks, CC, SVN, ...) car déjà éprouvés de longue date, avec une forte communauté et simple à mettre en place, ce que les outils MS ne sont pas forcément (avec VStudio 2005, j'avais lu de mauvaises critiques sur les outils de tests), notamment pour TFS, qui demandait trop d'investissement financier et personnel.
Et également une volonté d'adhésion au courant Alt.NET.
...et merci pour ton article sur les svn:externals qui nous a permis de mieux appréhender les références externes ! y'a plus qu'à maintenant.
Je suis aussi de plus en plus convaincu par la méthodologie TDD. Ca pousse vraiment à faire un code de meilleure qualité... C'est pas pour déplaire lorsqu'il y a de la maintenance de prévue..
J'en parle aussi mais sur du code Javascript:
http://www.poxd.org/blog/2008/12/03...