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