IV. Tests unitaires : Écrire des tests avec MsTest▲
Cette partie n'abordera que certaines des possibilités offertes par les tests unitaires. Entre autres, le « Data-Driven Testing » ne sera absolument pas abordé. Vous pourrez trouver plus de détails sur ce sujet dans la MSDN (voir la bibliographie « Data-Driven Testing »).
IV-A. Structure d'une classe de test▲
MsTest ainsi que les dernières versions de NUnit se reposent sur des attributs spécifiques pour définir les différentes méthodes de test à exécuter.
Il est également possible de définir des méthodes de pré et postprocessing des tests. Vous trouverez ci-dessous un exemple de classe définissant toutes les méthodes possibles, ainsi que l'ordre dans lequel celles-ci seront exécutées.
[TestClass]
public
class
MyClassTest
{
[AssemblyInitialize]
public
static
void
MyAssemblyInitialize
(
TestContext context)
{
//1. Sera lancé une et une seule fois avant que le moindre test
// de l'assembly ne soit exécuté
}
[ClassInitialize]
public
static
void
MyClassInitialize
(
TestContext testContext)
{
//2. Sera lancé une et une seule fois avant que le moindre test
// de la classe de test ne soit exécutée
}
[TestInitialize]
public
void
MyTestInitialize
(
)
{
//3. Sera lancé avant chacun des tests de la classe de test
// Cette méthode est donc commune à tous les tests de la classe
}
[TestMethod]
public
void
MyTest
(
)
{
//4. Test à exécuter - Ne sera lancé qu'une seule fois
}
[TestCleanup]
public
void
MyTestCleanup
(
)
{
//5. Sera lancé après chacun des tests de la classe de test
// Cette méthode est donc commune à tous les tests de la classe
}
[ClassCleanup]
public
static
void
MyClassCleanup
(
)
{
//6. Sera lancé une et une seule fois après que tous les tests de
// la classe de test auront été exécutés
}
[AssemblyCleanup]
public
static
void
MyAssemblyCleanup
(
)
{
//7. Sera lancé une et une seule fois après que tous les tests de
// l'assembly auront été exécutés
}
}
Notez que seuls les attributs TestClass et TestMethod sont obligatoires. Les autres fonctions et attributs décrits ci-dessus permettent de contrôler l'initialisation et le nettoyage qui peuvent être nécessaires pour chacun des tests, mais sont tout à fait optionnels.
IV-B. Vérifier la justesse d'un test▲
Un test doit vérifier le bon fonctionnement d'une méthode. Pour cela, il peut vérifier que :
- la valeur du paramètre de retour est correcte :
- la valeur des paramètres de type out ou ref est correcte :
- l'exception attendue a bien été levée :
- la réponse a bien été obtenue en moins de X secondes :
- la fonction a bien effectué le travail souhaité (en base de données, dans un fichier…).
Notez que pour être certain du résultat d'une méthode, on doit être en environnement contrôlé. Si par exemple une méthode va dépendre d'une valeur en bases de données (11) , cette valeur doit être connue et donc être insérée par le test lui-même.
Autrement dit, on a trois éléments différents de validation étant, du plus courant au moins courant :
- cohérence entre valeurs attendues et valeurs courantes (que ces valeurs courantes aient été retournées par la méthode ou aient été lues dans la base de données, dans un fichier…) ;
- présence d'une exception ;
- rapidité d'exécution (performance).
IV-B-1. Vérifier la cohérence entre valeurs attendues et valeurs courantes▲
Pour vérifier la cohérence des valeurs, nous avons à notre disposition des classes d'assertion : il s'agit des classes Assert, StringAssert et CollectionAssert.
On ne détaillera pas ici toutes leurs méthodes, mais elles ont toutes des caractéristiques communes :
- ce sont toutes des méthodes statiques (ces classes sont d'ailleurs statiques) ;
-
elles ont toutes au moins trois surcharges :
- la première prenant un message (message qui sera affiché si l'assertion - le test – échoue),
- la seconde prenant un message (avec formatage) et un « params de string » comprenant les paramètres. Il n'est donc pas nécessaire d'utiliser un string.Format avec les méthodes d'assertion,
- une autre surcharge existe sans aucun message d'erreur,
- les autres paramètres (et donc surcharges éventuelles associées) dépendent de chacune des méthodes ;
- elles ne retournent aucun paramètre ;
- en cas d'échec, elles font stopper immédiatement le test. Il faut savoir qu'en interne quand une assertion échoue, c'est une exception de type AssertFailedException qui est levée. (12) Il est donc possible (mais non souhaitable) d'encapsuler un test avec un bloc « try / catch » afin de cumuler plusieurs erreurs. Attention dans ce cas si aucun Assert.Fail n'est effectué, le test sera considéré comme réussi.
Classe |
Quelques Méthodes |
Commentaire |
---|---|---|
Assert |
AreEqual |
Compare l'égalité de 2 valeurs |
Fail |
Force l'échec d'un test |
|
Inconclusive |
Statut intermédiaire entre l'échec et le succès : lorsque le test a été exécuté, on ne pouvait conclure quant à sa réussite ou non |
|
IsTrue |
Teste une condition ou une valeur booléenne |
|
IsNull |
Teste un objet selon sa nullité ou son type |
|
StringAssert |
Contains |
Teste la présence d'une chaîne de caractère dans une chaîne donnée |
Matches |
Teste si la chaîne donnée correspond à un pattern d'une « regular expression ». |
|
CollectionAssert |
AreEqual |
Compare les deux collections fournies. Notez que la notion d'égalité traduit le fait que tous les éléments sont bien égaux (égalité de valeur et non de référence) et ce dans le même ordre et quantité. |
AllItemsAreUnique |
Teste les différents éléments de la collection fournie |
|
Contains |
Teste la présence / l'absence d'un élément particulier dans la collection fournie |
IV-B-2. Vérifier qu'une exception a bien été lancée▲
Il peut être important de vérifier qu'un cas d'exception a bien été traité, et donc de vérifier qu'un process lance une exception. On peut pour cela utiliser l'attribut ExpectedException.
public
class
ClasseATester
{
public
static
double
Divide
(
int
numerateur,
int
denominateur)
{
return
numerateur /
denominateur;
}
}
[TestMethod]
[ExpectedException(
typeof
(DivideByZeroException))]
public
void
TestDivide_DivideByZero
(
)
{
ClasseATester.
Divide
(
2
,
0
);
}
IV-B-3. Vérifier qu'une méthode s'exécute dans un temps raisonnable▲
Il peut être important de vérifier qu'un test s'est bien déroulé dans un laps de temps donné, principalement pour des tests de performance ou lors de refactoring ciblant l'amélioration des performances. Pour cela, l'attribut Timeout peut être utilisé.
[TestMethod]
[Timeout(
2000
)]
//Ce test échouera s'il se déroule en plus de 2000 millisecondes
public
void
TestSleep
(
)
{
Thread.
Sleep
(
3000
);
}
Notez cependant qu'une marge doit être prise. Dans l'exemple précédent, même un Thread.Sleep(1500) aurait pu faire échouer le test, si celui-ci était le seul lancé. En effet, le timeout prend en compte le temps d'exécution complet du test soit :
- l'initialisation du moteur de test ;
- le déploiement des différents fichiers nécessaires au test ;
- l'initialisation du test lui-même (AssemblyInitialize, ClassInitialize…).
IV-C. Tester l'API non publique▲
Une des grandes questions lors de l'écriture de tests unitaires est « que doit-on tester ? ». La réponse la plus logique est que l'on doit tester ce que le client (le client de notre code) va utiliser, c'est-à-dire l'API publique.
Cependant, un des principes de l'orienté objet est de limiter le plus possible l'interface publique qui ne devrait se réduire qu'au strict nécessaire, le reste étant composé de code internal, protected et private.
On va donc également tester ce code à la visibilité plus réduite, même si ce code n'est a priori pas visible dans les classes de tests.
IV-C-1. Tester les membres internal▲
IV-C-1-a. Présentation et utilisation▲
Imaginons que l'on ait une classe internal dans notre projet et que l'on veuille la tester. On peut alors utiliser l'attribut InternalsVisibleToAttribute.
Cet attribut se spécifie au niveau de l'assembly contenant le code à tester (typiquement dans le fichier AssemblyInfo) et permet à une autre assembly (celle contenant le code de test) de voir tous les membres internal comme s’ils étaient publics.
En supposant la solution suivante (Test.MyConsoleApplication référençant MyConsoleApplication) :
On peut déclarer la classe interne suivante :
using
System.
Runtime.
CompilerServices;
[assembly: InternalsVisibleTo(
"Test.MyConsoleApplication"
)]
namespace
MyConsoleApplication
{
internal
class
MaClasseInterne
{
internal
void
MaMethodeInterne
(
)
{
}
}
}
Et on peut alors la tester de la façon suivante, comme si la classe était publique :
using
Microsoft.
VisualStudio.
TestTools.
UnitTesting;
using
MyConsoleApplication;
namespace
Test.
MyConsoleApplication
{
[TestClass]
public
class
MaClasseInterneTest
{
[TestMethod]
public
void
MaMethodeTest
(
)
{
MaClasseInterne maClasse =
new
MaClasseInterne
(
);
maClasse.
MaMethodeInterne
(
);
//Autre code de test omis
}
}
}
IV-C-1-b. Utiliser l'attribut avec une DLL fortement signée▲
Si jamais on a une DLL signée (i.e. avec un strong name), nous allons également devoir signer notre DLL de test, puis spécifier sa clé publique lors de la déclaration de l'attribut.
Nous allons tout d'abord utiliser l'outil « sn » pour extraire la clé publique de notre DLL / exe. Cet outil est installé avec Visual Studio et peut être trouvé dans le répertoire « %ProgramFiles% \ Microsoft Visual Studio 8 \ SDK \ v2.0 \ Bin ». (13)
La syntaxe est la suivante :
Sn -Tp « Dll Path »
Dans mon cas, l'exécution de la ligne de commande donne le résultat suivant :
C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin>sn -Tp
"c:\Projects\MyTestSolution\Test.MyConsoleApplication\bin\Debug\
Test.MyConsoleApplication.dll"
Microsoft (R) .NET Framework Strong Name Utility Version 2.0.50727.42
Copyright (c) Microsoft Corporation. All rights reserved.
Public key is
00240000048000009400000006020000002400005253413100040000010001007b9e3798f117b7
8553f96e3c9bfae4fe533e1ad47dea71ea486b9f70a473570466d006cadce035730a1c96470e73
fad75c61cb90492825ba903c7b5aceafd2df8e57ab202b03687f7277e045c2d9b8085a446bbba6
c9c807617bf88052aab98c31fb6674afee59f9df5ce01cbe77c079c6eacc53287e6c26fd6b809b
df9ffce4
Public key token is ccc3404edeae0195
On pourra alors mettre à jour l'attribut pour introduire la clé publique.
[assembly: InternalsVisibleTo(
"Test.MyConsoleApplication,
PublicKey=
0024000004
......
809bdf9ffce4")]
IV-C-2. Tester les membres private et ou protected▲
IV-C-2-a. Présentation▲
De même, et pour les mêmes raisons, il peut être très intéressant de tester des méthodes non private ou protected.
Dès la version 2005 de Visual Studio, Microsoft a mis à notre disposition les « accesseurs » : il s'agit en fait de classes générées permettant d'atteindre (via réflexion) le code non public.
Cette fonctionnalité a fortement évolué depuis 2005 :
VS 2005 |
Possibilité de générer un accesseur sur un type en particulier: un fichier « .cs » (ou « .vb ») est alors généré dans le projet de test. Il s'agit d'un fichier lisible, sans aucune vérification à la compilation. En effet dans la mesure où l'on se base sur de la réflexion, on ne manipule que des strings, non liés aux types ou méthodes qu'ils reflètent. Ainsi pour chaque mise à jour du code original, il faut régénérer les accesseurs.Il y a de plus de nombreux cas pour lesquels il est impossible de générer des accesseurs, principalement dès que l'on joue un peu avec les génériques. |
VS 2008 |
Introduction de la notion de « Shadow ». Visual Studio génère maintenant un fichier d'extension « .accessor ». Ce simple fichier texte permettra de générer à la compilation des accesseurs pour tous les types de la DLL que l'on shadow.Une nouvelle DLL, appelée NomDllOrignal_Accessor.dll est alors généré à chaque compilation. Il n'y a plus aucun risque d'avoir des accesseurs qui ne sont plus à jour par rapport au code original. La majeure partie des problèmes liés aux génériques est corrigée. |
VS 2008 SP1 |
Seuls quelques problèmes persistent lors de l'utilisation d'interfaces génériques, lorsque le type possède des contraintes vers des types génériques. |
IV-C-2-b. Création d'un accesseur via Visual Studio▲
Imaginons maintenant que je rajoute un fichier dans mon projet avec la définition de certaines classes ou membres privates.
Pour générer un accesseur, il suffit :
- d'ouvrir un des fichiers du projet pour lequel on veut générer un accesseur ;
- clic droit sur le code et choisir « Create Private Accessor », puis de choisir le nom de la DLL de test vers laquelle on veut générer l'accesseur ;
- nous avons maintenant un accesseur dans notre projet de test.
IV-C-2-c. Utilisation d'un accesseur▲
Si nous avons écrit le code suivant :
namespace
MyConsoleApplication
{
public
class
MaClassePossedantDesMembresPrivate
{
private
void
MaMethodePrivee
(
) {
}
private
static
void
MaMethodeStatique
(
) {
}
}
public
class
ClasseAvecConstructeurPrivate
{
private
ClasseAvecConstructeurPrivate
(
int
i) {
}
}
}
Nous pouvons commencer par écrire un test pour appeler les membres de la classe MaClassePossedantDesMembresPrivate.
using
Microsoft.
VisualStudio.
TestTools.
UnitTesting;
using
MyConsoleApplication;
namespace
Test.
MyConsoleApplication
{
[TestClass]
public
class
MaClassePossedantDesMembresPrivateTest
{
[TestMethod]
public
void
MonTest
(
)
{
MaClassePossedantDesMembresPrivate maClasse
=
new
MaClassePossedantDesMembresPrivate
(
);
//1. Créer un accesseur pour appeler un membre d'instance
MaClassePossedantDesMembresPrivate_Accessor accessor
=
MaClassePossedantDesMembresPrivate_Accessor.
AttachShadow
(
maClasse);
accessor.
MaMethodePrivee
(
);
//2. Utiliser l'accesseur pour appeler un membre static
MaClassePossedantDesMembresPrivate_Accessor.
MaMethodeStatique
(
);
}
}
}
Ici on veut tester une classe instanciable. On peut donc créer une instance en utilisant le constructeur standard, et on utilise alors la méthode AttachShadow pour créer un accesseur qui nous permet d'accéder à l'instance qu'on lui fournit.
Toutes les méthodes non publiques sont alors disponibles sur cette instance d'accesseur.
En ce qui concerne les membres statiques, ils sont simplement disponibles sur le type de l'accesseur même.
Notez que la DLL de l'accesseur suit fidèlement la structure de la DLL originale, et que l'on retrouve exactement les mêmes namespace.
Si l'on veut tester un type en utilisant son constructeur privé, nous pouvons alors simplement utiliser le constructeur de l'accesseur comme suit :
using
Microsoft.
VisualStudio.
TestTools.
UnitTesting;
using
MyConsoleApplication;
namespace
Test.
MyConsoleApplication
{
[TestClass]
public
class
ClasseAvecConstructeurPrivateTest
{
[TestMethod]
public
void
MonTest_CtorPrivee
(
)
{
//On utilise le constructeur de l'accesseur pour utiliser un constructeur privé
ClasseAvecConstructeurPrivate_Accessor accessor
=
new
ClasseAvecConstructeurPrivate_Accessor
(
100
);
//Utilisation de la propriété Target pour accéder à l'instance
ClasseAvecConstructeurPrivate instance
=
accessor.
Target as
ClasseAvecConstructeurPrivate;
}
}
}
IV-C-2-d. Création d'un accesseur dans le fichier de projet▲
Comme nous l'avons vu, Visual Studio nous permet de créer facilement une DLL d'accesseurs. Cependant, ceci n'est possible que dans une DLL de test.
Supposons le scénario suivant : vous avez plusieurs DLL de tests, qui contiennent du comportement commun que vous voulez centraliser. Vous allez donc créer une « Class Library » qui va contenir ce comportement commun.
Cependant vous allez avoir besoin de travailler avec certaines classes d'accesseurs.
Dans ce cas, Visual Studio ne vous permet pas de créer un accesseur via l'interface graphique, car votre DLL cible est une « Class Library » et non une DLL de test. Il est cependant possible de le faire en travaillant directement dans votre fichier de projet « .csproj » ou « .vbproj ».
- Ajoutez un nouveau répertoire dans votre projet et nommez-le « Test References ».
- Ajoutez un fichier texte dans ce répertoire et nommez le « MonNomDeProject.accessor » où MonNomDeProjet est le nom du projet pour lequel vous voulez créer un accesseur.
-
Éditez ce fichier pour y ajouter deux lignes :
- sur la première ligne : le nom de la DLL/exe pour laquelle vous voulez créer un accesseur, extension comprise (par exemple : MonProjet.dll) ;
- sur la deuxième ligne : « Desktop ».
- Cliquez droit sur votre projet et choisissez « Unload Project ».
- Cliquez droit sur votre projet et choisissez « Edit xxx.csproj ».
- Ajoutez un nouvel ItemGroup avec les informations de l'accesseur (voir ci-dessous). Notez que cet ItemGroup doit se situer au même niveau que les autres éléments ItemGroup.
MyConsoleApplication.exe
Desktop
<ItemGroup>
<Shadow
Include
=
"Test References\MyConsoleApplication.accessor"
/>
</ItemGroup>
Vous pouvez alors recharger votre projet, et vous pourrez alors accéder à votre accesseur comme si vous étiez dans votre DLL de test.
Attention, il va de soi que les accesseurs ne doivent être utilisés que dans un contexte de test. En aucun cas vous ne devriez ajouter un tel accesseur dans une DLL de production.
Si du code de test peut faire abstraction du modèle objet encapsulé tel qu'il a été prévu et encapsulé, le code de production doit scrupuleusement suivre les règles de la programmation orientée objet.