les tests unitaires en pratique, par patrick smacchia

les tests unitaires en pratique, par patrick smacchia les tests unitaires en pratique par patrick smacchia depuis quelques années, la crème des experts mondiaux dans le développement logiciel (john vlissides, martin fowler, erich gamma, grady booch, kent beck…) encourage les développeurs à écrire des tests unitaires destinés à tester leurs classes, avant même que celles-ci soient développées. cette pratique est appelé test driven development (tdd) ou aussi test first development (tfd). dans cet article, nous allons commencer par expliquer comment mettre en œuvre les principes du tdd sous .net et quels sont les bénéfices que l’on peut espérer en retirer. dans un second temps, nous nous appliquerons à exhiber les problèmes rencontrés dans la pratique du tdd ainsi que leurs solutions. pré-requis 1 introduction au test driven development (tdd) 1 quel est le principe du tdd ? 2 qu’est ce qu’un test unitaire ? 2 a quoi servent les test unitaires? 2 un premier exemple avec l’outil nunit 2 les bénéfices évidents 6 un exemple plus réaliste illustrant la notion d’objet mock 6 des bénéfices moins évidents mais très intéressants 8 les problèmes rencontrés en pratique. 9 préserver l’encapsulation grâce à la réflexion. 9 où doit-on garder le code des tests unitaires et des classes mock? 11 l’explosion combinatoire des valeurs des entrées 11 la couverture du code. 12 tester le code d’une classe abstraite. 13 les problèmes posés par un environnement multi-threaded. 13 tester le code impliquant des accès bd. 14 tester le code impliquant des accès distants 16 tester le code d’une interfaces graphiques utilisateurs (riches ou légères) 17 tests unitaires vs. tests de recette. 18 adopter les principes tdd sur un projet existant 19 les coûts des tests unitaires 20 conclusion. 21   pré-requis cet article ne requiert aucune connaissance technique poussée particulière. il faut juste que vous ayez déjà pratiqué un minimum dans un langage objet quelconque. introduction au test driven development (tdd) quel est le principe du tdd ? le principe du tdd est très simple : le développeur doit rédiger un ensemble de test unitaires pour chaque classe, avant même que le squelette de celle-ci n’ait été écrit. cela explique l’appellation test-first. qu’est ce qu’un test unitaire ? dans la définition du principe du tdd, seul le terme test unitaire peut éventuellement prêter à confusion. un test unitaire est un bout de code qui provoque l’exécution d’un autre bout de code et qui en analyse le résultat. les caractéristiques d’un test unitaire sont les suivantes: ·        automatique : un test unitaire s’exécute automatiquement à une certaine étape du cycle du développement, en général juste après la compilation du composant qui contient le code à tester. cet automatisme se retrouve aussi dans la production des diagnostics des éventuels problèmes détectés. ·        répétable : un test unitaire est indépendant de l’environnement dans lequel il est exécuté. il peut être répété à souhaits sur n’importe quelle machine de développement, par n’importe quel personne impliquée dans le développement. ·        disponible : un test unitaire a la même disponibilité que le code source qu’il teste. si vous avez accès à une partie du code source, même qu’en lecture, vous devez donc être capable d’exécuter le(s) test(s) unitaire(s) associé(s). en pratique un test unitaire est une méthode d’une classe. il n’y a pas nécessairement une bijection entre les classes des tests unitaires et les classes de l’application. cependant, si une classe à tester admet plus d’une classe de tests unitaires, il est probable qu’elle ait trop de responsabilités et que sa conception soit à revoir (principe de la responsabilité unique d’une classe). en revanche, il est courant qu’une même classe de test unitaire teste le code de plusieurs classes d’une application. dans ce cas, il est souhaitable que ces classes soient dans le même composant (principe de la réutilisation commune). a quoi servent les test unitaires? tout développeur sait bien que plus un bug est détecté tôt dans le cycle du développement, moins il faudra d’énergie pour le corriger. une fois le bug identifié et reproduit, sa correction prend en général quelques minutes au développeur qui en est responsable. ainsi, la quasi-totalité de l’énergie nécessaire pour venir à bout d’un bug est consommée dans la détection du bug, sa reproduction ainsi que dans le déploiement du correctif associé. dans ce contexte, les tests unitaires sont les meilleures candidats pour aider un développeur à détecter et à corriger les bugs dans son code, avant même que ceux-ci aient été sauvegardés dans la base de code commune à l’équipe. tout cela sonne bien marketing alors vite, passons au concret. un premier exemple avec l’outil nunit l’outil de choix à l’heure actuelle pour pratiquer le tdd sur vos projets .net se nomme nunit. le plug-in vsnunit permet d’utiliser nunit directement à partir de visual studio .net. il est regrettable que microsoft ne propose aucun équivalent de nunit, et (à ma connaissance) ne projette pas d’en fournir un. nunit se présente sous la forme d’une dll managée nunit.framework.dll que vous devez référencer à partir des assemblages contenant le code des tests unitaires. l’outil nunit comprend aussi deux exécutables permettant d’exécuter les tests unitaires, simplement en spécifiant le nom des assemblages contenant le code des tests unitaires. nunit-gui.exe est un exécutable pourvu d’une interface graphique évoluée alors que nunit-console.exe et un exécutable en mode console, très pratique pour être lancé à partir d’un script. les copies d’écran de cet article sont réalisées à partir de vsnunit. voici un bref aperçu du fonctionnement de nunit : ·        nunit sait qu’une classe contient des tests unitaires car elle est marquée avec l’attribut nunit.framework.testfixtureattribute. dans une telle classe, un test unitaire est une méthode publique non statique marquée avec l’attribut nunit.framework.testattribute. a chaque exécution des tests unitaires, nunit instancie les classes marquées avec testfixtureattribute et exécute les tests unitaires un à un, dans leur ordre de déclaration dans la classe. notez que le nom d’une méthode marquée avec l’attribut nunit.framework.testattribute constitue aussi le nom du test unitaire. vous devez donc porter une certaine attention à ce nom. notez aussi que nunit reconnaît et exécute les tests unitaires grâce au mécanisme de réflexion. ·        le code d’un test unitaire contient en général l’appel aux méthodes des classes à tester ainsi que des assertions. en effet, nunit propose la classe nunit.framework.assert qui présente les méthodes istrue(), isfalse(), isnull(), isnotnull(), referenceequal(), equals(), areequal(), aresame(), fail(). un test unitaire est considéré comme concluant si durant son exécution aucune assertion n’est violée et aucune exception n’est lancée et non rattrapée. ·        comme on peut aussi vouloir tester si une exception est bien lancée lors de l’exécution d’un test unitaire, l’attribut nunit.framework.testattribute peut être paramétré avec l’attribut nunit.framework.expectedexceptionattribute qui prend en argument un type d’exception. le test est alors considéré comme concluant seulement si une exception de ce type est lancée et non rattrapée durant son exécution. ·        le principe du tdd étant d’écrire les tests unitaires avant le code à tester, il y a forcément un laps de temps durant lequel des tests unitaires sont supposés tester du code non encore écrit. pour ne pas être gêné par les échecs de tels tests, l’attribut nunit.framework.testattribute peut être paramétré avec l’attribut nunit.framework.ignoreattribute qui prend en argument une chaîne de caractères censée décrire la future condition de validité du test. ·        bien souvent, les tests unitaires contenus dans une même classe ont du code d’initialisation et de finalisation similaire. nunit vous offre la possibilité de factoriser le code de l’initialisation dans une méthode publique non statique marquée avec l’attribut nunit.framework.setupattribute. bien évidemment cette méthode est appelée automatiquement avant toute exécution des tests unitaires. le code de finalisation peut être factorisé dans une méthode publique non statique marquée avec l’attribut nunit.framework.teardownattribute. cette méthode est naturellement appelée automatiquement après les exécutions de tous les tests unitaires de la classe concernée. ·        enfin, nous souhaitons souligner que vous avez le choix de garder le code de vos tests unitaires dans le même composant que le code à tester, ou dans un autre composant. nous discutons un peu plus loin des différentes motivations guidant ce choix. nous sommes maintenant fin prêt pour illustrer toutes ces possibilités par un exemple concret. supposons que vous deviez développer une classe qui valide des adresses e-mail. il y a en fait deux types de validations : la validation syntaxique qui sera faite grâce à une expression régulière et la validité de l’existence de l’adresse e-mail, en questionnant directement le serveur par le protocole smtp. ne vous inquiétez pas, il ne sera pas nécessaire de rentrer dans les détails obscurs du protocole smtp puisque franklin.net fournit ce code tout prêt (http://www.franklins.net/dotnet/mailchecker.zip). de plus, nous allons nous intéresser dans un premier temps à tester unitairement le code de la validité syntaxique d’une adresse e-mail. pour cela il faut prévoir : ·        un test unitaire nommé testemailsyntaxarobas pour tester les problèmes syntaxiques liés au @, ·        un test unitaire nommé testemailsyntaxextension pour tester les problèmes syntaxiques liés à l’extension (.com ; .org ; .fr …), ·        un test unitaire nommé testemailsyntaxentreenulle pour tester le fait que le code de la validité syntaxique est supposé renvoyé une exception de type nullreferenceexception si on lui fournit une référence nulle en entrée (n’oubliez pas que le type string est un type référence). puisque chacun de ces tests unitaires nécessite une instance de la classe emailaddressvalidator, il est judicieux de factoriser le code de la création d’une telle instance dans une méthode marquée avec l’attribut nunit.framework.setupattribute. notez aussi que nous prévoyons un test unitaire nommé testemailexistence qui testera le code de test de l’existence de l’adresse e-mail. voici le code : using system; using nunit.framework; using system.collections; using system.text.regularexpressions;    //----------------------- la classe à tester ----------------------- class emailaddressvalidator {    public bool checkemailaddresssyntax(string semailaddress)    {       if(semailaddress == null ) throw new nullreferenceexception();       return regex.ismatch(semailaddress, @"^[\w\.\-]+@[a-za-z0-9\-]+(\.[a-za-z0-9\-]{1,})*(\.[a-za-z]{2,3}){1,2}$");    }    public bool checkemailaddressexistence(string semailaddress)    {       // todo à implémenter       return false;    } }   //----------------------- les tests unitaires ----------------------- [testfixture] public class testemailvalidator {    emailaddressvalidator eav;      [setup]    public void setup()    {       eav = new emailaddressvalidator ();    }    [teardown]    public void teardown()    {       // rien à faire ici    }      [test]    public void testemailsyntaxarobas()    {       assert.istrue(eav.checkemailaddresssyntax ("nom@entreprise.com"));       assert.isfalse(eav.checkemailaddresssyntax ("nom@@entreprise.com"));       assert.isfalse(eav.checkemailaddresssyntax ("nomentreprise.com"));       assert.isfalse(eav.checkemailaddresssyntax ("@entreprise.com"));       assert.isfalse(eav.checkemailaddresssyntax ("nom@"));    }      [test]    public void testemailsyntaxextension()    {       assert.istrue(eav.checkemailaddresssyntax ("nom@entreprise.com"));       assert.isfalse(eav.checkemailaddresssyntax ("nom@entreprise.comcom"));       assert.isfalse(eav.checkemailaddresssyntax ("nom@entreprise.c"));       assert.isfalse(eav.checkemailaddresssyntax ("nom@entreprise."));    }      [test,expectedexception(typeof(nullreferenceexception))]    public void testemailsyntaxentreenulle()    {       eav.checkemailaddresssyntax (null);    }      [test,ignore("verification de l'existence d'un email, pas encore implementee.")]    public void testemailexistence()    {       assert.istrue(eav.checkemailaddressexistence ("patrick@smacchia.com"));       assert.isfalse(eav.checkemailaddressexistence ("toto@smacchia.com"));    } } ce code compile directement et voici ce qu’affiche vsnunit : imaginez que nous retirions l’arobas dans l’expression régulière @"^[\w\.\-]+[a-za-z0-9\-]+(\.[a-za-z0-9\-]{1,})*(\.[a-za-z]{2,3}){1,2}$". elle ne peut plus détecter la validité syntaxique d’une adresse e-mail et vsunit affiche alors : les bénéfices évidents après ce petit exemple concret, il est très clair que le principe du tdd apporte les bénéfices suivants : ·        détection rapide de la plupart des régressions potentielles : lors de l’ajout de nouvelles fonctionnalités à un produit il est très courant d’introduire de nouveaux bugs sur des parties de code anciennes, considérées comme stables. si vous avez un minimum d’expérience, vous savez bien que cette nuisance, connue sous le nom de régression, est des plus courantes et des plus coûteuses. plus que tous principes de design anti-fragilité du code (par exemple ne pas utiliser l’héritage d’implémentation j), les tests unitaires constituent une solution efficace à ce problème puisqu’une grande partie des régressions seront détectées dès leur première compilation. ·        documentation du code : a l’instar d’une bonne documentation, les tests unitaires évoluent avec le code et sont disponibles en permanence. ils constituent un moyen unique pour que le développeur d’une classe communique à ses clients les bonnes façons de l’utiliser. ·        permet de mesurer l’état d’avancement d’un projet : la proportion de jaune sous l’interface graphique de nunit (ou de vsnunit) est égale à la proportion de fonctionnalités qui reste à implémenter si vous appliquez correctement le tdd. il y a peu de métriques objectives de l’avancement d’un projet et celle-ci est donc bien une aubaine. sami a récemment poussé le raisonnement un peu plus loin dans son blog, en proposant de lier automatiquement via xml cette métrique à un outil de gestion de projet tel que ms project 2003. un exemple plus réaliste illustrant la notion d’objet mock il est légitime d’être gêné par un article qui se dit pratique et qui s’appuie sur une règle aussi simpliste que la vérification d’une adresse e-mail. en pratique, on est amené à tester des règles métiers beaucoup plus complexes, faisant intervenir plusieurs classes métiers avec des guis, de la persistance et des accès distants. le but des tests unitaires n’est pas de tester la chaînes des appels de a à z déclenchée par un cas d’utilisation mais de ne tester qu’un maillon de la chaîne, aussi complexe soit-il (nous reviendrons sur ce point). il est facile pour un test unitaire de se faire passer pour une couche appelante de la logique à tester. cependant rien ne nous garantie à priori que l’échec d’un test unitaire n’a pas été entraîné par un bug situé dans une des couches de code sur laquelle s’appuie la logique métier à tester. la notion d’objet mock constitue la solution à ce phénomène indésirable. un objet mock partage la même interface (au sens type .net) que les objets utilisés par la logique métier à tester. la différence est que les objets mock sont conçus spécialement pour se comporter d’une certaine manière, la manière qui nous permet de valider la logique à tester. notez que dans la littérature la notion d’objet mock (empruntée à la tendance xp) est aussi nommée design pattern service stub (martin fowler). illustrons tout ceci par un petit exemple. supposons que la logique métier à tester soit la suivante : la validité de l’existence d’une adresse e-mail n’est déclenchée que si la validité syntaxique de l’adresse e-mail est concluante. pour tester unitairement cette logique, on ne souhaite clairement pas dépendre ni de la logique de validation syntaxique de l’adresse e-mail ni de la logique de la validation de l’existence de l’adresse e-mail. la moindre régression dans ces logiques pourrait entraîner des échecs dans nos tests unitaires. pire encore, même une panne réseau pourrait momentanément entraîner des échecs dans nos tests unitaires (puisque la validité de l’existence d’une adresse e-mail nécessite des accès réseaux). on est spontanément poussé à créer deux objets mock, un pour remplacer chacune de ces validations. il est assez naturel que chacun des objets mock présente une interface telle que iemailaddressvalidator qui présente au moins une méthode du style bool checkemailaddress(string semail). voici à quoi ressemblerait alors le code : //----------------------- code à tester ----------------------- using system; using nunit.framework; using system.collections;   interface iemailaddressvalidator {    bool checkemailaddress(string semail); }   class emailcontroller {    private iemailaddressvalidator m_emailaddresssyntaxvalidator;    private iemailaddressvalidator m_emailaddressexistencevalidator;      public emailcontroller(   iemailaddressvalidator syntaxvalidator,                              iemailaddressvalidator existencevalidator)    {       if( syntaxvalidator == null || existencevalidator == null) throw new nullreferenceexception();       m_emailaddresssyntaxvalidator = syntaxvalidator;       m_emailaddressexistencevalidator = existencevalidator;    }      public bool validateemailaddress(string semail)    {       if( semail == null ) throw new nullreferenceexception();       if( ! m_emailaddresssyntaxvalidator.checkemailaddress(semail) ) return false;       return m_emailaddressexistencevalidator.checkemailaddress(semail);    }    // autre exemple de méthodes pour cette classe: saveemailaddressindb, sendemail, getallemailfromdb... }     //----------------------- les tests unitaires et les objets mock-----------------------   class mockemailaddressvalidator : iemailaddressvalidator {    public bool breturn = false;    public bool bhavebeencalled = false;    public bool checkemailaddress(string semail)    {       bhavebeencalled = true;       return breturn;    } }   [testfixture] public class testemailcontroller {    mockemailaddressvalidator mocksyntax;    mockemailaddressvalidator mockexistence;    emailcontroller ectrl;       [setup]    public void setup()    {       mocksyntax = new mockemailaddressvalidator();       mockexistence = new mockemailaddressvalidator();       ectrl = new emailcontroller(mocksyntax,mockexistence);    }      [test]    public void testvalidateemailaddress_validateexistenceifonlysyntaxok()    {       mocksyntax.bhavebeencalled = false;  mockexistence.bhavebeencalled = false;       mocksyntax.breturn = false;          mockexistence.breturn = false;       ectrl.validateemailaddress("toto@toto.org");       assert.istrue(mocksyntax.bhavebeencalled);       assert.isfalse(mockexistence.bhavebeencalled);         mocksyntax.bhavebeencalled = false;  mockexistence.bhavebeencalled = false;       mocksyntax.breturn = true;           mockexistence.breturn = false;       ectrl.validateemailaddress("toto@toto.org");       assert.istrue(mocksyntax.bhavebeencalled);       assert.istrue(mockexistence.bhavebeencalled);    }      [test]    public void testvalidateemailaddress_returntrueifsyntaxandexistence()    {       mocksyntax.bhavebeencalled = false;  mockexistence.bhavebeencalled = false;       mocksyntax.breturn = true;           mockexistence.breturn = true;       assert.istrue(ectrl.validateemailaddress("toto@toto.org"));    }      [test,expectedexception(typeof(nullreferenceexception))]    public void testentréenulle()    {       ectrl.validateemailaddress(null);    } } des bénéfices moins évidents mais très intéressants dans l’exemple précédent, nous avons vu qu’un certain nombre de choix de design nous ont semblé naturel pour la seule raison qu’il fallait tenir compte des tests unitaires : ·        il semblait naturel que la logique de validation syntaxique d’une adresse e-mail ne soit pas dans la même classe que la logique de validation d’existence d’une adresse e-mail. ·        il semblait naturel d’introduire une interface pour abstraire la logique de validation d’une adresse e-mail. ·        il semblait naturel qu’une classe tel que emailcontroller ne soit pas couplée avec une implémentation de validation d’une adresse e-mail. autrement dit, nous avons spontanément renforcer la cohérence en isolant les différentes logiques de validation dans des classes différentes et il semblait naturel de découpler les implémentations grâce à des abstractions. tous ces choix sont clairement des bons choix de design car il fallait tenir compte de notre besoin d’objets mock. on peut certainement en conclure que : le fait d’écrire des tests unitaires nous amène à une meilleure conception du code testé. les problèmes rencontrés en pratique préserver l’encapsulation grâce à la réflexion le code d’un test unitaire doit avoir une vue intime de la classe qu’il teste. par exemple, ce code peut être amené à manipuler des membres privés ou protégés. plusieurs astuces existent pour contourner ce problème : ·        on peut placer le code des tests unitaires dans des méthodes de la classe à tester. cette solution tend à alourdir le code source de la classe à tester. de plus, elle oblige à placer le code à tester et le code des tests unitaires dans le même fichier, ce qui, nous allons le voir, n’est pas toujours souhaitable (cet argument n’est pas valable dans whidbey puisqu’une classe peut être définie sur plusieurs fichiers). cette solution ne fonctionne pas si le test unitaire doit connaître intimement plusieurs classes. enfin, cette solution tend à introduire des failles de sécurité en introduisant des points d’accès publics sur la classe à tester. ·        une autre solution consiste à placer le code des tests unitaires dans le même composant et à augmenter la visibilité des membres privés à la visibilité interne au composant. il est clair qu’augmenter la visibilité consiste à affaiblir l’encapsulation est n’est donc pas une bonne chose. ces solutions deux solutions sont bancales. il existe heureusement une alternative satisfaisante non encore directement supportée par nunit, mais que vous pouvez néanmoins appliquer. l’idée consiste à accéder aux membres privés par le biais de la réflexion. ceci est possible seulement si l’assemblage contenant les tests unitaires a la permission typeinformation de system.security.permissions.reflectionpermission (par défaut, les assemblages que vous développez ont cette permission sur votre disque). si cet assemblage a aussi la permission memberaccess vous avez de plus accès aux types non visibles (i.e encapsulés dans d’autres types avec une visibilité non publique). supposons que la méthode checkemailaddresssyntax de notre premier exemple ait une visibilité privée et réécrivons notre test unitaire en utilisant la réflexion : ... using system.reflection; using system.security.permissions; ... //----------------------- la classe à tester ----------------------- class emailaddressvalidator {    private bool checkemailaddresssyntax(string semailaddress)    {       if(semailaddress == null ) throw new nullreferenceexception();       return regex.ismatch(semailaddress, @"^[\w\.\-]+@[a-za-z0-9\-]+(\.[a-za-z0-9\-]{1,})*(\.[a-za-z]{2,3}){1,2}$");    }    ... }   //----------------------- les tests unitaires ----------------------- [testfixture] public class testemailvalidator {    emailaddressvalidator eav;    methodinfo micheckemailaddresssyntax;      [setup]    public void setup()    {       eav = new emailaddressvalidator ();         // note : demand() va lancer une exception si la permission n'est pas accordée.       // comprenez bien que le programme marche aussi si on enlève ces deux lignes.       // ces deux lignes n'augmentent pas l'ensemble des permissions, elles permettent       // seulement de s'assurer que l’on a bien la permission d’invoquer des membres privés.       reflectionpermission rp = new reflectionpermission(reflectionpermissionflag.typeinformation);       rp.demand();             assembly a = assembly.getassembly(typeof(emailaddressvalidator) );       type t = a.gettype(   "emailaddressvalidator",                             false,  // ne pas lancer d'exception si pb                             false); // tenir compte de la casse       micheckemailaddresssyntax = t.getmethod("checkemailaddresssyntax", bindingflags.nonpublic | bindingflags.instance );    }       [test]    public void testemailsyntaxarobas()    {       assert.istrue((bool) micheckemailaddresssyntax.invoke(eav,new object[] { "nom@entreprise.com" } ));       assert.isfalse((bool) micheckemailaddresssyntax.invoke(eav,new object[] { "nom@@entreprise.com" } ));        ...    }    ...    [test,expectedexception(typeof(nullreferenceexception))]    public void testemailsyntaxentreenulle()    {       // notez bien que l’exception de type nullreferenceexception lancée durant l'invocation       // est encapsulée dans une l’exception de type targetinvocationexception du fait que l'on       // invoque la méthode par réflexion.       try{micheckemailaddresssyntax.invoke(eav,new object[] { null } );}       catch(targetinvocationexception ex){throw ex.innerexception;}    }    ... } où doit-on garder le code des tests unitaires et des classes mock? les tests unitaires et les classes des objets mock servent les mêmes objectifs et ont les mêmes dépendances, à savoir les interfaces des classes à tester. il est donc normal que leur code soit stocké au même endroit. il n’y a pas de règles bien établies quant à l’endroit où doit être stocké ce code. plusieurs facteurs antagonistes entrent en jeu : ·        l’accès en écriture au code des tests unitaires peut être critique. pour être certain de maîtriser toutes les évolutions d’un projet, un responsable peut souhaiter interdire l’accès en écriture aux tests unitaires à ses développeurs. dans ce cas, le code des tests unitaires ne peut être dans le même fichier que le code des classes à tester. ·        un facteur à prendre en compte est la sécurité. il est préférable que le code source et le code compilé des tests unitaires ne soit pas déployé hors de l’entreprise puisque nous avons vu qu’il constitue de facto une documentation du code source (même obfusqué). un autre argument allant dans le même sens est que l’on ne veut pas alourdir les composants déployés avec les tests unitaires. notez que l’on peut éventuellement contourner ce problème en utilisant des directives de pré compilation pour empêcher de compiler le code des tests unitaires lors d’une build destinée à être déployée. ·        un dernier facteur à prendre en compte est la disponibilité des tests. si le code compilé des tests unitaires est en permanence dans le même composant qui contient le code à tester on aura une disponibilité maximale. ce facteur s’oppose donc exactement au précédent. voici un tableau récapitulatif pour vous aidez  dans une prise de décision: facteur à prendre en compte   endroit où stocker les tests : gestion du type d’accès aux sources des tests (lecture/écriture) non déploiement du code compilé des tests unitaires (footprint + sécurité) disponibilité des tests dans la classe du code à tester dans le même fichier - - + dans la classe du code à tester dans des fichier différents (whidbey) + - + dans le même composant mais pas dans la même classe + - + dans la classe du code à tester dans le même fichier avec directive de précompilation - + - dans la classe du code à tester dans des fichier différents (whidbey) avec directive de précompilation + + - dans le même composant mais pas dans la même classe avec directive de précompilation + + - dans un composant différent + + - l’explosion combinatoire des valeurs des entrées une problématique potentielle lors de l’écriture de tests unitaires est l’explosion combinatoire des entrées. dans l’exemple de la méthode qui valide syntaxiquement une adresse email, il est clair que l’on peut écrire un test unitaire efficace en ne testant qu’une vingtaine d’adresses email. la raison est que ce problème particulier admet peu de dimensions (la position et la présence de l’arobas, la taille et la présence de l’extension, l’interdiction de certains caractères) et que chacune de ces dimensions admet peu de valeurs représentatives (arobas au début, plus d’une arobas, arobas à la fin…). en théorie, il serait assez simple de fabriquer un problème avec de nombreuses dimensions, chacune ayant un très grand nombre de cas particuliers (par exemple tous les nombres de 1 à 10000000). en pratique, on se rend compte que l’ensemble des valeurs possibles représentatives d’une entrée est assez restreint. par exemple, si votre test admet une entrée de type entier positif, il suffit en général de la tester avec 0, 1 un petit nombre (2 ou 10) et un grand nombre (123456 ou 123456789). la couverture du code un problème plus ardu que l’explosion combinatoire des valeurs des entrées est la gestion des multiples chemins que peut prendre une exécution. les tests unitaires doivent être conçus pour balayer un maximum de chemins possibles et idéalement, tous les chemins. pour aider le concepteur des tests dans sa tâche, il existe des outils permettant de donner une approximation du ratio de chemins couverts sur l’ensemble des chemins possibles, ce qu’on appelle la couverture du code. ces outils sont relativement nouveaux dans la communauté .net (03/2004). nous avons testé l’outil ncover disponible sur le site gotdotnet. il faut savoir qu’il existe un autre outil homonyme disponible sur sourceforge. ces deux outils sont de plus assez similaires : ils sont prévus pour fonctionner avec nunit et ils reportent le nombre de fois que chaque instruction il du code à tester a été exécutée durant les tests. pour cela ils se basent sur la possibilité de profiler le clr à l’exécution (plus de détails sur le profiling du clr ici). bien qu’un addin vs.net existe pour l’outil ncover de gotdotnet, nous estimons qu’il est plus pédagogique d’expliquer son fonctionnement en mode ligne de commande. ·        l’option /c permet de saisir la ligne de commande permettant d’exécuter les tests. ·        l’option /a permet de donner le nom des assemblages qui contiennent le code dont on souhaite mesurer la couverture. notez que la casse est prise en compte pour le nom d’un assemblage et il ne faut pas mettre l’extension .exe ou .dll. notez aussi que ces assemblages doivent être compilés en mode debug et que leurs fichiers pdb correspondant doivent être dans le même répertoire. supposons que nous avons compilé le code de notre premier exemple dans un assemblage nommé emailasm.dll. la ligne de commande à écrire pour utiliser l’outil ncover serait alors : c:\program files\ncover>ncover.exe /c  "c:/program files/nunit v2.1/bin/nunit-console.exe d:proto/nunit/emailasm/bin/debug/emailasm.dll" /a emailasm   ncover produit en sortit un fichier coverage.xml dont voici un extrait : ...     <method name="checkemailaddresssyntax" class="emailaddressvalidator">       <seqpnt visitcount="10" line="11" column="3" endline="11" endcolumn="29" document="d:\proto\nunit\test2\class1.cs"/>       <seqpnt visitcount="1" line="11" column="30" endline="11" endcolumn="65" document="d:\proto\nunit\test2\class1.cs"/>       <seqpnt visitcount="9" line="12" column="3" endline="12" endcolumn="114" document="d:\proto\nunit\test2\class1.cs"/>       <seqpnt visitcount="9" line="13" column="2" endline="13" endcolumn="3" document="d:\proto\nunit\test2\class1.cs"/>     </method> ... il y a pour l’instant peu d’outils pour manipuler ce fichier xml mais rien ne vous empêche de produire vos propres statistiques à partir de ces données. en appliquant la transformation coverage.xsl fournie avec ncover sur le fichier coverage.xml .vous obtenez une présentation html du style : emailaddressvalidator.checkemailaddresssyntax visit count line column end line end column document 10 11 3 11 29 d:\proto\nunit\test2\class1.cs 1 11 30 11 65 d:\proto\nunit\test2\class1.cs 9 12 3 12 114 d:\proto\nunit\test2\class1.cs 9 13 2 13 3 d:\proto\nunit\test2\class1.cs signalons enfin qu’un nouveau type d’outils destinés à optimiser la couverture de code émerge. ainsi, l’outil jester (qui a un homologue .net nester en développement) tente de localiser le code non couvert. cet outil tente aussi de révéler des problèmes potentiels dans le code couvert. pour cela, il applique des changements sur le byte code (bornes d’une boucle for, condition…) et repasse les tests unitaires sur le code modifié. si les tests passent malgré la modification, il tente d’établir un diagnostic et vous le communique. le principal problème de tels outils est qu’ils sont très gourmands en ressources (les tests unitaires sont exécutés de multiples fois) et il est encore tôt pour se prononcer sur leur réelle efficacité en pratique. signalons enfin que les diagnostics produits par tout outil de couverture de code ne donnent qu’une approximation de la réalité. aussi, en tant que chef de projet il est inutile d’imposer un ratio de 100% de code couvert à votre équipe. tester le code d’une classe abstraite il peut arriver que vous souhaitiez tester le code d’une classe abstraite indépendamment du code de ses classes dérivées. en effet, il est assez courant qu’une classe abstraite ne soit pas située dans le même composant que ses classes dérivées. en outre, nous avons vu qu’il est préférable qu’un test unitaire ne fasse pas intervenir le code de plus d’un composant. dans ce cas, il est clair que le test unitaire ne peut instancier directement la classe abstraite. la solution à ce problème est simple mais nous préférons la souligner : il suffit de rédiger une classe mock instanciable, qui dérive de la classe abstraite à tester. les problèmes posés par un environnement multi-threaded notez que nous considérons ici qu’une application est multi-threaded à partir du moment où elle fait intervenir au moins deux threads simultanément, dans un seul processus ou non. parmi le top 3 des bugs les plus coûteux, il est clair que ceux issus d’un problème de concurrence d’accès à une ressource dans un environnement multi-threaded sont bien classés. ces bugs se divisent en deux catégories : les interblocages (deadlock en anglais) et les situations de compétitions (race conditions en anglais). ces problématiques sont détaillées dans le chapitre 5 de mon ouvrage pratique de .net et c# téléchargeable en pdf ici. ces bugs résultent d’un état inattendu d’une application multi-threaded. or, le nombre d’états potentiels d’une application multi-threaded est colossal à cause du caractère non-déterministe introduit par plusieurs unités d’exécutions simultanées. ainsi en pratique, on ne peut que rarement être certains que l’ensemble des états possibles soient couverts par des tests unitaires multi-threaded. il y a cependant un point positif à souligner : les tests unitaires sont eux aussi non déterministes puisqu’ils font intervenir plusieurs threads. ainsi, le grand nombre d’exécutions inhérent à tous test unitaire joue en notre faveur puisqu’il permet de démultiplier l’ensemble des états testés. un autre problème dû aux tests unitaires faisant intervenir plusieurs threads peut provenir de la durée d’exécution. il faut être certains que le test sera concluant ou révèlera un échec en un temps raisonnable et borné. pour obtenir cette garantie, le mieux est de détruire sciemment les threads impliqués dans un test unitaire après un certain délai fixé par le concepteur du test. le cas échéant, il faut considérer que le test a échoué. par exemple une telle situation peut révéler un interblocage. un dernier point à considérer est le fait que certains problèmes de concurrence ne peuvent se révéler que dans un environnement muti processeurs. dans le cas d’une application destinée à tourner sur ce type de machine il vaut mieux effectuer les tests sur un tel environnement le plus souvent possible. l’outil nunit n’admet pas encore à ma connaissance de plug-in permettant de simplifier les tests faisant intervenir plusieurs threads. il serait pourtant assez simple d’ajouter un attribut paramétrable (nombre de threads, délais d’attente maximale…) aux méthodes de tests unitaires destinées à être exécutées simultanément par plusieurs threads (avis aux amateurs…). en attendant, rien ne vous interdit de manipuler les threads vous même en vous inspirant par exemple de cet article (en trois parties). tester le code impliquant des accès bd tester du code qui entraîne l’utilisation d’une base de données est certainement la problématique la plus couramment rencontrée lorsque l’on fait du tdd. par définition, les données d’une bd sont persistantes. elles survivent aux exécutions d’une application et donc, aux exécutions des tests unitaires. on met alors en défaut le critère de répétitivité qui caractérise un test unitaire, puisque l’environnement dans lequel s’exécute le test peut varier d’une exécution à l’autre. il existe deux approches pour aborder ce problème : ·        l’utilisation d’un objet mock qui se substitue à la couche d’accès aux données lors de l’exécution des tests unitaires. dans ce cas, bien évidemment, le concepteur du test unitaire ne doit pas espérer détecter un quelconque bug dans la couche d’accès aux données. ·        le remplissage de la base de donnée à chaque exécution du test unitaire. nous allons illustrer ces techniques en supposant que notre application de manipulation d’adresses email utilise une base de données. nous supposons que la couche d’accès aux données présente l’interface suivante : interface iemailaddressdb {    bool insertemailaddress(string semail);    bool deleteemailaddress(string semail);    stringcollection getallemailaddresses(); } l’introduction d’une interface pour manipuler les données se fait de manière fluide. ici aussi, les bonnes pratiques de design découlent naturellement de l’approche tdd. en se basant sur l’interface iemailaddressdb, les objets mock qui se substituent à notre couche d’accès aux données lors de l’exécution des tests unitaires peuvent être des instances de la classe suivante : class mockemailaddressdb : iemailaddressdb {    private bool m_breturninsert;    private bool m_breturndelete;    private bool m_emailaddresses;    public mockemailaddressvalidator(bool breturninsert,bool breturndelete, stringcollection emailaddresses)    {       m_breturninsert = breturninsert;       m_bbreturndelete = bbreturndelete;       m_emailaddresses = emailaddresses;    }    bool insertemailaddress(string semail){return m_breturninsert;}    bool deleteemailaddress(string semail){return m_bbreturndelete;}    stringcollection getallemailaddresses(){return m_emailaddresses} } dans la seconde approche qui consiste à remplir la bd à chaque exécution des tests unitaires, voici à quoi pourrait ressembler le code de nos tests unitaires: ... [testfixture] public class testemailaddressdb {    const string scnx = "server = localhost ; database = foo";    const string saddr1 = "pierre@entreprise.com";    const string saddr2 = "paul@entreprise.com";    const string saddr3 = "jacques@entreprise.com";    idbconnection cnx;    iemailaddressdb db;       [setup]    public void setup()    {       cnx = new oledbconnection(scnx);       cnx.open();       idbcommand cmd = new oledbcommand("drop table emailadresses");       cmd.connection = cnx;       // rattrape une exception au cas ou la table n’existait pas.       try{cmd.executenonquery();}catch(exception){}       cmd.commandtext = "create table emailadresses (address nvarchar(50) not null)";       cmd.executenonquery();       cmd.commandtext = "alter table emailadresses add constraint address_primaire primary key (address)";       cmd.executenonquery();       cmd.commandtext = "insert into emailadresses values ('"+saddr1+"')";       cmd.executenonquery();       cmd.commandtext = "insert into emailadresses values ('"+saddr2+"')";       cmd.executenonquery();    }      [teardown]    public void teardown()    {       cnx.close();    }      [test]    public void testvalidateemailaddress_validateexistenceifonlysyntaxok()    {       // test getallemailaddresses       stringcollection col = db.getallemailaddresses();       assert.istrue(col.count == 2);       assert.istrue(col.contains(saddr1));       assert.istrue(col.contains(saddr2));         // test insertemailaddress       db.insertemailaddress(saddr3);       col = db.getallemailaddresses();       assert.istrue(col.count == 3);       assert.istrue(col.contains(saddr1));       assert.istrue(col.contains(saddr2));       assert.istrue(col.contains(saddr3));         // test deleteemailaddress       db.deleteemailaddress(saddr1);       col = db.getallemailaddresses();       assert.istrue(col.count == 2);       assert.isfalse(col.contains(saddr1));       assert.istrue(col.contains(saddr2));       assert.istrue(col.contains(saddr3));    } } ... lorsque l’on utilise cette technique il faut suivre une règle essentielle : les tests unitaires ne doivent jamais manipuler des données en production. en effet, le code du test commence par détruire les tables concernées si elles existent (drop table). je connais un (très) grand compte qui a eu ses données en production polluées par les tests d’un développeur. trois ans après cette maladresse, elle entraînait encore des bugs en production. pour éviter un tel scénario catastrophe, vous devez absolument prévoir au moins une base de données dédiées aux tests unitaires. idéalement, chaque machine susceptible d’exécuter les tests unitaires devrait avoir une telle base de données. en plus d’isoler les différentes exécutions, cette pratique force les tests à ne manipuler que des chaînes de connexion contenant localhost ou 127.0.0.1. cela réduit d’autant les risques de se connecter par inadvertance à une base de données en production. ce risque potentiel peut aussi amener les responsables à ne pas donner aux développeurs l’accès en écriture au code source des tests unitaires. l’autre gros problème engendré par la manipulation d’une base de données par les tests unitaires se situe au niveau des performances. a chaque exécution des tests unitaires et donc en général, à chaque compilation, des bases doivent être détruites, reconstruites et manipulées. dans le cas d’un projet conséquent le délai introduit peut devenir inacceptable (plusieurs minutes). si l’on ne peut pas optimiser l’exécution des tests, il faut alors envisager de les exécuter moins souvent au détriment de la détection asap des bugs. un dernier problème récurrent qui arrive dans cette situation est dû au stockage de dates dans la base de données. imaginez qu’il soit prévu dans une application de ne pas manipuler certaines données antérieures à plus d’un mois car elle sont alors considérées comme périmées. si les tests unitaires se font avec des données constantes, il arrivera un moment où ils échoueront. ils ne sont donc pas répétables. il faut donc en général initialiser les données de type date en fonction de la date d’exécution du test unitaire. tester le code impliquant des accès distants considérons qu’il y a trois catégories d’accès distants: ·        ceux où nous ne sommes responsables que du code de l’appelant (certains clients riches, vérification par dns de la validité d’une l’adresse email, envoie d’un document à imprimer…). ·        ceux où nous ne sommes responsables que du code de l’appelé (service, application web…). ·        ceux où nous sommes responsables du code de l’appelé et de l’appelant (objet distribué…). il est clair que les tests unitaires faisant intervenir un accès distant de première catégorie doivent utiliser un objet mock simulant le niveau le plus bas de la communication. en effet, une fois développé et testé, le code responsable d’un protocole de communication est aussi stable que le protocole lui même. en outre un serveur distant peut être indisponible et on ne va pas vérifier qu’un document a été imprimé à chaque exécution des tests unitaires. il n’y a pas non plus beaucoup de polémiques en ce qui concerne les tests unitaires faisant intervenir un accès distant de deuxième catégorie. le test unitaire doit se faire passé pour un client en local. il peut s’apparenter alors à un test de recette. si il y a plusieurs niveaux dans le protocole utilisé (authentification, encryptions, compression…) il vaut mieux tester unitairement chacun de ces niveaux et prévoir des tests unitaires présupposant que chacune de ces tâches s’est exécutée avec succès. notez que dans la section suivante, nous exposerons un outil pour tester les applications asp.net avec nunit. un accès distant de troisième catégorie ne pose pas de problèmes particuliers puisqu’on peut toujours le voir comme deux accès distants, un de première et un de deuxième catégorie. vous pouvez néanmoins envisager des tests unitaires couvrant du code du coté appelant et du code du coté appelé en utilisant la notion de domaine d’application .net. dans ce cas, il est de la responsabilité du test unitaire de créer et de remplir (avec les assemblages concernés) le(s) domaine(s) d’application(s) nécessaires à la simulation des processus distants. précisons qu’en pratique, les bugs dus à la présence d’accès distants sont surtouts des problèmes de déploiement et de configuration (présence d’un firewall…). pour les détecter avant la mise en production, vous devez effectuer des tests de déploiements. tester le code d’une interfaces graphiques utilisateurs (riches ou légères) comme le nom l’indique, les interfaces graphiques utilisateurs (gui) sont faites pour être utilisées par des humains. en conséquences, il est problématique de les tester à partir d’un programme. il y a deux types approches pour tester unitairement une gui : ·        simuler les actions d’un utilisateurs (click d’un bouton, remplissage d’une text box…) puis analyser les conséquences. ·        tester directement le code sous jacent à la gui. la première approche induit un challenge technique : comment simuler les actions d’un utilisateur ? en ce qui concerne les clients web asp.net il existe un outil très pratique qui adresse cette problématique : nunitasp. nunitasp se base sur nunit, il n’y a donc pas de surprises quant à sa philosophie. il fournit une bibliothèque de classes spécialement conçues pour agir sur les contrôles de vos pages asp.net (labeltester, listcontroltester, usercontroltester…). nunitasp propose aussi la classe webformtestcase dont devront hériter vos classes ‘test fixtures’. cette classe présente des commodités comme un champ browser qui est une instance de httpclient. la classe httpclient est aussi fournie par nunitasp et permet de simuler les caractéristiques d’un navigateur (facilités pour la gestion des cookies, des urls…). le mieux est encore d’illustrer l’utilisation de nunitasp à partir d’un exemple (repris du site de nunitasp) : ... [test] public void testexample() {    // first, instantiate "tester" objects:    labeltester label = new labeltester("textlabel", currentwebform);    linkbuttontester link = new linkbuttontester("linkbutton", currentwebform);    // second, visit the page being tested:    browser.getpage("http://localhost/example/example.aspx");    // third, use tester objects to test the page:    assertequals("not clicked.", label.text);    link.click();    assertequals("clicked once.", label.text);    link.click();    assertequals("clicked twice.", label.text); } ... vous trouverez ici quelques ‘bonnes pratiques’ concernant l’utilisation de l’outil nunitasp. en ce qui concerne les clients riches winform et les clients office (word, excel) il n’y a pas (à ma connaissance) d’outils qui permettent de simuler le comportement d’un utilisateur. vous êtes donc obligez d’adopter la seconde approche pour tester ce type de ui : tester directement le code sous jacent à l’interface graphique. cette approche se base sur un design pattern bien connu de tous ceux qui développent sérieusement des interfaces graphiques : le model view controller (mvc). en utilisant le mvc pour tester unitairement le code sous jacent à notre interface graphique, le but est clairement de réduire le nombre d’instructions dans la classe dérivant de system.windows.forms.form aux seuls appels du contrôleur. encore une fois le principe de tdd nous pousse naturellement vers une bonne pratique de design, le découpage vue/contrôleur. notez que cette seconde approche s’applique aussi parfaitement aux applications asp.net. l’idée est ici aussi de minimiser le nombre d’instructions dans la classe dérivant de system.web.ui.page. l’avantage de cette seconde approche par rapport à l’utilisation d’un outil tel que nunitasp est double : ·        elle pousse le développeur à considérer un découpage vue/contrôleur, très utile par exemple pour supporter différentes formes de gui pour la même logique (web, riches…). ·        pour des raisons d’ergonomie et d’esthétique les interfaces graphiques sont certainement un des domaines les plus versatiles du développement software. en couplant vos tests unitaires avec vos interfaces graphiques, vous vous exposez aux risques de devoir les maintenir à chaque changement mineur. tests unitaires vs. tests de recette ecrire des tests unitaires peut vous permettre d’obtenir une bonne garantie quant à la qualité du code mais ne permet pas toujours de tester votre application au niveau fonctionnel. supposons que vos tests unitaires démontrent que le code pour valider l’existence d’une adresse email dans la db fonctionne, que le code pour envoyer un mail marche et que le contrôleur chargé de tester l’existence d’une adresse puis d’envoyer un mail à cette adresse est valide. supposons que le développeur en charge de coder tout ceci n’ait pas bien compris qu’il fallait en fait ne jamais envoyer de mail à une adresse sauvée dans la bd (par exemple parce que l’instruction lui a été donné par téléphone par un galois avec un très fort accent, qu’il y avait de la friture sur la ligne et qu’il avait enterré sa vie de garçon la veille). du point de vue du développeur, tous les tests unitaires passent. il a rempli sa partie du contrat. du point de vue du client, l’application ne répond pas au besoin fonctionnel qui était de proscrire l’envoie d’email aux adresses sauvées (peut être parce que leurs propriétaires ont exprimé le souhait de ne plus recevoir d’email de la part de ce client). pour pallier ce type de scénario, il faut avoir recourt aux tests de recette (acceptance test en anglais). le tableau suivant, tiré du site http://www.design-up.com/ avec l’aimable autorisation de régis medina, précise les différences entre ces deux types de tests :   tests unitaires tests de recette ecrit par… les développeurs le client ou son représentant portent sur… des méthodes unitaires l’ensemble de l’application approche… boîte blanche boîte noire concerne le client… non oui les tests de recette sont clairement plus intéressants dans l'absolu, mais ils sont généralement assez coûteux à mettre en oeuvre. a l'inverse, les tests unitaires sont beaucoup plus légers : ·        la mise en place du contexte de test unitaire et la vérification du résultat sont le plus souvent très simples, et le coût d'écriture des tests est très réduit. ·        les rédacteurs des tests unitaires connaissent la structure interne du module testé (approche boîte blanche). les rédacteurs des tests de recette ne connaissent pas la structure interne du module testé (approche boîte noire). ·        les tests unitaires peuvent être lancés à chaque compilation de la classe (les tests de recette ne peuvent être lancés que lorsque toute l'application est compilée). ·        les tests unitaires peuvent être exécutés très tôt dans le développement d'une tâche (les tests de recette ne peuvent être lancés que lorsque la fonctionnalité est complètement implémentée). les tests unitaires apportent donc un feedback beaucoup plus rapide que les tests de recette, pour un coût nettement plus réduit. notez qu’il est dans l’intérêt de l’équipe de développement de ne pas écrire les tests de recette. on ne pourra pas se retourner contre eux dans le cas d’un malentendu au niveau fonctionnel, tel que celui cité ci-dessus. en revanche, il incombe à l’équipe de développement de tout mettre en œuvre pour simplifier la rédaction des tests de recette, par exemple, en fournissant un petit langage de script (de préférence xml) spécifique à l’application. pour la conception d’un tel langage de script, nous vous conseillons d’avoir recours à la réflexion.  <markemailasforbidden assert="testpass">   <address>patrick@smacchia.com</address> </markemailasforbidden >   <sendmail assert="testfail">   <to>patrick@smacchia.com</to>   <subject>viagra/dhea</subject>   <body>blabla</body> </sendmail> adopter les principes tdd sur un projet existant les articles dithyrambique concernant le principe du tdd, omettent souvent d’aborder l’argument peut être le plus défavorable : comment mettre en place des tests unitaires sur un projet existant ? (retrofitting unit tests en anglais). il est illusoire de penser que l’évolution du produit peut être gelée pendant le temps nécessaire pour développer les tests sur un code qui est déjà validé par la production. il est à peine plus réaliste de penser qu’une directive telle que : dorénavant nous souhaitons que toutes les nouvelles classes développées ainsi que les classes modifiées soient accompagnées de tests unitaires, soit efficace si les moyens nécessaires ne sont pas débloquer. en l’occurrence, il faut accepter que le développement aura momentanément plus d’inertie. en outre, les bénéfices du tdd ne sont pas immédiats puisqu’il concerne le design, la détection des régressions et la documentation. la mesure de l’avancement du projet est à considérer à très long terme puisque les tests concerneront en grande partie du code existant et déjà validé. l’implémentation des tests unitaires implique souvent un design adapté. même si du temps est mis à notre disposition, il reste difficile psychologiquement de refactorer du code validé sur lequel on a tant souffert. une bonne pratique pour ne pas bousculer trop rapidement trop de code, consiste à commencer par écrire des tests embrassants des grosses parties de votre application, un peu comme des tests de recette. en effet, à un haut niveau le code est en général suffisamment bien agencé pour être facilement testé. si vous avez besoin de soutient dans votre décision d’adopter des tests unitaires dans votre application nous vous conseil ce lien. personnellement, je trouve l’argument suivant convaincant: if you do not start adding unit test today then one year from now you will still not have a good unit test suite. don wells enfin, nous mentionnons cet article qui énumère des cas problématiques à tester lorsqu’ils sont déjà implémentés (casser une méthode trop longue, séparer la construction d’un objet de son utilisation, tester une classe singleton…). les coûts des tests unitaires il ne faut pas se voiler la face comme certains aficionados de l’xp aiment à le faire : les bénéfices des tests unitaires ne sont pas gratuits. ils sont même plutôt très coûteux. en pratique, si vous appliquez rigoureusement les principes du tdd, il se peut que le volume de code des tests unitaires dépasse le volume de code à tester. ce fait se vérifie sur les exemples de cet article. de plus le code des tests unitaires doit évoluer avec l’application et doit donc être maintenu. de même qu’on ne voit pas un enfant grandir lorsqu’on le voit régulièrement, on a tendance à accepter une durée d’exécution des tests unitaires prohibitive parce qu’elle s’est accrue progressivement. ceci constitue bien un coût et voici les solutions possibles: ·        isoler les tests unitaires identifiés comme gourmand en temps (accès db, concurrence…) et les exécuter moins souvent qu’à chaque compilation, par exemple avant chaque archivage du code source dans la base de code commune (cette option pourrait être élégamment supportée par un plug-in vs.net/vss, encore une fois, avis aux amateurs). ·        n’exécuter que les tests unitaires concernant l’assemblage qui a été modifié. il vaut donc mieux ne pas avoir de trop gros assemblages. le principe du tdd a tendance à agir sur le moral des développeurs pour plusieurs raisons: ·        ils peuvent ressentir le développement et la maintenance des tests comme autant de temps non consacré à l’application elle même. ·        en outre, les tests unitaires engendrent de la frustration puisqu’ils sont par définition un moule auquel doit se conformer en permanence le code. en supposant que les tests soient imposés au développeur, ils limitent de facto sa marge de manoeuvre. ·        enfin, il faut bien admettre que lors de la rédaction d’un test unitaire, on a tendance a être convaincu que le code testé ne boguera jamais. mais qui donc irait modifier l’expression régulière qui valide la syntaxe d’une adresse mail ? pour cette exemple précis, il existe une réponse pertinente et pas forcément évidente : l’expression régulière qui valide la syntaxe d’une adresse mail va sûrement être modifiée lorsque les clients voudront pendre en comptent des adresses telles que celles ci : (plus de détails disponibles ici). "address, test" <test@towerdata.com> test((my) email address)@towerdata(call us!).com "test address"@towerdata.com test@[123.32.0.48] ces problèmes strictement humains ne peuvent être réglés que par un bon chef d’équipe qui, en plus d’être intimement convaincu de la démarche, s’emploiera à souligner régulièrement les pièges qui ont été évité grâce aux tests. conclusion les plus gros problèmes rencontrés dans la pratique du tdd ont spécialement été traité en fin d’article. en effet, bien que les bénéfices du tdd soient conséquents, il n’est pas souhaitable que les lecteurs s’y mettent trop hâtivement. il faut garder un esprit critique et une attitude circonspecte car personne ne peut affirmer que le rapport bénéfice/coût du tdd est favorable pour toutes les applications et toutes les équipes. j’espère cependant que vous allez considérer sérieusement l’éventualité d’implémenter des tests unitaires sur vos propres projets en cours et à venir, et que les différents conseils et pratiques exposés dans cet article vont vous y aider. ps: veuillez ne pas utiliser le matériel contenu dans cette article pour devenir riche en développant un logiciel de spam, merci. patrick smacchia patrick smacchia assure de nombreuses formations sur .net, à la fois dans l’industrie et dans le milieu universitaire (université de nice). passionné par l’architecture logicielle, il aide les entreprises à concevoir et à développer leurs applications. ingénieur diplômé de l’enseeiht, il a notamment collaboré avec amadeus et avec les divisions espace et téléphonie mobile d’alcatel. son site expose plus en détail ses activités. ses compétences ont été reconnues par microsoft france, ce qui lui a valu la distinction mvp .net (most valuable professional sur les technologies .net).   l’ouvrage pratique de .net et c# (o’reilly 2003) l’ ouvrage pratique de .net et c# (o’reilly 2003) couvre la plupart des aspects du développement sous .net avec le langage c# (architecture .net sous jacente; langage c#, bibliothèques ado.net, xml, winform, gdi+, architectures distribuées avec com+, .net remoting et asp.net etc.). cet ouvrage contient de nombreux rappels pour le rendre accessible aux étudiants et aux débutants. les développeurs confirmés pourront quant à eux rapidement exploiter les subtiles possibilités proposées par .net, que sont par exemple la réflexion, la programmation orientée aspect ou le mécanisme d’attribut.   les outils: http://www.nunit.org/ http://www.relevancellc.com/vsnunit.htm http://nunitasp.sourceforge.net http://ncover.sourceforge.net/ http://sourceforge.net/projects/nester/ http://sourceforge.net/projects/jester/ http://www.gotdotnet.com/community/workspaces/workspace.aspx?id=3122ee1a-46e7-48a5-857e-aad6739ef6b9 (ncover) http://www.gotdotnet.com/community/workspaces/workspace.aspx?id=03791d39-b33a-4021-81fb-db5b28cf984f (ncoverviewer)   références: http://www.c2.com/cgi/wiki?unittestinglegacycode http://www.codeproject.com/csharp/autp1.asp http://dotnetjunkies.com/weblog/darrell.norton/articles/3374.aspx http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnaspp/html/aspnet-testwithnunit.asp http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp03202003.asp http://dotnetjunkies.com/weblog/darrell.norton/archive/2004/01/22/5950.aspx#5951 http://www.dotnetguru.org/articles/outils/tests/nunit/nunit.htm http://www.dotnetguru.org/blogs/sami/index.php?m=200402#31 http://www.peterprovost.org/wiki/ow.asp?test%2ddriven%5fdevelopment%5fin%5f%2enet http://www.codeproject.com/gen/design/nperf.asp http://www.theserverside.net/articles/article.aspx?l=unittesting http://dotnetjunkies.com/weblog/darrell.norton/archive/2004/02/16/7354.aspx http://www.design-up.com/methodes/testsunitaires/index.html http://www.agilealliance.com/articles/articles/freeman-simmons--retrofittingunittests.pdf http://www.agilealliance.com/articles/reviews/ miller1/articles/acceptancetesting.pdf http://www.objectmentor.com/resources/articles/testingthingsthatareha~9740.ppt http://msdnaa.net/resources/display.aspx?resid=2364 http://blogs.geekdojo.net/richard/archive/2003/09/24/180.aspx http://www.dallaway.com/acad/dbunit.html http://www.ftponline.com/javapro/2003_12/online/rnettleton_12_10_03/default_pf.aspx http://www.cs.wpi.edu/~gpollice/cs562-s03/resources/xp_test_driven_development_guidelines.htm http://www.design-up.com/data/principesoo.pdf http://www.franklins.net/dotnet/mailchecker.zip http://www.dotnetforums.net/t49640.html http://www.eppend.com/verify/mxvalid_features.htm   n'hésitez pas à laisser vos commentaires. article rédigé par patrick smacchia  mars 2004 ( page 1/1 | nc caractères | commentaires  | imprimer le pdf) copyright © dotnetguru - tout droits réservés  

Acceuil

suivante

les tests unitaires en pratique, par patrick smacchia  certification bureautique Microsoft Office Specialist, liste ...  CODE DE LA ROUTE LEADER - DES TESTS DE CODE - LE N°1 DU CODE EN ...  Rake test:units lance les tests sur la BDD de développement ...  France 2 -> TESTS - Testez-vous  Retraite et pénibilité : vers des tests ADN ? - POLITIQUE SOCIALE ...  Tests d'imprimabilité, travaux pratiques, tutoriel, EFPG  TTests-e-Performance : Tests de charge et de performance ...  Art de vivre : Tests interactifs  Formule 1 - Schumacher effectuera des tests à Barcelone pour ...  Eclairage sur le RGAA: la logique des tests unitaires mai 2007 ...  Les tests GED  iPod Nano Blog.fr: News, info, tests, astuces, reviews ...  Mode, stars, amours, santé, psycho... : testez-vous sur Marie Claire  Tests ADN, François Bayrou : "Que le Conseil constitutionnel dise ...  Tests de QI et psycho-tests - Home  AOL Jeux - Tests de jeux vidéos  Mondes Persistants, l'actu des MMOG et MMORPG » Tests  Test-Recrutement.com : Tests de recrutement, test recrutement ...  Introduction aux tests unitaires avec PHPUnit 3.1 - Club d ...  ESSAI-CLINIQUE.COM tests cliniques et essais cliniques rémunérés.  N°25 Avis sur l'application des tests génétiques aux études ...  Tests ADN : Ils ont voté contre : la vraie couleur du cameleon  Capture d’écran, Présentations, Tests d’utilisabilité - TechSmith ...  jQuery 1.1.4 : plus rapide, plus de tests, prêt pour la version ...  Anti-patrons de tests unitaires - Club d'entraide des développeurs ...  Test de français langue étrangère et seconde  Tests et diagnostics du réseau  Les bêta-tests de MMOG et MMORPG  Logiciels pedagogiques, tests en ligne, sources de programmation ...  Bienvenue sur Esopole.com  Tests de QI gratuits, bibliographie sur les tests, jeux gratuits ...  Journal des Femmes Psychologie : Tous nos tests  Concours infirmier infirmiere tests psychotechniques test psy  Test.com Web Based Testing Software  Tests logiciels de l'année 2007 - ZATAZ.COM Journal, Actualité ...  ou-bien.com-Tests 2007  Tests  AIDE : Question à propos les tests psychotechniques d'embauche ...  MySQL AB :: MySQL 5.0 Reference Manual :: 7.1.4 La suite de tests ...  JOUEZ! - Tests  Tests astro sur www.horoscope.fr : amour, feminin, personnalite ...  Tests de bilan de compétences  Tests de bilan de compétences  Les tests du processeur Intel Core 2 Extreme QX9650 (Penryn)  100% mobile : 1er site de modes d'emploi et de tests pour télà ...  Tests  Législation: Les tests ADN permis par le droit européen - L'Express  Projet Roddier  Tous les tests de jeux Xbox et Xbox 360  Tests Pc et consoles  Tests de français avec fichiers audios  Tests high-tech  Tests unitaires et backtrace - Club d'entraide des développeurs ...  Tests et Jeux - Tickle Tests de Personnalité  Tests - PC-WELT  tests non paramétriques sous Excel  CulinoTests - Les CulinoTests : présentation  Moto-Net - Essais et tests de motos et scooters  Magazinevideo.com : Articles en ligne : tests  Des vidéos, des tests et toute l'actualité Wii des sites ...