IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Intégration Continue avec Visual Studio et Team Foundation Server - Partie I, Présentation de MsTest et de Static Code Analysis


précédentsommairesuivant

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.

Liste des différentes méthodes pouvant être exécutées avant ou après un test
Sélectionnez
[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.
Liste des principales méthodes d'assertion

Classe

Quelques Méthodes

Commentaire

Assert

AreEqual
AreNotEqual

Compare l'égalité de 2 valeurs
Existe en version générique et non générique
Possibilité de passer un « delta » : différence maximale autorisée

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
IsFalse

Teste une condition ou une valeur booléenne

IsNull
IsNotNull
IsInstanceOf
IsNotInstanceOf

Teste un objet selon sa nullité ou son type

StringAssert

Contains
StartsWith
EndsWith

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
AreNotEqual
AreEquivalent
AreNotEquivalent

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é.
La notion d'équivalence vérifie uniquement que tous les éléments sont présents (ordre non important).

AllItemsAreUnique
AllItemsAreNotNull
AllItemsAreInstancesOfType

Teste les différents éléments de la collection fournie

Contains
DoesNotContain

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.

Utilisation de l'attribut ExpectedException
Sélectionnez
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é.

Utilisation de l'attribut Timeout
Sélectionnez
[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) :

Solution

On peut déclarer la classe interne suivante :

Définition d'une classe internal
Sélectionnez
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 :

Déclaration de l'attribut InternalsVisibleToAttribute
Sélectionnez
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.

Solution avec Strong Name

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 :

Syntaxe de sn.exe
Sélectionnez
Sn -Tp « Dll Path »

Dans mon cas, l'exécution de la ligne de commande donne le résultat suivant :

Utilisation de l'outil sn.exe pour extraire la clé publique
Sélectionnez
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.

Spécification d'une clé publique avec « InternalsVisibleToAttribute »
Sélectionnez
[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.

Ajout d'une classe avec membres privés


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.
Un accesseur a été généré
IV-C-2-c. Utilisation d'un accesseur

Si nous avons écrit le code suivant :

Classes possédant des membres privés
Sélectionnez
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.

Utiliser un accesseur pour appeler des méthodes privées
Sélectionnez
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 :

Utiliser un accesseur pour appeler un constructeur privé
Sélectionnez
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.
Contenu du fichier MyConsoleApplication.accessor
Sélectionnez
MyConsoleApplication.exe
Desktop
Définition de l'ItemGroup dans le fichier de projet
Sélectionnez
<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.


précédentsommairesuivant
On dépasse dans ce cas le contexte d'un test « unitaire »
Dans le cas de l'utilisation d'Assert.Inconclusive, c'est une exception de type AssertInconclusiveException qui est lancée. Cette classe et AssertFailedException héritent toutes les deux de UnitTestAssertException.
Notez que l'outil n'a pas été mis à jour avec Visual Studio 2008. C'est pourquoi vous le trouverez toujours dans le répertoire d'installation « Microsoft Visual Studio 8 » et non dans le répertoire « %ProgramFiles% \ Microsoft Visual Studio 9.0 \ SDK \ v3.5 \ Bin »

Copyright © 2009 Pierre-Emmanuel Dautreppe . Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.