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 ...