Les tests unitaires avec Nunit par Jean-Louis Vidal (jean-louis-vidal@wanadoo.fr)

L'intérêt des tests et d'un Framework dédié aux tests

Comment développez-vous ?

Êtes vous plutôt du style "organisé" avec une démarche itérative formalisée qui place les phases d'analyse et de conception avant le codage, le déboguage et les tests ou plutôt du style "sauvage" sans démarche dogmatique mais avec une propension à plonger sur le code à réaliser en comptant sur les tests pour vérifier le bon fonctionnement du code ?

Et bien … peu importe car nous retrouvons le mot TEST partout.

En quoi consiste cette activité ?

Le logiciel doit être l'expression "software" des besoins exprimés en amont. Ils sont une manière d'assurer la vérification du bon fonctionnement du logiciel en comparaison avec les résultats attendus. Mais où placer les tests ? Et bien sûrement pas au niveau du programme d'utilisation. En effet, dans ce cas chaque programme devrait coder les vérifications des règles qui sont souvent communes. Etant donné qu'en C# (ou en Java d'ailleurs) chaque classe peut posséder un point d'entrée unique, en l'occurrence le Main, on pourrait placer les tests unitaires à ce niveau. Si le regroupement est évident cela implique de polluer chaque classe par ses tests. Ne serait-il pas dans ce cas plus judicieux de sortir les tests des classes et les regrouper dans un espace de nommage (namespace) particulier ? 

Quand les écrire ?

La technique classique : le code d'abord, les tests ensuite. La technique inversée : les tests d'abord et le code ensuite (et oui, n'avons-nous pas l'expression des besoins - voire les Use Cases et scénarios - pour nous guider dans leur écriture) La technique entrelacée... on s'aide de l'avancement du projet pour affiner et compléter les tests.

Quand les exécuter ?

Le mieux est une procédure automatique. Les tests doivent pouvoir être exécutés sans intervention humaine et donner un résultat chiffré (par exemple 97 tests réussis sur 100). Voyons plus en détail le Framework de tests que nous vous proposons aujourd'hui : Nunit.

Présentation du Framework Nunit

Nunit est un Framework open source de Tests Unitaires. Il correspond à la réécriture (ou le portage) du célèbre outil Java : JUnit. Le site de référence de NUnit est : http://www.nunit.org. Nous vous encourageons vivement à télécharger le fichier NUnit-V2.0.msi et à installer le produit. Mais attention ! Il convient de contrôler l'adhérence à une version spécifique du Framework .NET (par exemple, l'utilisateur de VisualStudio 2003 (V1.1), préfèrera peut-être recompiler NUnit à partir des sources (et du projet fourni avec NUnit-V2.0.msi).

Il existe énormément d'autres Frameworks existants que vous pourrez trouver à l'adresse suivante : http://www.sourceforge.net/projects, à laquelle il faudra rajouter le nom du projet, soit : JUnit (Java), NUnit (.NET), csUnit (c#), CppUnit (C++), CUnit (C), DUnit (Delphi), SUnit (Smalltalk), PyUnit (Python), PHPUnit (php), JSUnit( JavaScript), XMLUnit (XML DTD et XPath) et d'autres . . . PBUnit (PowerBuilder), SPUnit (SQL Stored Proc), HTTPUnit, DBUnit, JFCUnit, JUnitEE, TagUnit (Tags JSP) etc.

Attention au changement de Version, des modifications importantes ont été réalisées entre la V1 et la V2.

Style V1 

La classe centrale est nommée TestCase et le développeur de test doit la dériver pour implémenter ses propres tests.

public abstract class TestCase implements Test 
  { 
       
private final String fName; 
       
public TestCase(String name) 

       
fName= name; 
   { //le nom est utile pour savoir quel test échoue   

           
public abstract void run(); //méthode abstraite pour obliger le développeur à fournir une implémentation, il y mettra ses tests
           
           
  }

 Style V2 (plus simple : celui qui est présenté ici)

[TestFicture]
public
class Test {
           
[Test]
           
public void MethodeTest(){. . .}//prototype obligatoire public void <Methode>()
}

Premiers pas 

Sur un exemple simple, chaque règle de gestion peut (doit) être l'occasion d'un test, voire de plusieurs. Prenons le cas de l'objet suivant de type Voiture qui permet la construction, le démarrage/arrêt du moteur, l'accélération/freinage, ... Nous ferons en sorte de rendre la classe métier Voiture imparfaite de sorte à déclencher des erreurs de test.

namespace Circulation {
           
public class Voiture     {
                       
private bool moteurDemarre_ = false;
                       
private int vitesseMax_ = 0;
                       
private int vitesse_ = 0;
                       
public Voiture(int vitesseMax)  {
                                  
vitesseMax_ = vitesseMax;
                       
}
                       
public void DemarrerMoteur()  {
                                  
moteurDemarre_ = true;
                       
}
                       
public void ArreterMoteur() {
                                  
moteurDemarre_ = false;
                       
}
                       
public void Accelerer(int dv) {
                                  
vitesse_ += dv;
                       
}
                       
public void Ralentir(int dv) {
                                  
vitesse_ -= dv;
                       
}
                       
public void FreinageArret() {
                                  
vitesse_ = 0;
                       
}
                       
public int Vitesse
{
                                  
get {
                                    
return vitesse_;

                                  
}
                       
}
           
}

}

Que veut-on tester ? 

Le scénario nominal : Création d'une voiture, démarrage, accélération en deçà de la vitesse max, vérification de la vitesse, freinage jusqu'à l'arrêt puis arrêt du moteur. 

Un scénario " anormal " : Création d'une voiture, <oubli du démarrage>, accélération en deçà de la vitesse max, vérification de la vitesse, freinage jusqu'à l'arrêt puis arrêt du moteur, etc. Une première approche de test consiste donc à coder dans chacune des classes une méthode Main.

public static void Main() {
     
//scénario Nominal  
     
System.Console.Write(
"test d’accélération n°1 : Accélération sous maximale : ");
     
Voiture v1 = new Voiture(160);
     
v1.DemarrerMoteur();
     
v1.Accelerer(60);
     
if (v1.Vitesse != 60)
           
System.Console.WriteLine("ERREUR");
     
//test produit sur mesure 
     
else
           
System.Console.WriteLine("OK");
     
//scénario de l’accélération sans avoir démarré le moteur  
     
System.Console.Write(
           
"test d’accélération n°2 : Accélération moteur non démarré : ");
     
Voiture v2 = new Voiture(160);
     
v2.Accelerer(60);
     
if (v2.Vitesse !=
0)
           
System.Console.WriteLine("ERREUR"); //test produit sur mesure 
     
else
           
System.Console.WriteLine("OK");
}

On peut compiler (en ligne de commande) avec la ligne suivante:

csc /out:Circulation.exe Voiture.cs 

Et l'exécution nous donne : 

C:\VC#JL\TestNUnit>circulation                                                               
test d'accélération n°1 : Accélération sous maximale : OK                
      
test d'accélération n°2 : Accélération moteur non démarré : ERREUR
   

On peut également regrouper les tests dans une classe spécialisée, ici TestCirculation qui pour l'instant ne teste que notre Voiture. 

L'exemple suivant résume la démarche :

namespace Circulation {

            public class TestCirculation 
           
{ //code de test regroupé dans une classe "Test Métier"

                       
public static void Main() 
                       
{
                                  
//scénario Nominal  
                                  
System.Console.Write(
"test d’accélération n°1 : Accélération sous maximale : ");
                                  
Voiture v1 = new Voiture(160);
                                  
v1.DemarrerMoteur();
                                  
v1.Accelerer(60);

                                  
if (v1.Vitesse != 60)
                                              
System.Console.WriteLine("ERREUR");
                                              
//test produit sur mesure  
                                  
else
                                              
System.Console.WriteLine("OK");
                                  
//scénario de l’accélération sans avoir démarré le moteur  

                                  
System.Console.Write(
                                              
"test d’accélération n°2 : Accélération moteur non démarré : ");
                                  
Voiture v2 = new Voiture(160);

                                  
v2.Accelerer(60);
                                  
if (v2.Vitesse !=
0)
                                              
System.Console.WriteLine("ERREUR");
                                              
//test produit sur mesure
                                  
else
                                              
System.Console.WriteLine("OK");
                       
}
           
}
}

Dans ce cas, on compilera la classe Voiture sous la forme d'une DLL et le Test sous la forme d'un fichier d'extension .EXE. La ligne de commande suivante nous illustre le principe :

csc /target:library /out:Circulation.dll Voiture.cs                                                  
csc /reference:Circulation.dll /out:TestCirculation.exe TestCirculation.cs      

Nous venons donc de vous montrer ce que tout développeur/testeur ferait de manière classique. Le but de cet article étant de vous montrer l'utilisation du Framework de test Nunit, voyons comment cette démarche peut être automatisée. Pour ce faire, il nous faut transformer notre classe de test pour la rendre conforme à nunit :

using namespace NUnit.Framework;
                        
 
namespace Circulation 
                        
  {
                                   
  [TestFixture] 
                                    public
class TestCirculation 
                                   
  {
                                               
  [Test]
                                                
public void AccelerationSousMaximale()
                                                           
  //les méthodes peuvent avoir les noms désirés, pas besoin de les préfixer par Test… utiliser les noms des use cases ou scénarios est direct ! 
                                               
  { //scénario Nominal  
                                                           
  Voiture v1 = new Voiture(160);
                                                           
  v1.DemarrerMoteur();
                                                           
  v1.Accelerer(60);
                                                           
  Assertion.AssertEquals(v1.Vitesse, 60);
                                               
  }
                                               
  [Test]
                                                
public void AccelerationSansMoteur() 
                                               
  {
                                                           
  //scénario de l’accélération sans avoir démarré le moteur  
                                                           
  Voiture v2 = new Voiture(160);
                                                           
  v2.Accelerer(60);
                                                           
  Assertion.AssertEquals(v2.Vitesse, 0);
                                               
  }
                                   
  }
                        
  }
            
  }

A noter qu'une surcharge de Assertion.Equals permet de placer sa propre chaîne de message en premier paramètre, pour un dépouillement des tests plus confortables. La compilation se fait avec (sur une seule ligne):

C:\VC#JL\TestNUnit>csc /out:TestCirculation.dll /target:library /r:"c:\Program Files\NUnit V2.0\bin\nunit.framework.dll",Circulation.dll  TestCirculation.cs

Et le test sous l'environnement NUnit se lance par le menu "Démarrer\Programmes\NUnitV2.0\NUnit-GUI" ou par le raccourci placé sur le bureau Windows, par l'installation automatique.

Le test possède à présent deux cas, dont un seul a réussi. Il a été placé dans une Assembly séparée (mais dans le même namespace) du code métier. Attention à bien prendre en compte le fait que chaque test doit absolument disposer d'une visibilité sur le namespace " métier ", alors que l'inverse est fortement déconseillé... On pourra donc éventuellement créer un namespace particulier réservé aux tests.

Faisons maintenant évoluer notre classe Voiture de façon à ce que les tests passent à 100%.

namespace Circulation 
{

           
public class Voiture 
           
{
                       
private int vitesseMax_ = 0;
                       
private int vitesse_ = 0;
                       
private bool moteurDemarre_ = false;
                       
public Voiture(int vitesseMax) 
                       
{
                                  
vitesseMax_ = vitesseMax;
                       
}
                       
public void DemarrerMoteur() 
                       
{
                                  
moteurDemarre_ = true;
                       
}
                       
public void ArreterMoteur() 
                       
{
                                  
moteurDemarre_ = false;
                       
}
                       
public void Accelerer(int dv) 
                       
{
                                  
if (moteurDemarre_)
                                              
vitesse_ += dv;
                       
}
                       
public void Ralentir(int dv) 
                       
{
                                  
vitesse_ -= dv;
                       
}
                       
public void FreinageArret() 
                       
{
                                  
vitesse_ = 0;
                       
}
                       
public int Vitesse 
                       
{
                                  
get 
                                  
{
                                              
return vitesse_;
                                  
}
                       
}
           
}
}

Utilisons l'option Reload du menu fichier de l'utilitaire NUnit-GUI et relançons ensuite les tests :

Parcourrons quelques unes des possibilités du Framework. Il est possible de vérifier certaines situations à l'aide des méthodes suivantes de la classe Assertion (la liste est obtenue par CTRL+Espace dans Visual Studio) :

La méthode Assertion.Assert est utilisée pour vérifier qu'une condition particulière (booléen) est vérifiée. Pour faire l'inverse nous utilisons donc Assertion.Fail.

Chaque méthode de test devant recourir à l'instanciation d'une Voiture, il est recommandé d'en faire un attribut de la classe mais attention, si nous déclarons et initialisons un simple attribut privé ainsi : 

private Voiture v = new Voiture(160) ; Et que nous l'utilisons dans les diverses méthodes de test, alors les tests vont s'enchaîner sans réinitialisation de l'objet du test. En re-fabriquant le module TestCirculation.dll, et en re-testant le projet, tous les résultats deviennent faux ! Il est donc recommandé de procéder ainsi : Déclarer l'attribut dans la classe et placer dans la méthode d'initialisation un bloc [SetUp], le nom préféré pour cette méthode est Init.

[SetUp] public void Init() {
           
v = new Voiture(160);

}

Que décidons nous de faire lorsque la méthode DemarrerMoteur est invoquée alors que le moteur est déjà démarré ? Cela doit être indiqué dans les spécifications de votre application. Disons (pour l'exemple) qu'une exception doit être lancée par la méthode DemarrerMoteur. Il y a moyen d'en exprimer la règle à l'aide du Framework. Revenons dans le fichier Voiture.cs pour définir notre Exception. Vu le bruit que cela fait de tourner la clef de contact alors que le moteur tourne, appelons la : CrouicException.

public class CrouicException : System.ApplicationException { }

//On rajoute alors le test suivant
[ExpectedException(
typeof(CrouicException))]
[Test]

public
void DoubleDemarrageMoteur() {
    //actionner la clef de contact après avoir déjà démarré le moteur

    v.DemarrerMoteur();
    v.DemarrerMoteur();
    //une exception est attendue ICI
}

Comme l'exception n'est pas encore levée dans la méthode DemarrerMoteur, le cas de test échoue.

Nous revenons donc sur notre méthode pour la corriger :

public void DemarrerMoteur(){
    if
(moteurDemarre_)
        throw
new CrouicException();
    moteurDemarre_ =
true;
}

Il est également envisageable de réaliser d'autres tests : 

On notera que l'équipe chargée des tests n'est habituellement pas celle qui est chargée du développement. La technique de recherche et d'écriture de tests s'adapte parfaitement à ce cas, étant proche des besoins utilisateurs et ayant une connaissance minimale du fonctionnement interne des composants.

Quant aux attributs, ils sont de plusieurs natures :

Voici un exemple d'un test mis temporairement de côté, on ne peut pas l'oublier car la bande verte est transformée en jaune !

Remarque : 

La bonne pratique est de ne placer qu'une cause d'erreur par méthode de test. En effet, le framework sort directement de la méthode après la première erreur détectée pour chaque méthode.

Industrialisation

Maintenant comment faire tourner ces tests en ligne de commande ? Pour ce faire, recherchez le fichier nunit-console.exe ou compiler le projet (VSProj) correspondant dans le répertoire C:\Program Files\NUnit V2.0\bin

set path=%path%;”C:\Program Files\NUnit V2.0\bin”   

Lancez ensuite les tests sur l'assembly choisie:

nunit-console /assembly:TestCirculation.dll                      

Le résultat est bien sûr envoyé à l'écran, mais EN PLUS dans un fichier TestResult.XML que l'on peut spécifier en ligne de commande et envoyé par défaut dans le répertoire de l'assembly testée.

nunit-console /assembly:TestCirculation.dll /XML:testCirculation.xml

Cette technique permet ensuite, par manipulation des fichiers XML, de consolider les différents tests, l'alimentation de bases de données, la source d'information pour les outils de suivi, etc.

Addin pour Visual Studio

Il existe également un Addin pour Visual Studio permettant d'utiliser simplement le framework NUnit à partir de l'environnement de développement ; Téléchargez le à partir du site http://sourceforge.net/projects/nunitaddin/

Pour l'installer, rien de plus simple, après avoir lancé l'exécutable, le programme enregistre un composant dans la base de registre. N'oubliez pas de vérifier son installation sous la clef HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\7.1\Addins\NUnitAddin.Connect\Options\NUnitAddin. Ensuite, lors du prochain démarrage de Visual Studio, vous verrez apparaître une nouvelle entrée sous le menu Tools/Addin Manager. On doit aussi retrouver un nouveau type de projet "Test Suite" dans chacun des langages (C#, VB, C++ et J#) 

Enfin, il est possible de lancer le projet de test d'un simple clic droit (Run Test), comme le montre l'image suivante :

Conclusion

Nous espérons que cet article vous a convaincu de l'intérêt des tests unitaires. Non seulement ils sont nécessaires pour assurer la qualité d'une application mais le framework Nunit vous permet réellement de tirer parti de la puissance de .NET (attributs, …). Par ailleurs, les autres bénéfices apportés par Nunit sont nombreux :

N'hésitez pas à vous référer au site officiel où vous trouverez des modules spécialisés dans les tests sous ASP.NET, sous WebServices, ... bon tests !

Auteur : Jean Louis Vidal

Copyright © Avril 2003


  Qui est Jean-Louis Vidal ?

     Jean-Louis est Consultant et Formateur indépendant depuis 1998 après être passé par plusieurs (et parfois très longues) expériences dans de grosses sociétés. Diplômé de Supélec en 1980 il a très vite été attiré par le développement en architecture distribuée. Passion qu'il a aujourd'hui la chance de pouvoir communiquer à travers les formations qu'il anime (J2EE et .NET, MCT depuis 1998). 

  Ses compétences ...
      Ses langages favoris sont C++, Java et C# et il possède la fameuse double compétence J2EE/.NET lui permettant de faire ressortir leurs ressemblances/différences respectives.

 

Ressources

http://sourceforge.net/projects/nunitasp/

http://sourceforge.net/projects/skeletonunit/