| « en attendant le BDD... Tester une application Asp.net MVC en 10 minutes | pour les brèves, préférez Twitter » |
Bien Tester une application Asp.net MVC
Lien: http://docs.google.com/View?id=dhp3ggmx_168c5md3p52
Bien Tester ASP.Net MVC 1.0
Genèse
ASP.Net MVC est né avec plein de bonnes résolutions, et notament celle d' enfin permettre du test unitaire sur les applications Web programmées avec le framework.Net .
Je dis "enfin" car le Test ne passionne pas les foules, et pas vraiment les développeurs, encore moins les responsables ou les clients qui voient en lui une perte de temps ou source de coûts supplémentaires!
Evidemment - et paradoxalement- nos clients (ou parfois chefs de projets) aimeraient bien voir leurs applications garanties exempte de tout défaut, et donc testées, mais bien souvent n'imaginent pas que cela est possible à faire à moindre coût et par un automate.
Comme ils croient surtout que cela coûte trop cher (ou trop de temps) ils font donc passer à la trappe cette étape, qu'ils rejettent d'ailleurs -à tort- en fin de planning alors qu'elle devrait prendre place dès le tout début du projet en se fondant dans les spécifications (pour en diluer le coût).
Pour ce qui est des technologies .Net, on ne peut pas dire que le test ait été placé au coeur de la plateforme. Il a fallu tout de même attendre la version 4 du framework pour réaliser la notion de "code contract", notion omni-présente dans des langages comme Eiffel dont s'inspire pourtant largement C#.
Pour ce qui est de la programmation Web sur la plateforme Microsoft, les WebForms (ASP.Net classique) étant intestables dans leur coeur, il fallait introduire soi-même le pattern MVC, celui par qui le test est possible, ou utiliser un framework additionnel (Monorail par exemple, donc j'avais parlé ici il y a quelques années http://www.castleproject.org/monorail/index.html )
Heureusement, et presque 10 ans après l'apparition d'ASP.Net, Microsoft rend officiel l'utilisation du pattern MVC. Il était temps! Mais que de retard accumulé par rapport aux autres plateformes!
Aidé par un puissant site de présentation et d'aide aux développeurs ( http://www.asp.net/mvc/ ), les développeurs vont enfin pouvoir sortir du modèle "dictatorial" des Webforms et retrouver des façons de programmer le web plus "dans les normes", ou du moins, plus proches des autres frameworks (JSF, RubyOnRails, PHP, etc...)... et pouvoir enfin envisager de tester facilement et sereinement!
Pourtant, trouver des bons exemples de code de tests (TDD) pour ASP.Net MVC, relève du parcours du combattant: code obsolète, différentes releases de ASP.Net MVC, examples incomplets, y compris dans la doc officielle (et quand ca compile, on peut s'estimer heureux), informations contradictoires, besoin d'utiliser à la fois des Mock-ups et de de l'injection de dépendance.... de quoi décourager le plus motivés des adeptes du TDD comme moi.
Quasiment tous les articles qui datent de 2008 sont obsolètes (par exemple http://dotnetslackers.com/articles/aspnet/ASPNETMVCFrameworkPart2.aspx ) mais ils contiennent des idées qu'il faut assimiler.
Indispensable, la classe utilitaire donnée par Scott H. mais qui compile pas :((( http://www.hanselman.com/blog/ASPNETMVCSessionAtMix08TDDAndMvcMockHelpers.aspx
... que d'embuches!
Quand on sait que l'approche TDD (et BDD) [Test Driven Development et Behavior Driven Development ] est la seule qui permette de GARANTIR qu'un logiciel est conforme à ce qu'on attend de lui (via l'écriture de spécification formelles), on a besoin d'y voir clair dans les tests.
Alors voici un article en français qui explique tout, simplement (j'espère), et dont les exemples compilent!
1 - Que faut-il pour tester?
un framework de test est indispensable. Mais lequel choisir? il y en a tellement....
pour comparer d'un point de vue pratique, vous pouvez consulter cette page:
http://xunit.codeplex.com/wikipage?title=Comparisons
mais chacun a ses préférences... difficille de faire un classement objectif et trouver des critères de notations entièrement acceptés par la communauté.
Pour ma part, j'ai choisi xUnit, uniquement pour sa simplicité
http://jamesnewkirk.typepad.com/posts/2007/09/announcing-xuni.html
voici quelques unes de ses caractéristiques:
· Single Object Instance per Test Method.
· No [SetUp] or [TearDown].
· Aspect-Like Functionality.
· Reducing the Number of Custom Attributes.
· [TestFixture] was removed entirely, so tests can be anywhere.
· [Ignore] is expressed using the Skip= parameter on [Test]
· [ExpectedException] was replaced with Assert.Throws.
· [TestFixtureSetup] and [TestFixtureTearDown] are instead expressed as implementations of an interface (ITestFixture).
· Support for IDisposable was added for tests.
2 - Tester, tester.... oui mais tester quoi?
Une application, qu'elle soit Web ou pas, est un ensemble composite.
Il y a multitude de choses qui s'y passe.
Quand on n'utilise pas de pattern et qu'on ne se pré-occupe pas d'architecture logicielle, tout est mêlé dans un code "spaghetti", souvent dans peu de fichiers qui font chacun des milliers de lignes. Dans ce cas, on va avoir beaucoup de mal d'effectuer des tests clairs.
Une Séparation des Responsabilités dans le code (SoC: Separation of Concerns), une architecture N-tiers, une approche Domain Driven, un couplage lâche, une architecture bien pensée, sont évidemment les pré-requis d'un bon projet guidé par les tests (Tests Driven Development).
Le pattern MVC va beaucoup nous aider, puisqu'il sépare le code de la vue, de celui du contrôleur, et du modèle.
Tester la vue en elle même
Pour ce qui est de tester le contenu d'une page HTML, qu'une zone de texte soit bien remplie par la bonne valeur, que la bonne CSS s'applique ou que le nombre d'éléments dans une boite déroulante soit conforme à ce que l'on attend, le Unit Testing n'est pas vraiment l'outil idéal.
Quoiqu'il existe des frameworks qui ont tenté de le faire : http://nunitasp.sourceforge.net/ projet abandonné, ou http://htmlunit.sourceforge.net/ mais pas de portage .Net à ma connaissance.
Il existe malgré tout des outils de tests qui agissent directement sur l'IHM et donc dans notre cas sur le navigateur Web , avec des capacités de comprendre ce qui se passe sur la page HTML (à l’aide de notre ami JQuery) comme le très puissant Selenium IDE ( http://seleniumhq.org/ ) qui va vous générer le code NUnit de test des pages Html à intégrer dans votre solution .Net. Malheureusement vous serez dépendant d’un serveur Web pour effectuer vos tests, et le principe d’isolation des tests unitaires n’est pas respecté.
En attendant la solution qui comble la brèche…
Tester qu'une vue s'affiche bien
C’est assez trivial mais c'est un début :
|
[Fact] public void ReturnsViewResultWithDefaultViewName() { // Arrange var controller = new HomeController();
// Act var result = controller.Index();
// Assert var viewResult = Assert.IsType<ViewResult>(result); Assert.Empty(viewResult.ViewName); } |
Cela permet de vérifier que vous n'avez pas fait de grosses erreurs dans le montage de votre solution Asp.Net MVC. Malheureusement ca ne teste pas des erreurs éventuelles dans le fichier de vue (.aspx)
Notez au passage le tryptique classique d'une méthode de test 1)Arrange 2)Act 3)Assert.
Si votre contrôleur fait passer des bouts de données à la vue par l'intermédiaire du dictionnaire ViewData , il vous suffira de rajouter une ligne:
Assert.Equal("Welcome to ASP.NET MVC!", viewResult.ViewData["Message"]);
Toujours trivial. Et relativement peu utile.
La bonne pratique est d'utiliser une vue fortement typée, ce qui donne quelque chose comme ca dans l'entête de la déclaration de votre vue:
|
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<ManagePassword.Domain.OperationResult>" %> |
ManagePassword.Domain est la librairie de domaine de notre application, et OperationResult est l'objet Modèle (au sens MVC) qui va transiter entre la vue et le contrôleur.
Notre test pourra déjà s'assurer que le contrôleur renvoie bien un objet de ce type là.
|
// ASSERT Assert.NotNull(results); Assert.NotNull(results.ViewData.Model); var res = Assert.IsAssignableFrom<OperationResult>(results.ViewData.Model); |
L'idée d'avoir un modèle déterminé (et donc fortement typé) afin de pouvoir passer de multiples valeurs et tester ce que le contrôleur va retourner à la vue.
Par exemple, on a souvent besoin d'une page qui fait le résumé de l'opération qui vient d'être demandé, et un modèle simple (mais récurrent) pourrait être:
|
public class OperationResult { public bool isOk; public string errorMessage; } |
Notre test va pouvoir porter maintenant sur les membres du Model, et donc faire plus de vérifications.
|
// ASSERT Assert.NotNull(results); Assert.NotNull( results.ViewData.Model); var res = Assert.IsAssignableFrom<OperationResult>(results.ViewData.Model); Assert.Equal(false, res.IsOk); |
Tester les redirections
Une redirection peut avoir lieu dans votre contrôleur, la tester permet de savoir par quel point ^de code le contrôleur a terminé son exécution. Pour cela le code de test ressemblera à ceci :
|
///ACT var results = controler.ChangePassword(userInfo) as RedirectToRouteResult;
// ASSERT Assert.True(controler.ViewData.ModelState.IsValid); // permet de tester que c'est bien un RedirectToRouteResult qui a été retourné (à cause du AS plus haut) Assert.NotNull(results); |
La ligne 1) ne plantera pas au cas où le code du contrôleur ne renvoi pas un objet de redirection ( ). Par contre Assert.NotNull(results); génèrera un échec du test car en cas d’echec du cast par AS, la variable contient le pointeur vers Null.
Pour rappel, une méthode de test ne doit attendre qu’un seul comportement possible.
Si vous voulez tester le cas où le contrôleur ne fait pas d’indirection (et renvoi des valeurs via le modèle), écrivez alors une autre méthode de test pour ce cas là. Attention à ne pas mettre de logique IF… ELSE… dans une méthode de test. Car cela est banni.
Dans la vraie vie, le but du programme est de faire en sorte que le Contrôleur appelle un (ou plusieurs) objets du domaine avec un minimum de logique (car la logique doit être dans le domaine). Le contrôleur s’occupe de la logique nécessaire pour préparer l'affichage (et donc le Modèle pour alimenter la vue), et c'est de cela dont il va falloir maintenant s’occuper.
Tester le contrôleur tout seul
Comme je le disais plus haut, la meilleure pratique nous conseille de déporter la logique du domaine (que l'on appelle souvent Métier en France) de l'application.
Hors, dans une démarche de test, la meilleure pratique est de ne tester d’une seule chose à la fois (isolation du test).
Si nous testons le contrôleur, nous devons ne tester que les appels de code dans son corps et non pas tous les objets dont il dépend et/ou auxquels il fait appel.
Il va donc falloir « débrancher » le Contrôleur de tout ce qui pourrait parasiter le test.
Comment faire sans écrire du code compliqué, et surtout sans toucher au code du contrôleur qui doit être transparent au test ? On ne doit pas changer le code de ce qui doit être testé pour le rendre testable ! Sinon le test est biaisé.
Les 2 choses typiques à débrancher sont : le ou les objets de domaine (ou système d’accès aux données), et le contexte sous-jacent au contrôleur (routage, requête http, contexte session, etc.).
Pour ce dernier, ASP.Net MVC est assez bien fait et les dépendances en place n’empêchent pas les tests de fonctionner en isolation, c'est-à-dire sans la présence d’un serveur HTTP.
Ceci dit, si on a besoin de tester le comportement d’un contrôleur en fonction de ce qui peut se passer au niveau Requête HTTP par exemple, on a la possibilité de fournir un faux contexte HTTP.
Vous trouverez de quoi réaliser ceci avec une classe d’aide aux tests, que vous devrez intégrer dans le projet de test : MvcMockHelpers
Vous en trouvez le code ici : http://www.hanselman.com/blog/ASPNETMVCSessionAtMix08TDDAndMvcMockHelpers.aspx
Elle vous propose (entre autres) un FakeHttpContext que vous brancherez à votre contrôleur avec une méthode d’extension, comme celle ci :
|
controler.SetFakeControllerContext(); |
Cette classe repose sur un framework de Mock-up. Le code donné fonctionne avec différentes librairies : RhinoMocks, TypeMock ou Moq.
Personnellement j’ai choisi Moq, car c’est le plus « fluent » au niveau API, sans être abscon, et celui qui a la syntaxe la plus claire pour spécifier le comportement des bouchons, mais nous allons le voir plus loin.
Le seul (petit) problème c’est que cela peut ne pas compiler. Il vous faudra chercher un certain moment sur le net avant de trouver ce « hack » :
http://stackoverflow.com/questions/205644/error-when-using-extension-methods-in-c
et qui vous conseille d’ajouter ces quelques lignes de code au fichier dans lequel vous aurez mis le MvcMockHelpers:
|
namespace System.Runtime.CompilerServices { /// <summary>/// to avoid the following error: Missing compiler required member 'System.Runtime.CompilerServices.ExtensionAttribute..ctor' /// </summary> public class ExtensionAttribute : Attribute { } } |
Les méthodes d’extension SetHttpMethodResult et SetupRequestUrl vous seront bien utiles pour tester le comportement de votre contrôleur quand l’invocation se fait par une méthode HTTP GET ou HTTP POST, ou si son code vérifie l’URL demandée.
Bouchons à la rescousse
Mais une bonne partie du comportement d'un contrôleur repose sur l'appel à (et sur les réponses fournies par) un ou plusieurs objets de domaine.
Personnellement je mets toute la logique de mon application dans ces objets et je ne fais pas d'appel aux données directement. Pour ceux qui font le contraire, la logique de tests est de toute façon la même. Pour que le test puisse tourner indépendamment (sur un serveur de Build ou dans Gallio Icarus par exemple), il ne faut pas de liaison à une base de données ou à tout autre système externe.
C'est aussi pour cela que passer par des objets de domaine, même s'ils ne font que du CRUD, va simplifier le code dans le contrôleur et donc faciliter les tests.
L'avantage majeure résidant dans ces objets de domaine, puisque c'est nous qui les écrivons "à la main", c'est d'avoir une interface que les bouchons vont exploiter.
Je ne vous fais pas l'affront d'expliquer pourquoi une interface est bonne pour votre architecture.
Tout ce qui est à l'extérieur de notre contrôleur à tester devrait avoir une interface. C'est le cas par exemple d'un Web Service. Quand vous allez vous brancher à une Web Reference, Visual Studio va avant tout construire une Interface (au sens C#) et vous allez pouvoir travailler avec celle-ci.
De toutes façons, il est aisé d’admettre (ou de constater) que tous les frameworks de bouchonnage (mockup) vont exiger une interface afin de pouvoir travailler, donc vous n'avez pas le choix.
Partons du cas où nous avons été de bons élèves et où nous disposons d'une IquelqueChose pour nos objets de domaine (Si ce n'est pas le cas, un petit coup de refactoring et en 3 secondes nous auront ce précieux fichier).
Armé de cette précieuse Interface, donc, vous la passerez en carburant au constructeur du Mock. Avec Moq, cela ressemble à ceci :
|
//Arrange // create the mock var mockRepository = new Mock<IDomainManagingPasswords>(); |
L’idée est ensuite de faire passer au contrôleur, un objet factice fabriqué par le mockRepository, qui va répondre à l’interface voulue.
Donc vous aurez à modifier le code du contrôleur pour qu'il travaille non plus avec une instance interne du (ou des) objet de domaine, mais avec une référence sur l'Interface, et fourni à l'extérieur du contrôler. C’est la seule chose que vous devez modifier dans votre contrôleur pour le rendre testable, mais à bien y réfléchir, c’est plus une façon de programmer qu’une contrainte donnée par la démarche de test. Je m’en expliquerai dans un prochain article sur la qualité du code et les enjeux du test dans l’optique de la programmabilité et de la maintenabilité du code.
Cette différente façon de faire consiste à très fortement découpler les objets entre eux. A bien y réfléchir, un objet (ici le contrôleur) a toujours besoin d’un (ou plusieurs) autre objet pour réaliser le comportement attendu. Ne serait-ce que parce que nous avons à notre disposition foultitude d’objets prêt à être réutilisés.
Pour ce faire, nous avions souvent l’habitude d’instancier ces objets tiers, au moment où nous en avions besoin. Parfois, au mieux, nous procédions à leur instanciation/initialisation dans le constructeur de l’objet qui en fait usage.
Ce n’est pas une bonne pratique, car cela ne laisse aucun contrôle sur ces objets (comment ils doivent être initialisés, etc). Il est donc plus profitable (et pas que pour des besoins de tests) de passer des pointeurs sur ces objets au moins au constructeur de celui qui en fait usage, et voir même à chaque méthode dans certains cas.
Pour notre contrôleur cela donne ceci :
|
private readonly IDomainManagingPasswords _domainObject;
/// <summary> /// constructeur avec parametre pour accepter le Mocking... /// </summary> /// <param name="domainObj">The domain object.</param> public ManageController(IDomainManagingPasswords domainObj) { _domainObject = domainObj; } |
Ensuite à l’intérieur des méthodes du contrôleur, on utilise librement la variable _domainObject comme si de rien n’était.
Cette variable sera occupée par l’objet bouchon lors du test, et cela nous permettra d’ordonner au bouchon d’avoir un comportement A ou B, selon les besoins du test.
Les possibilités donc de « jouer » avec le test sont maintenant très nombreuses.
Par contre, il faut que le contrôleur continue de fonctionner dans son environnement initial, à savoir lorsque le site Web fonctionne dans IIS ou Cassini (ou Apache). Et dans ce cas, le framework Asp.Net MVC s’occupe d’initialiser les contrôleurs, et pour cela il appelle le constructeur sans paramètre de chacun ; ce qui est bien normal, car il n’est pas capable de décider quelle instance d’objet tiers lui passer.
Et évidement, dans ce contexte, on ne travaille plus avec les bouchons mais avec les implémentations « qui marchent » !
Alors comment faire ?
Inversons le contrôle
Il faudrait avoir un mécanisme qui de manière transparente à tout mécanisme (je pense en particulier au mécanisme d’initialisation de Asp.Net MVC, puisse instancier lui-même les objets « qui marchent » et les donner à ceux qui s’en serve.
Dans notre cas, il s’agit de faire passer des objets de domaine correctement instanciés et initialisés, aux constructeurs des contrôleurs lorsque ceux-ci sont mis en route par l’infrastructure de Asp.Net MVC, sur laquelle bien sur nous ne pouvons pas opérer de modification de comportement, puisque le code ne nous appartient pas et qu’il est déjà compilé.
C’est là où l’Inversion Of Control (IoC, tellement plus chic) entre en scène et trouve une de ses nombreuses utilités.
L’idée initiale de l’AOP (qui a inspiré l’Injection de Dépendance) était de permettre au développeur de supprimer la plomberie dans son code, c'est-à-dire sortir toutes les lignes de code qui n’avaient pas vraiment attrait à son algorithme mais qui étaient nécessaires ; sans quoi, justement, les objets dont il dépendait (accès aux données ou à une information tierce) ne pouvaient pas opérer comme il le souhaitait.
Dans le cas du test d’un projet Asp.Net MVC avec de l’injection de dépendance, vous trouverez foison d’exemples et de frameworks.
Quelques points d’entrées :
http://www.mikesdotnetting.com/Article/117/Dependency-Injection-and-Inversion-of-Control-with-ASP.NET-MVC
http://www.rickardnilsson.net/post/2009/12/25/Dependency-injection-in-ASPNET-MVC-with-Unity-IoC-Container.aspx
http://scottfindlater.blogspot.com/2009/11/tdd-mvc-12-inversion-of-control-ioc.html
http://codeclimber.net.nz/archive/2009/02/05/how-to-use-ninject-with-asp.net-mvc.aspx
Mais nous voulons rester pragmatiques, et le problème qui nous intéresse ici est « comment faire en sorte que le contrôleur obtienne un objet de domaine correctement instancié et initialisé quand le projet tourne en mode non-test, c'est-à-dire avec un constructeur sans paramètre ? ».
Mon critère est assez simple : j’ai retenu la solution qui m’oblige à écrire le moins de code possible, qui soit la moins intrusive dans le code existant, et qui m’épargne des lignes de configuration XML qu’on ne sait jamais comment écrire correctement (et auxquelles ont préfère une API fluente, qui permette à l’Intellisense de nous guider intuitivement dans l’usage des objets exposés).
StructureMap est l’un d’entre eux.
Nous avons écrit plus haut un contrôleur MVC doté d’un constructeur avec paramètre qui permet de faire passer une instance d’un objet de domaine.
Hors la fabrique de contrôleurs que Asp.Net MVC utilise en interne ne sait instancier les contrôleurs qu’en utilisant un constructeur sans paramètre. Il faut donc fournir une autre fabrique plus souple.
Heureusement que Scott et son équipe ont tout prévu, et que nous pouvons surcharger la fabrique de contrôleurs par défaut (DefaultControllerFactory) en préparent une autre comme ceci :
|
using System; using System.Web.Mvc; using StructureMap;
namespace yourProject { public class DependencyControllerFactory : DefaultControllerFactory { protected override IController GetControllerInstance(Type controllerType) { return ObjectFactory.GetInstance(controllerType) as Controller; } } }
|
L’ObjectFactory permettra à StructureMap de résoudre les dépendances et trouver la bonne implémentation concrète de tous ce qui présente une interface remplaçable à la volée par un objet concret.
Ce nouveau Controllerfactory sera enregistré dans la logique Asp.Net MVC en ajoutant une petite ligne dans Global.asax.cs, dans Application_Start() plus précisément :
|
ControllerBuilder.Current.SetControllerFactory(new DependencyControllerFactory()); |
Et juste ensuite (toujours dans Application_Start) nous préciserons à StructureMap quel mapping opérer entre les interfaces et les implémentations concrètes, comme ici :
|
ObjectFactory.Initialize(registry => registry .ForRequestedType<IDomainManagingPasswords>() .TheDefaultIsConcreteType<DomainManagingPasswords>()); |
Et voila!
C’est assez simple, notez juste que dans cet exemple, mon classe d’implémentation concrète DomainManagingPasswords disposerait d’un constructeur sans paramètre. Pour les cas plus complexes, lire la documentation ici : http://codebetter.com/blogs/jeremy.miller/archive/2008/08/20/smartinstance-in-structuremap-2-5.aspx
Il y a des frameworks IoC qui sont plus explicites encore, et qui vous laissent choisir quelle implémentation viendra s’injecter selon telle ou telle condition.
Cela peut être aussi en intéressant à utiliser si vous voulez par exemple, changer un fournisseur de données par un autre, à la volée dans votre programme.
http://www.iridescence.no/post/Inversion-of-Control-ASPNET-MVC-and-Unit-Testing.aspx
Mais y-a-t-il plus simple ?
Sortir les patterns qui vous font passer pour le roi de la programmation est parfois superflu.
Le crédo est d’être économe, de n’utiliser que ce dont on a besoin. Pourquoi lancer toute la machine d’un IoC, sinon à passer pour un érudit ?
Dans notre cas, nous avions simplement besoin de fournir à la factory par défaut de construction des Contrôleur Asp.Net MVC, un constructeur sans paramètre (tout en gardant celui avec paramètre qui sert aux tests).
Il suffisait d’écrire ce constructeur, et c’est lui qui se chargeait d’instancier l’objet de domaine concret répondant à l’interface IDomainManagingPasswords :
|
public ManageController() { _domainObject = new DomainManagingPasswords(); } |
Un bien pour un mal. En faisant cela nous économisons toute la coûteuse introspection qu’opère un Injecteur de Dépendances, mais nous introduisons avant compilation du couplage très fort entre nos classes…
Retour au test, comment pousser le bouchon plus loin ?
Maintenant que nous savons comment faire pour préparer notre objet testé (le contrôleur) pour pouvoir fonctionner de manière identique dans un cas de test ou dans un cas de non-test, voyons comment spécifier le comportement des bouchons pendant le test.
C’est la que notre ami Moq refait surface, et nous permet de piloter le comportement de l’objet « bouchon ».
|
// create the mock var mockRepository = new Mock<IDomainManagingPasswords>(); |
Je veux que lorsque la méthode X mon contrôleur fera appel à la méthode FindUserInAdam de l’objet de domaine qu’elle emploie, et quelque soit la valeur des paramètres, il me retourne une valeur de mon cru, rien de plus simple :
|
mockRepository.Setup(cr => cr.FindUserInAdam (It.IsAny<string>())) .Returns(new resultOfSearchingUser { errorMessage = "test error", codeResult = codeResult.NotFound }); |
Ce qui se lit (donc se programme) aisément :
Permet de poster les choses. La lambda expression permet d’exprimer ce qui se passera en cas d’appel à la méthode voulue. Les arguments sont gérés aussi par Moq. It est un objet pratique qui permet de dire « Ceci » est une valeur « peu importe » de type string : It.IsAny<string>().
Ou d’autres formules (une regex par exemple).
L’expression qui s’en suit dit quoi faire quand la méthode sera réellement invoquée. Ici c’est un retour (Returns) d’une valeur assemblée de toute pièce pour les besoins du test (new resultOfSearchingUser).
N’oubliez pas que les Mocks sont des objets creux, ils répondent à une interface mais ne font rien. Donc à vous de programmer leur comportement.
On pourrait vouloir tester comment le contrôleur réagit si l’objet Bouchon de domaine renvoit une exception. Là aussi, c’est assez simple à exprimer :
|
mockRepository.Setup(cr => cr.FindUserInAdam (It.IsAny<string>())) .Throws(new System.OverflowException("test error")); |
Pour terminer, on n’oubliera pas de donner en carburant cet objet bouchon au contrôleur avant de lancer (ACT) l’opération à tester:
|
//bind the mocked object to the controller var controler = new ManageController(mockRepository.Object);
///ACT var results = controler.DoFindUser() as ViewResult; |
A partir de là, vous avez toutes les billes en main pour tester tout ce que vous pouvez imaginer.
C’est d’ailleurs cette trop grand liberté qui peut être désarmante, et on cherche une méthode pour savoir quoi écrire comme cas de test. La réponse est 2 paragraphes plus loin.
Tester le Modèle
le "Modèle" en tant que structure de données que l'on passe du contrôleur à la vue ne se teste pas en lui-même. Par contre c'est le "fournisseur du modèle" c'est à dire l'objet que vous devez invoquer (et que nous avons substitué par un Mock) qui doit être testé pour lui-même.
A ce moment là c'est un projet de test autour des classes de domaine (classes métier pour ceux qui préfèrent ce terme) aux quelles il faut ajouter une batterie de Tests Unitaires "classiques" pour vérifier leur comportement.
Tout ce que je viens de dire sur les contrôleurs s’applique évidement aux classes de domaines. Elles doivent elles-aussi être fournie avec un jeu de test « en isolation complète ».
3 - Plaidoyer pour une approche Comportementaliste (BDD)
Si vous êtes en mal d’inspiration pour écrire vos tests, voici une idée qui pourra faire son chemin…
Le Behavior Driven Development (ou développement conduit par le comportement et donc les spécifications) devrait être LA façon de programmer universelle.
Affirmer aujourd'hui que la programmation d'un logiciel ne peut se faire qu'en fonction des spécifications passe pour une vérité des plus banales.
Or, jusque là, personne ne s'était occupé de rendre vérifiable, "prouvable" une telle démarche. Le développeur écrivait donc son programme, bon an, mal an. Et vérifiait une fois déployé (en général lors de la préparation de la recette ou de lors de la recette elle même) si le logiciel produit était bien conforme aux spécifications.
Entre écriture des spécifications (en majorité sous forme littéraire) et code (forme "machine", c'est à dire langage de développement) il y a un certain écart, pour ne pas dire un gouffre.
Ce que propose l'approche BDD c'est d'éliminer purement et simplement tout écart.
Et d'écrire les spécifications de manière formelle (donc dans une langue compréhensible par l'ordinateur) et reliée au langage du développement.
Ainsi un automate (un framework de tests unitaires est préposé à cela) peut relier la vérification des spécifications ainsi décrites au code réellement produit.
Le tout, de manière systématique, automatique, constante et répétée (dans le cadre d'une usine logicielle bien construite).
Ce ne sont pas les ressources qui manquent sur le net pour vous expliquer que faire du BDD c'est bon pour vous, essayez par exemple http://behaviour-driven.org/
Ou lisez Dan North, qui est celui qui a posé l'article fondateur : http://dannorth.net/introducing-bdd
Pour ma part, je vous proposerai bientôt un article entièrement consacré au sujet, autour d'une exemple concret bien entendu.
Le BDD est présenté par beaucoup d'éminent spécialiste comme l'aboutissement des méthodes SCRUM et XP
http://www.code-magazine.com/Article.aspx?quickid=0805061
Cette approche est également la plus fidèle à la notion d'Agilité, puisqu'elle permet d'exploiter à 100% les efforts faits en matière de spécification par UserStories et Scenarios.
Mais si elle est légion en Ruby et Python, si on la trouve beaucoup en Java, il faut dire que .Net est à la traine. Et c'est quelque peu frustrant voir même insultant (n'avons nous pas le droit de faire de meilleurs logiciels?)
Cela me rend plutôt inquiet, .Net est-il une plateforme laissée aux seuls Geeks et bricoleur de l'informatique? ou aux sociétés de service pressées de faire du chiffre et ne mettant pas vraiment l'accent sur la qualité logicielle?
Toujours est-il qu'il faut se lever tôt pour trouver un framework prêt à l'emploi, qui fonctionne (sic), et simple à prendre en main (re-sic) pour la plateforme .Net
Beaucoup de projets sont abandonnés en phase initiale (BDD Extensions project on Google Code.)
J'ai essayé de tester tout ce qui était compilable et seul NBehave semble tenir ses promesses; mais comme vous pouvez le voir dans cet article, l'écriture reste peu lisible hélas http://pebblesteps.com/post/Behavior-Driven-Development-with-NBehave.aspx
les BDD extensions de xUnit semblait offrir une forme plus sympathique à lire, mais bon.
Il y avait des initiatives très prometteuses comme MisBehave, basé sur M et Oslo... mais là encore aucun engouement, ni de la part de la communauté ou des têtes pensantes chez Microsoft (d'ailleurs, que devient M à part un textual DSL pour de la modélisation de données).
Il faudra attendre que la communauté .Net se (re)mobilise et trouve enfin de l'intérêt à faire du BDD pour voir naître un framework nous donnant vraiment envie de faire du comportementalisme et que cette pratique n'apparaisse plus comme une perte de temps, alors que justement c'est le contraire qui devrait en résulter.
4 - D’autres exemples ?
Un projet complet TDD avec MVC plus NHibernate est à découvrir (avec des pages Wiki complètes) sur http://sharparchitecture.net/
Allez voir aussi sur http://scottfindlater.blogspot.com/2009/10/tdd-mvc-adventures.html
Enjoy!
2 commentaires
il y a tout de même de l'espoir côté .NET, même si c'est un peu frustrant de voir à quelle vitesse ces idées avancent côté Ruby... Voici quelques options .NET :
- NBehave à la Cucumber http://blog.developpez.com/bruno-orsier/p8428/bdd/nbehave-a-la-cucumber/
- Cucumber avec IronRuby, ca marche ! http://blog.developpez.com/bruno-orsier/p8440/bdd/cucumber-avec-ironruby-ca-marche/
- Behavior Driven Development avec Cuke4Nuke http://blog.developpez.com/bruno-orsier/p8444/bdd/behavior-driven-development-avec-cuke4nu/
C'est donc très viable d'écrire des scénarios Cucumber pour du code .NET !
Bruno