Le framework DUnit, les tests sous Delphi

Dunit est un framework de test dérivé de Junit qui est son pendant pour le langage Java. Mais qu'est qu'un framework de test ?

Pour simplifier un framework de test peut être considéré comme une application qui va vérifier votre projet pour s'assurer qu'elle sera exempte de bug (le moins possible, l'erreur étant humaine) et qu'elle le reste (les tests étant sauvegardés).

Ce genre de framework est apparu avec l'extrême programming, une méthode de développement de logiciel basé sur les tests et d'autre point tout aussi innovant. Je n'aborderais pas l'Extreme Programming, si cela vous intéresse demander à votre ami Google (le site de xp-france est un bon point de départ), mais la façon d'utiliser la Dunit notamment pour déceler les fuites de mémoires que provoque votre application.

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Téléchargement du framework

Tout d'abord télécharger la dernière version de Dunit. Si le lien est cassé, essayez de trouver un site miroir sur Google.

II. Installation

II-A. Préambule

Pour l'installation, les choses se gâtent. En effet, le framework consiste en de simple fichier. Ce n'est pas une librairie ou un composant. En terme clair, il faut à chaque fois faire un projet qui testera votre projet en développement. Dans ce projet, il faudra inclure les unités du framework Dunit. Un avantage, c'est pas compliqué à faire, un inconvénient il faut le faire à chaque fois !

Bien sur ces qualités/défauts ne concernent que le point de vue de la mise en place. Le véritable avantage de Dunit, c'est surtout la possibilité de tester de manière très précise votre projet et de pouvoir garder ces tests actifs !

Dans l'archive que vous allez récupérer, il y a un certain nombre de chose. Vous y trouverez une aide en anglais, quelques exemples et un répertoire src. Pour ce tutoriel, il n'y a que le contenu du répertoire src qui m'intéresse mais je ne vous interdis pas de jeter un coup d'œil au reste.

Quelle que soit la façon dont vous allez intégrer le framework Dunit à votre projet test, je vous conseille de décompresser l'archive dans un nouveau répertoire.

Ce répertoire doit être facilement accessible car tous vos projets tests vont devoir y accéder (à la racine du disque dur par exemple ou si comme moi vous avez un dossier dédié au projet delphi voilà une place toute trouvé).

On pourrait bien sûr copier le répertoire src de Dunit dans le répertoire de chaque projet-test, mais cela produira une multitude de copie (une par projet). Je sais que l'espace sur vos disques durs a considérablement augmenté mais c'est dommage de le perdre pour ça.

Dans le répertoire src, il y a un projet et plusieurs fichiers, dans le tableau ci-dessous, je vous indique les fichiers importants.

Fichier Description Utilisé
TestFramework.pas Le framework himself, contient tous les outils pour les tests. Oui
TestExtensions.pas Contient des fonctions qui permettent de répéter les tests en une exécution. Oui
GUITesting.pas Une suite de classe pour tester les formes et dialogBox de l'utilisateur. Non
TextTestRunner.pas Si vous voulez lancer vos tests en mode console. Non
GUITestRunner.pas L'interface graphique pour le projet test. Oui
GUITestRunner.dfm La form associée à l'unité du même nom. Oui

Attention, point important :
Pour que le framework puisse faire son travail, vous retourner les erreurs, il faut activer les assertions. Les assertions sont un peu comme des macros commandes. Elles permettent de tester qu'une condition est vraie, exemple que 1+1 est bien égale à deux.

Pour activer les assertions, aller dans Projets -> Options et l'onglet Compilateur puis cochez la case Assertions, la case défaut et validez.

Version utilisée et bug rencontré :
La version que j'utilise, est la 9.2.0 (mais à l'époque où j'ai rédigé ce cours, c'était la 7.2). Or les développeurs ont choisi de ne pas maintenir le code pour les anciennes versions de delphi. Ils abandonnent la version delphi 4 et 5. Ce qui pose un problème pour deux fonctions. Le bug est le suivant, deux fonctions dans l'unité TestFrameWork posent problème au compilateur. Ces deux fonctions surchargent une fonction avec des paramètres différents mais le compilateur n'arrive pas à voir la différence (je suppose que pour les versions Delphi supérieurs à 5, le problème ne se pose pas). Je vous livre la solution la plus rapide, je mets en commentaire ces deux fonctions (comme ça le code est toujours présent et si vous avez le temps vous pourrez le modifier pour qu'il marche), n'oubliez pas de mettre en commentaire l'appel à ces fonctions. Ce n'est gênant que si vous voulez comparez des binaires ou des variables hexadécimales.

Les deux fonctions à problème sont CkeckEqualsBin et CheckEqualHex, elles surchargent la fonction checkEquals. Je pense que c'est la manière de transmettre les paramètres qui posent problème, la transmission fait appel à une fonction builtin (InToHex) et une fonction déclarée dans TestFrameWork (IntToBin). Le compilateur des versions 4 et 5 ne doit pas réussir à définir le type des variables.

Ce problème a déjà été porté à l'attention des développeurs de Dunit qui indiquent qu'ils ne maintiennent pas le code pour les versions 4 et 5 de Delphi. Cela dit Dunit reste encore compatible, je n'ai trouvé que ce bug.

II-B. Installation pour tout projet

Permet de spécifier le chemin du framework Dunit pour tous les projets test à venir mais également pour n'importe quel projet !

Dans le menu Outil -> Option d'environnement, choisissez l'onglet bibliothèque :

Image non disponible
Fenêtre environnement.

Définissez le chemin de recherche en ajoutant celui du framework Dunit, le répertoire src.

II-C. Installation pour projet unique

Permet de spécifier le chemin du framework Dunit pour le projet test en court. Sauf si vous cochez défaut avant d'enregistrer, mais je vous le déconseille !

Dans le menu Projet -> Option, choisissez l'onglet Répertoire/Condition :

Image non disponible
Fenêtre projet.

Définissez le chemin pour le framework Dunit, le répertoire src.

Pour l'instant, ce chapitre ne vous a pas apporté grand-chose. Mais maintenant que le framework est prêt, je vous propose d'attaquer la pratique avec le chapitre suivant.

III. Utilisation de base de Dunit

Ce chapitre est consacré exclusivement à la prise en main de Dunit. Pour l'instant, je n'ai pas prévu d'utiliser les unités de Dunit qui permettent de tester les forms, boîte de dialogue et autres interface graphiques. Cependant, sachez que ces unités permettent surtout de simuler l'interaction de l'utilisateur avec l'interface de votre projet final. Nous verrons quand même comment tester une unité contenant une form.

Le présent tutoriel est surtout axé sur la détection de fuite de mémoire, la vérification du comportement des fonctions et l'initialisation/affectation des données. Pour l'utilisation de Dunit dans le cadre d'un projet, il vous faudra lire le chapitre suivant.

III-A. Découverte et première prise en main

Dans cette partie, nous allons prendre en main le framework Dunit à travers quelques tests qu'il nous permet de réaliser. Nous poursuivrons par le développement d'une unité supplémentaire qui permettra de détecter les fuites mémoire de vos projets. Ce détecteur de mémoire n'est pas inclus dans le Dunit aussi bizarre que ça puisse paraître. Enfin, le but de ce tutoriel sans prétention, vous aidez à tester vos applis. Allez en route.

Nous allons faire un premier projet-test histoire de nous faire la main. Contrairement à l'utilisation que vous ferez de Dunit, nous n'allons pas tester un autre projet. Pour l'instant, nous allons simplement voir comment mettre en place des tests avant d'attaquer un vrai projet.

Cet exemple nous servira quand même de base à tous nos projet-tests.

Première étape, crée un nouveau projet avec une seule unité sans forme.

  • Nouveau projet,
  • Retirer l'unité par défaut,
  • menu Nouveau, choisir unité (sans forme)

Enregistrer cette nouvelle unité et ce projet, normalement dans le répertoire du projet à tester. Je les nommerais pour ma part en respectant ma convention de nommage ce qui donne : MainTestUnt pour l'unité et TesteurPrj pour le projet.

Dans l'unité :

  • ajouter l'unité TestFrameWork,
  • Déclarer une nouvelle classe, TTestPremierEssai, dérivant de la classe TTestCase,
  • Ajouter une première méthode, procédure de la classe, TestPremier,
  • Dans la partie implémentation de TestPremier, tapez (ou copier/coller cette ligne) : Check(1 + 1 = 2, 'L''ordinateur ne sait plus compter !');

Petite explication :
L'unité TestFrameWork contient toutes les procédures pour tester facilement vos applis, la classe TTestCase est dans cette unité. Chaque classe de Test dérivera de cette classe pour hériter de ses propriétés ou devrait le faire. Nous verrons plus loin que nous dériverons d'une autre classe et pourquoi, promis.

La mise en place de test se fait par l'intermédiaire de méthode qui n'accepte aucun paramètre (c'est un point important). Ces méthodes doivent se situer dans la section published de la classe.

L'étape suivante : préciser que l'on veut exécuter cette méthode. Les concepteurs du Framework appellent cela publier. Pour cela, un petit rappel sur les sections d'une unité Delphi ne me paraît pas inutile.

Vous connaissez tous les sections interface et implémentation, mais il y aussi la section initialization qui est parcouru à chaque démarrage de l'application. Elle permet d'initialiser, d'où le nom, certains paramètres.

C'est dans cette section initialization que nous allons publier nos tests, en ajoutant initialization et la ligne suivante à la fin de l'unité et juste avant le End final :

 
Sélectionnez
 TestFramework.RegisterTest(TTestPremierEssai.Suite);

Comme vous pouvez le voir, on publie une suite. Toutes les méthodes que vous avez déclarées published dans la classe seront publiées (il y a comme une certaine logique dans le choix des noms :) ).

Vous ne pensiez tout de même pas que vous alliez devoir toutes les publier une par une ;) . Rappelez-vous cet adage, un bon programmeur est un programmeur fainéant, moins il en fait mieux il se porte.

Ce qui donne :

 
Sélectionnez
unit MainTestUnt;
interface
uses
 TestFrameWork;

type
 TTestPremierEssai = class(TTestCase)
 published
   procedure TestPremier;
 end;

implementation

procedure TTestPremierEssai.TestPremier;
begin
 Check(1 + 1 = 2, 'L'ordinateur ne sait plus compter !');
end;

initialization
 TestFramework.RegisterTest(TTestPremierEssai.Suite);
end.

Un dernier effort et nous pourrons lancer notre premier test à savoir est-ce que pour l'ordinateur 1+1 est bien égale à 2.

Afficher la source du projet et modifiez-le comme suit :

  • Ajoutez entre Form et votre unité, les unités TestFrameWork et GUITestRunner,
  • Après Application.Initialize, ajoutez la ligne : GUITestRunner.RunRegisteredTests;

L'unité GuiTestRunner contient l'interface pour le projet-test et la ligne GUITestRunner.RunRegisteredTests exécute cette interface avec les tests que vous avez publiés.

Ce qui donne :

 
Sélectionnez
program TesteurPrj;
uses
 Forms,
 TestFrameWork,
 GUITestRunner,
 MainTestUnt in 'MainTestUnt.pas';

{$R *.RES}

begin
 Application.Initialize;
 GUITestRunner.RunRegisteredTests;
end.

Notre projet-test est enfin terminé. Vous pouvez le compiler mais pour l'exécution, je vous conseille de le faire depuis l'exécutable. En effet si vous le faites depuis Delphi, vous reviendrez sous le RAD (Delphi) à chaque erreur. Ce qui peut vite devenir irritant si vous avez de nombreux tests. Cela est du à l'utilisation d'assertion qui fonctionne un peu comme les exceptions.

Le fait que Delphi s'arrête et montre l'endroit de l'erreur (ce qui n'est pas toujours vrai d'ailleurs) est pratique pour le débogage mais ici nous ne déboguons pas notre projet-test mais un autre projet. D'ailleurs, lorsque vous utiliserez D'unit pour tester vos applis, gardez bien à l'esprit la simplicité des tests à mettre en place.

Petit plus :
La fonction RegisterTest peut être écrit d'une autre façon qui permet de spécifier le nom de la suite de test. RegisterTest('Ma suite de Test', TTestPleinDeChose.Suite); par exemple, affichera le libellé 'Ma suite de Test' dans l'interface.

Une fois lancé, vous obtenez ceci :

Image non disponible

et après exécution :

Image non disponible

Comme vous le constatez, 1+1=2. Tout va bien :)

III-B. Quelques tests

Poursuivons en testant des cas d'erreur, histoire de voir de quoi il retourne avec des cas simples. Renommons la classe TTestPremierEssai en TTestArithemetique.

N'oubliez pas la procédure également. Ajoutons deux nouvelles méthodes, TestSecond et TestTrois.

 
Sélectionnez
procedure TTestArithemetique.TestSecond();
begin
   Check(1 + 1 = 3, 'Défaillance délibérée !');
end;

procedure TTestArithemetique.TestTrois();
var
   i : integer;
begin
   i := 0;
   Check(1 div i = i, 'Exception délibérée !');
end;

Voilà, c'est tout car les tests sont déjà publiés. Pour vous amusez, testez l'application directement depuis son exécutable puis depuis le RAD (l'EDI de Delphi). Vous verrez que depuis Delphi, le programme n'est pas marrant :(

Notez au passage les différentes couleurs :
Vert pour Ok,
Magenta pour erreur dans la vérification (i.e. 1+1 n'est pas égale à 3),
Rouge pour une exception, si le fait qu'une égalité ne soit pas vérifié peut être un problème, l'exception dénote d'une erreur plus sérieuse !

Dans la pratique, vous utiliserez ce genre de test pour vérifier les initialisations et les affectations. Si vous développez des unités mathématique (ou toutes fonctions réalisant des opérations), vous pourrez tester que vos nouveaux opérateurs calcul correctement (multiplication de matrice par exemple), avouez que ça serait l'horreur si la multiplication d'une matrice par l'identité ne donnait pas la même matrice :(

Fort de cette première expérience, honnêtement j'espère que vous n'avez pas eu de souci pour suivre, passons à des choses plus intéressantes et au moins aussi amusante.

Ajoutons dans la clause uses l'unité Classes car nous allons manipuler une stringlist. Déclarons ensuite une nouvelle classe dérivant toujours de la classe TTestCase. Je profite de l'occasion pour introduire deux méthodes sympathiques du Framework D'unit, SetUp et TearDown. C'est deux méthodes se déclarent dans la classe dans la partie protected et en override (car elles existent dans la classe TTestCase). Elles sont appelées avant chaque procédure test pour SetUp et après pour TearDown. Ce qui permet d'initialiser et de nettoyer avant et après chaque test.

Pour la déclaration de la classe :

 
Sélectionnez
TTestStringList = class(TTestCase) // Test stringlist
   private
      _Fsl : TStringList;
   protected
      procedure SetUp; override;  // exécuter avant chaque test
      procedure TearDown; override; // exécuter après chaque test
   published
      procedure TestStringListHabiter();
      procedure TestStringListTrier();
   end;

Par convention, je nomme mes variables privates en commençant par le signe souligné. Les deux tests vont nous permettre de vérifier qu'une liste est vide et qu'elle est triée.

Pour l'implémentation :

 
Sélectionnez
procedure TTestStringList.TestStringListHabiter();
var
   i : integer;
begin
   // Vérifie le nombre d'élément dans la liste
   Check(_Fsl.Count = 0,'Déjà pollué !');
   for i:=1 to 50 do
      _Fsl.Add('i'); // met 50 i.
    // Vérifie si la liste est remplit avec le bon nombre d'élément
   Check(_Fsl.Count = 50, 'Pas le bon nombre d''élément');
end;

procedure TTestStringList.TestStringListTrier();
begin
    // Vérifie que la liste est triée
   Check(_Fsl.Sorted = False, 'Liste déjà triée !');
   Check(_Fsl.Count = 0, 'liste : pas vide !');
   _Fsl.Add('Xtreme');
   _Fsl.Add('Milieu');
   _Fsl.Add('Avant');
   _Fsl.Sorted := True;
    // Vérifie que l'ordre est le bon
   Check(_Fsl[2] = 'Xtreme', 'Liste : elt2 non trié');
   Check(_Fsl[1] = 'Milieu', 'Liste : elt1 non trié');
   Check(_Fsl[0] = 'Avant', 'Liste : elt0 non trié');
end;

Si nous n'avions pas utilisez SetUp et TearDown, il aurait fallu pour chaque test créer et libérer la stringlist. Là, nous n'avons que deux tests mais imaginez dans le cadre de votre projet !

Cet exemple est sans doute plus intelligent que les tests du genre 1 + 1 = 2 même si on pouvait s'attendre à la validité du code car nous n'avons utilisez que les fonctions de Delphi. Mais il en sera de même avec vos propres codes à l'avenir :)

La bonne façon de faire une procédure de Test :
Dans la procédure, le test réalisé doit être simple et ne doit vérifier qu'un point précis à chaque fois. Dans l'exemple, nous testons d'abord le nombre d'élément dans une liste. Ensuite nous ajoutons la vérification dans le trie de la liste parce qu'il serait idiot et source d'erreur de trié une liste vide.

La bonne façon de faire une classe de test :
Le cas le plus facile, vous avez une classe dans votre projet de développement alors vous devez avoir une classe de test (de même pour les unités, une unité de test à chaque fois). Pour les autres cas (dans une unité mélange de fonctionnalité), il faut faire une classe par type de test. Ici, nous avons une classe pour les tests sur l'arithmétique et une pour la manipulation de TStringList. On aurait très bien pu faire une seule classe qui teste le tout mais difficile de se retrouver dans le brouillon qui en résulterait !

III-C. Exécuter les tests plusieurs fois

Poussons un peu plus loin, notre exploration. Dunit permet d'exécuter plusieurs fois les tests, histoire de vérifier qu'ils passent plus d'une fois (problème de donnée mal initialisée). Qui n'a jamais pesté contre un programme qui ne marchait qu'une seule fois ? Tout d'abord, il faut ajouter l'unité TestExtensions après TestFrameWork et avant Classes. Au passage je vous conseille de commenter l'utilité de cette unité comme je l'ai fait jusqu'à présent. Ici, vous pouvez indiquer que cette unité inclut la classe TRepeatedTest.

Saisissez le code suivant :
Juste avant la section Initialization car c'est une bonne chose de regrouper les repeats en bas.

 
Sélectionnez
function RepetitionTestMath: ITest;
var
  ATestArithemetique : TTestArithemetique;
begin
 ATestArithemetique := TTestArithemetique.create('TestPremier');
 Result := TRepeatedTest.Create(ATestArithemetique, 10);
end;

RepetitionTestMath est le nom choisi pour regrouper les tests qui vont être répétés. Cette fonction renvoie une interface de test. Portez une attention particulière à la création de l'objet ATestArithemetique, dans create passez en paramètre la méthode que vous voulez tester en répétition.

Ici, il s'agit de TestPremier.
Il faut ajouter dans la section initialization la ligne suivante pour publier l'interface de test :

 
Sélectionnez
 TestFramework.RegisterTest('repeat test',UnitRepeatTests);

Vous remarquerez que le nombre de répétition est indiqué dans l'interface.

Cette déclaration de répétition est bien mais pas top. En effet, on a été obligé de définir la procédure que l'on voulait. Pour une ça peut aller mais pour une dizaine, le code va devenir rapidement illisible en plus d'être laborieux ! Heureusement, la Dunit offre la possibilité de définir une suite qui nous permettra de regrouper les tests à répéter dans une seule fonction.

 
Sélectionnez
function RepetitionSuite : ITestSuite;
var
  AEnsTestSuite: TTestSuite;
begin
  // Répète une suite de fonction, ici les méthodes de TTestStringList
  AEnsTestSuite := TTestSuite.create('Ensemble pour la répétition');
  AEnsTestSuite.addTests(TTestStringList);
  result := TTestSuite.create('Répète les méthodes de TTestStringList 10 fois');
  result.addTest(TRepeatedTest.Create(AEnsTestSuite, 10));
end;

Il y a peu de différence avec la répétition simple ce qui n'est pas plus mal :). Il faut aussi ajouter une ligne dans la section initialization :

 
Sélectionnez
 TestFramework.RegisterTest('Répétition suite', RepetitionSuite);

Pas très originale, n'est-ce pas ? Et voilà pour la répétition, notez que j'ai choisi la classe TTestStringList car il n'y a pas d'erreur générée lors de ses tests. Du coup, c'est plus agréable sous Delphi :)

IV. Utilisation 'Avancée' de Dunit

IV-A. Détecter les fuites de mémoire

Voilà la partie qui me paraît la plus intéressante. Lorsqu'une application s'exécute, elle se réserve une partie des ressources de la machine, il y a la mémoire mais il y en a d'autres (les handles en sont). Malheureusement, il n'est pas rare que l'application ne rende pas ce qu'elle a emprunté et la police ne fait rien ! Nombre d'entre nous rêvent d'un outil qui nous permet de ne pas commettre un tel crime, gratifiant l'utilisateur de message d'erreur abscons (violation d'accès) ou rendant tétraplégique sa machine de compétition.

Et bien, ne rêvez plus, nous allons de ce pas le réaliser pour ce qui concerne la gestion de la mémoire.

Le détecteur de fuite de mémoire va se faire en deux étapes, correspondant à deux unités distinctes. La première que nous allons appeler DetecteurFuiteMemoireUnt et la seconde qui s'appellera TestCaseDfmUnt. Attention comme ces unités pourront et devraient être utilisé par tout vos projet-tests présents et avenir, je vous suggère de l'enregistrer dans le répertoire src du Dunit plutôt que dans le projet courant. L'unité DetecteurFuiteMemoireUnt contiendra le code permettant de déceler la perte de mémoire tandis que l'unité TestCaseDfmUnt permettra de l'utiliser de façon transparente, on est pas là pour se prendre la tête.

Commençons par l'unité DetecteurFuiteMemoireUnt. Créons une classe TDetecteurFuiteMemoire, contenant une variable _memoireAlloueeInitiale déclarée dans la section private en tant que type cardinal. Cardinal est un type d'entier court, largement suffisant pour stocker les valeurs utilisées. Ajoutons dans la section public un constructeur et un destructeur.

 
Sélectionnez
TDetecteurFuiteMemoire=class
   protected
      _memoireAlloueeInitiale : cardinal;
   public
      Constructor Create;
      Destructor Free;
   end;

Le constructeur va nous servir à noter la quantité de mémoire à l'instant où il sera appelé (i.e. lorsqu'un objet TDetecteurFuiteMemoire sera créé). Pour réaliser cet exploit, on utilise une Api : getHeapStatus. Pour ceux que ça intéresse, cette API a de nombreuses propriétés. Ici nous n'utiliserons que TotalAllocated.

 
Sélectionnez
constructor TDetecteurFuiteMemoire.Create();
begin
   // note la mémoire avant création
   _memoireAlloueeInitiale := getHeapStatus.TotalAllocated;
end;

Le destructeur nous servira à contrôler que la quantité de mémoire noté est égale à celle actuelle. Autrement dit que toute la mémoire a été libérée.

 
Sélectionnez
destructor TDetecteurFuiteMemoire.Free();
var
   _memoireAlloueeActuelle : cardinal;
begin
   _memoireAlloueeActuelle := getHeapStatus.Totalallocated;
   // vérifie qu'après libération que tout est rendu
   assert(_memoireAlloueeInitiale = _memoireAlloueeActuelle,
     'Erreur : fuite de mémoire.');
end;

Nous en avons fini avec cette unité. Il n'y a pas de section initialization car pas de test à publier. Attaquons tout de suite la dernière unité pour en finir avec notre détecteur, TestCaseDfmUnt. Cette unité va automatiser la détection de fuite de mémoire, rendant ainsi notre travail plus facile. Dans TestCaseDfmUnt, créons une nouvelle classe qui dérive de la classe TTestCase. Nous héritons ainsi des propriétés de la classe de base de Dunit et nous allons lui ajouter de nouvelle capacité.

Pour l'instant, nous avons une classe qui fait la même chose que son parent. Ajoutons l'unité DetecteurFuiteMemoire à cette unité, puis ajoutons dans la section protected un objet de la classe TDetecteurFuiteMemoire. Nous allons utiliser SetUp et TearDown pour qu'avant et après chaque test, on fasse un contrôle de la mémoire. Comme, SetUp et TearDown sont exécutés respectivement avant et après chaque méthode de test, voilà un tour facile. N'oubliez pas non plus le mot clé override pour hériter le code parent.

 
Sélectionnez
TTestCaseDfm = class(TTestCase)
      // classe de détection de fuite de mémoire
   protected
      detecteurFuiteMemoire : TDetecteurFuiteMemoire;
      procedure setUp();override;
      procedure tearDown();override;
   end;

Pour la déclaration, de Setup

 
Sélectionnez
procedure TTestCaseDfm.setUp();
begin
   inherited; // important hérité en premier l'ancien code avant de mettre le nouveau !
   detecteurFuiteMemoire := TDetecteurFuiteMemoire.Create;
end;

Pour le TearDown

 
Sélectionnez
procedure TTestCaseDfm.tearDown();
begin
   detecteurFuiteMemoire.Free;
   inherited; // Hériter l'ancien code après le nouveau !
end;

Voilà c'est terminé ! Notre détecteur de fuite de mémoire est fonctionnel. Enregistrez précieusement ces deux unités.

Un petit exemple pour vérifier nos connaissances de la stringlist.

  • Créons un nouveau projet-test avec son unité sans form.
  • Enregistrez le tout et modifiez comme précédemment le source du projet.
  • Ajoutez ensuite l'unité DetecteurFuiteMemoire et l'unité TestCaseDfmUnt.
  • Dans l'unité du projet-test, ajoutons dans la clause uses TestFrameWork et TestCaseDfmUnt.
  • Créons la classe test, TTestStringListe.
  • Définissons la méthode TestCreationDestruction dans la section published.

Pour l'implémentation de la méthode TestCreationDestruction, nous allons déclarer une variable TStringlist (n'oubliez pas d'ajouter Classes dans le uses). Ensuite, nous créons une instance de l'objet liste ainsi déclaré et libérons la liste (exemple idiot, j'en conviens). Dans la partie initialization, vous publiez la suite de test.

Si vous avez suivi jusqu'ici vous devriez obtenir un code similaire à celui si :

 
Sélectionnez
unit TestStringListUnt;

interface

uses
   TestFrameWork,
   TestCaseDfmUnt, // Détection de fuite de mémoire
   Classes;

type
   TTestStringList = class(TTestCaseDfm) // dérive de la classe Dfm

   published
      procedure TestCreationDestruction();
   end;

implementation

procedure TTestStringList.TestCreationDestruction;
var
   MaListe : TStringList;
begin
   MaListe := TStringList.Create;
   MaListe.Free; // N'oubliez pas cette ligne !
end;

initialization
  // publication pour exécution
  TestFrameWork.RegisterTest(TTestStringList.Suite);
end.

Si on exécute l'application, le test passe sans problème. Aucune fuite n'est détectée car on a bien libéré la liste.

Maintenant pour vérifier que notre détecteur marche, supprimons la ligne 'N'oubliez pas cette ligne'. Je parle de la ligne entière pas seulement du commentaire !

Exécutez de nouveau l'application. Cette fois la fuite de mémoire est bien détectée car la liste n'est pas libérée. Si par malheur, vous avez exécuté depuis Delphi, vous allez bloquer sur le module DetecteurFuiteMemoire, continuez l'exécution sans vous en souciez. Au passage vous aurez remarqué que l'erreur n'est pas détecté au bon endroit ce qui tout à fait normal ! Delphi n'a pas détecté la fuite de mémoire, lui. Mais il voit par contre que l'assertion n'est pas vérifiée !

IV-B. Étude de quelque cas avec Dfm

Je sais que beaucoup d'entre vous se demande quand ils doivent libérer la mémoire que leur projet a requis et qu'est-ce qui demande une libération explicite ? Quand on crée un tableau dynamique, doit-on rendre l'espace alloué ? S'il s'agit d'un objet ? Un objet de Delphi comme la stringlist ? Et pour les objets contenus dans d'autre objet ? Il y a aussi la question sur les fiches.

Une fois pour toute, vous saurez par l'intermédiaire de ces exemples de quoi il retourne.

Pour m'épargner un peu, je ne vais pas retaper l'unité entière à chaque fois, alors utilisez celle du test sur les tableaux dynamiques comme référence. Je vous rassure quand même, je suis un adapte du copier/coller :)

IV-B-1. Les tableaux dynamiques

Je me suis déjà posé la question relative à ces tableaux. Quand on utilise un tableau dynamique, on précise la taille voulu par l'intermédiaire de la commande SetLength. Alors une fois terminé, doit-on rendre cet espace demandé ?

Saisissons le code suivant :

 
Sélectionnez
unit TestLibererMaRamUnt;

interface

uses
   TestFrameWork,
   TestCaseDfmUnt; // Détection de fuite de mémoire

type
   TTestLibererMaRam = class(TTestCaseDfm) // dérive de la classe Dfm
   published
      procedure TestOuiNon();
   end;

implementation

procedure TTestLibererMaRam.TestOuiNon;
var
   MonTableau : array of integer;
begin
   SetLength(MonTableau, 1000);
end;

initialization
  // publication pour exécution
  TestFrameWork.RegisterTest(TTestLibererMaRam.Suite);
end.

Exécutez le test. Comme prévu aucune libération de mémoire n'a été nécessaire. Disons qu'on a fait aucune demande d'allocation de mémoire, juste préciser la taille que prenait le tableau a un instant t. Rappelez vous aussi que vous pouvez changer la taille du tableau en court de route.

Position de la déclaration de la variable :
Prenez garde à l'endroit où vous déclarez le tableau! Rappelez-vous qu'un TearDown est exécuté à chaque sortie de procédure Test. Ce TearDown provient de l'héritage de la classe TTestCaseDfm et il contrôle la mémoire avant et après la procédure. Le tableau étant en local, il est bien libéré en sortie de la procédure TestOuiNon.

Si vous déclarez le tableau en variable globale ou dans la classe TTestlibererMaRam alors le tableau existera toujours en sortie de TestOuiNon et le TearDown échouera en indiquant une fuite de mémoire. Le TearDown constate une différence de mémoire avant et après car le tableau conserve sa taille à la sortie de TestOuiNon. Le tableau existe toujours et il a conservé aussi ses valeurs, encore heureux.

IV-B-2. Les pointeurs

Après les tableaux dynamiques, voyons les pointeurs. Logique vu que les premiers dérivent des seconds. Mais qu'en est-il pour la mémoire ?
Remplaçons Montableau par un pointeur sur entier :

 
Sélectionnez
 PMonPointeur : ^integer;

Modifions la méthode TestOuiNon :

 
Sélectionnez
procedure TTestLibererMaRam.TestOuiNon;
var
   PMonPointeur : ^integer;
begin
   PMonPointeur := nil;
   Check(1 + 1 = 2, 'Il faut au moins un test');
end;

Si on exécute ce programme, il ne garde pas de mémoire pour lui. Mais en affectant nil au pointeur, il n'existe pas vraiment. Remplaçons la ligne d'affectation par une ligne de création :

 
Sélectionnez
 new(PMonPointeur);

Cette fois, la mémoire n'a pas été libérée alors qu'on a demandé une allocation (operateur new). Corrigeons afin de rendre les ressources :

 
Sélectionnez
procedure TTestLibererMaRam.TestOuiNon;
var
   PMonPointeur : ^integer;
begin
   new(PMonPointeur);
  // ici on peut manipuler le pointeur ...
   dispose(PMonPointeur);
end;

Voilà, vous n'avez plus d'excuses maintenant. Pour plus d'informations sur les pointeurs, consultez le chapitre du guide correspondant. Au passage, notez que le test bidon a disparu mais c'est un détail.

IV-B-3. Les objets

Voilà des bêtes bien plus sympathiques que les pointeurs mais qui requiert autant de précision dans leur utilisation. Créons une classe tout ce qui a de plus bête, TMaClass. Dans TestOuiNon, on déclara un objet de cette classe et on le crée. Attention, ne surtout pas oublier de le crée ou Delphi va vraiment raller.

 
Sélectionnez
unit TestLibererMaRamUnt;

interface

uses
   TestFrameWork,
   TestCaseDfmUnt; // Détection de fuite de mémoire

type
   TMaclass = class
      caract1 : integer;
   end;

   TTestLibererMaRam = class(TTestCaseDfm) // drive de la classe Dfm
   published
      procedure TestOuiNon();
   end;

implementation

procedure TTestLibererMaRam.TestOuiNon;
var
   MonObjet : TMaClass;
begin
   MonObjet := TMaClass.Create;
   MonObjet.caract1 := 2;
end;

initialization
  // publication pour exécution
  TestFrameWork.RegisterTest(TTestLibererMaRam.Suite);
end.

Grâce à cet exemple simpliste, on voit clairement que même un objet n'ayant qu'un entier à proposer à besoin d'être libéré. On a fait une demande d'allocation de la taille d'un entier pour MonObjet, on doit donc désallouer cette mémoire.

Petite remarque : même si TMaClass ne contient pas de constructeur, ni de destructeur, on a quand même la procédure create! En réalité, notre classe dérive d'une autre classe même si c'est transparent.

Ajoutons la ligne libératrice :

 
Sélectionnez
MonObjet.Free;

IV-B-4. Les objets d'objet

Voilà, qui mérite notre attention. Que se passe-t-il quand une variable contient d'autre objet ? Modifions notre exemple :

 
Sélectionnez
procedure TTestLibererMaRam.TestOuiNon;
var
   MonTabObjet : array of TMaClass;
begin
   Setlength(MonTabObjet,3);
end;

Je ne vous ferais pas l'affront de vous demander de faire un run, car on n'a pas encore demandé d'allocation. Voir la partie sur les tableaux dynamique. Créons quelques objets dans ce tableau :

 
Sélectionnez
procedure TTestLibererMaRam.TestOuiNon;
var
   MonTabObjet : array of TMaClass;
   i : integer;
begin
   Setlength(MonTabObjet,10);
   for i:=0 to High(MonTabObjet) do
      MonTabObjet[i] := TMaClass.Create;
end;

Je vous propose un petit jeu, essayer de libérer la mémoire sans lire la suite.

 
Sélectionnez
procedure TTestLibererMaRam.TestOuiNon;
var
   MonTabObjet : array of TMaClass;
   i : integer;
begin
   Setlength(MonTabObjet,10);
   for i:=0 to High(MonTabObjet) do
      MonTabObjet[i] := TMaClass.Create;

   for i:=0 to High(MonTabObjet) do
      MonTabObjet[i].Free;
end;

Corsons la difficulté, créons une classe qui contient des objets. Ajoutons une classe qui utilise des objets de la classe déjà écrite.

Attention, je ne parle pas d'hériter de la classe!

 
Sélectionnez
unit TestLibererMaRamUnt;

interface

uses
   TestFrameWork,
   TestCaseDfmUnt; // Détection de fuite de mémoire

type
   TMaClass = class
      caract1 : integer;
   end;

   TMaClassDeClass = class
      caract2 : integer;
      sousObjet : TMaClass;
   end;

   TTestLibererMaRam = class(TTestCaseDfm) // drive de la classe Dfm
   published
      procedure TestOuiNon();
   end;

implementation

procedure TTestLibererMaRam.TestOuiNon;
var
   MonObjet : TMaClassDeClass;
begin
   MonObjet := TMaClassDeClass.Create;
   MonObjet.sousObjet := TMaClass.Create;
   MonObjet.Free;
end;

initialization
  // publication pour exécution
  TestFrameWork.RegisterTest(TTestLibererMaRam.Suite);
end.

Si on lance le test, on s'aperçoit que le fait de libérer l'objet parent ne libère pas les objets enfants. Remanions un peu notre code pour écrire correctement la création de l'objet.

 
Sélectionnez
unit TestLibererMaRamUnt;

interface

uses
   TestFrameWork,
   TestCaseDfmUnt; // Détection de fuite de mémoire

type
   TMaClass = class
      caract1 : integer;
   end;

   TMaClassDeClass = class
      caract2 : integer;
      sousObjet : TMaClass;
      constructor Create(val : integer);
      destructor Free();
   end;

   TTestLibererMaRam = class(TTestCaseDfm) // dérive de la classe Dfm
   published
      procedure TestOuiNon();
   end;

implementation

constructor TMaClassDeClass.Create(val : integer);
begin
   // Crée les sous Objets et initialise sa propriété.
   sousObjet := TMaClass.Create;
   // l'objet est crée alors on ne se prive pas
   sousObjet.caract1 := val;
end;

destructor TMaClassDeClass.Free();
begin
   //
   sousObjet.Free;
end;

procedure TTestLibererMaRam.TestOuiNon;
var
   MonObjet : TMaClassDeClass;
begin
   MonObjet := TMaClassDeClass.Create(2);
   // On peut manipuler sousObjet car il est crée dans le constructeur
   // de TMaClassDeClass
   Check(MonObjet.sousObjet.caract1 = 2, 'La valeur affectée à la création '
    + 'n''est pas retrouvé');
   MonObjet.Free;
end;

initialization
  // publication pour exécution
  TestFrameWork.RegisterTest(TTestLibererMaRam.Suite);
end.

Voilà, au passage j'en profite pour vérifier l'initialisation. En gardant ce genre de test, vous saurez que votre initialisation se comporte comme vous l'avez souhaité au départ.

IV-B-5. Les fiches

Pour les fiches créées automatiquement, l'application se charge de les libérer car l'application est alors le parent de ces fiches. Lorsque l'application reçoit l'ordre de se fermer, elle envoie à tous ses enfants l'ordre de libérer la mémoire. Ses enfants répercutent cet ordre à leur propre enfant (en générale les composants comme les boutons). Si au contraire, vous créez une fiche ou un composant sans parent, c'est à vous de libérer les ressources ainsi utilisé.

Libération manuelle :
Dans le cas où vous devez libérer manuellement les ressources, fiche ou composant sans parent, prenez garde sur la façon de le faire. En effet, selon que vous libériez les ressources à partir de l'objet ou depuis un code extérieur, la méthode employée sera différente.

Pour libérer les fiches depuis un gestionnaire d'événement leur appartenant ou à un de leur composant, vous devez utilisez la méthode release. « Tous les gestionnaire d'événement de la fiche doivent utiliser Release à la place de Free. Si vous ne respectez pas cette règle, une violation d'accès risque d'être générée." Extrait de l'aide Delphi sur Release.

« Ne jamais libérer explicitement un composant dans un de ses propres gestionnaires d'événements, ni libérer un composant du gestionnaire d'événement d'un composant qu'il possède ou contient. Par exemple, ne libérez pas un bouton dans son gestionnaire d'événement OnClick, ou ne libérez pas la fiche possédant le bouton depuis l'événement OnClick du bouton.free » Extrait de l'aide Delphi sur Free.

Si le code de libération provient d'une autre unité, c'est la méthode Free qu'il faut utiliser.

Reprenons notre référence le projet-test pour les tableaux dynamiques. Ajoutons dans la clause uses l'unité Dialogs pour pouvoir utiliser le showmessage. Créez ensuite une nouvelle fiche, j'ai ajouté un label avec la propriété caption valant 'fiche de test' pour faire beau. Enregistrez la sous le nom fmTestFrm.pas et ajoutez là au projet-test. Dans la clause uses de l'unité TestLibererMaRamUnt.

Nous allons modifier la procédure TestOuiNon afin de tester la création et la libération de la fiche de l'unité fmTestFrm. Comme la libération ne sera pas immédiate, j'ai ajouté un appel à ShowMessage pour temporiser. J'aurais pu faire une boucle conséquente, ou trouver un moyen plus propre pour faire la pause mais rappelez vous les tests doivent être simple (dans le sens qu'ils ne doivent pas inclure de source d'erreur).

En plus, chose amusante si vous êtes un peu trop pressé, le détecteur de fuite de mémoire va se déclencher :)

Le nouveau code de TestOuiNon devient :

 
Sélectionnez
procedure TTestLibererMaRam.TestOuiNon;
var
   fmTest : TForm1;
begin
   fmTest := nil;
   try
     fmTest := TForm1.create(nil);
     fmTest.ShowModal
   finally
     fmTest.Free;
     // obliger pour le test, car il y a un temps de latence pour le free ou
     // release.
     // Si on ne met pas d'attente, on aurait une erreur de détection dfm !
     ShowMessage('attente : ne cliquez pas trop vite (environ 5 sec)!');
   end;
end;

Notez que j'ai déclaré une variable locale fmTest de Type TForm1 (le nom de classe de la fiche) mais on aurait pu aussi utiliser Form1 qui est la variable globale déclarée dans l'unité fmTestFrm.

Ceci devrait vous permettre surtout de tester si une fois la fiche libéré, toutes les ressources qu'elle a pu monopoliser ont bien été rendues. Je pense notamment au composant crée dynamiquement.

De plus en utilisant GUITesting.pas, vous pourrez simuler l'utilisation de l'interface par un utilisateur. GUITesting.pas fait partie des unités que je n'ai pas utilisées dans le cadre de ce tutoriel. Elle contient un ensemble de fonction qui permet de simuler et tester l'interface (la fiche).

Cette fois, je pense que vous êtes prêt à mettre en pratique vos acquis. Je vous propose le chapitre suivant pour voir l'utilisation de Dunit dans un cas concret.

V. Dunit, pratique

Enfin le chapitre final (quoique …). Ici nous allons voir comment utiliser Dunit, dans le cadre d'un projet. Fini de s'amuser maintenant, c'est du sérieux ! Bon d'accord, je plaisante. Je vais essayer de vous montrer l'intérêt de Dunit sur un projet qui pourrait être réel. Remarquez que je n'utiliserai pas Dunit dans le contexte où il est sensé évoluer à savoir l'extreme programming. Je ne dirais que deux choses à ce sujet.

L'extrême programming part des tests pour obtenir le code et pour plus d'info faite appel à votre moteur de recherche préféré (un bon point de départ : Image non disponible Xp-france.net).

Autre point important, ne tardez pas trop à mettre en place vos tests. Si vous pensez coder les tests une fois le projet terminée, vous allez vite vous décourager. Rappelez vous les principes de la bonne programmation, vous devez tester chaque fonction avant d'en commencer une autre (DUnit ou pas !). Ici comme le projet est petit et utilisé à des fins didactiques, je mets les tests une fois l'unité complètement terminée mais je les attaque un à la fois. Bon trêve de blabla et au boulot.

V-A. Tester vos projets par Dunit

Nous allons maintenant voir comment utiliser Dunit pour contrôler votre projet. J'ai un peu cherché quel genre d'exemple, je pourrais bien vous proposer et je suis tombé sur l'exercice de programmation des piles du guide. Dés que j'ai vu la présence de pointeur, j'ai su que c'était un bon exemple (enfin je l'espère). Le mot pointeur est celui qui effraie le plus le programmeur en herbe, ou lui font briller ses yeux de convoitise :)

J'ai volontairement été moins dictatique sur cette partie histoire de vous apprendre à voler de vos propres ailes. J'annonce ce que vous devez faire et ensuite ce que vous devriez obtenir.

  • Préparerons d'abord les répertoires pour ce nouveau projet. Un de base et un sous répertoire pour accueillir le projet-test. Cette façon de faire permet de bien séparer les choses, en effet vous ne souhaitez pas forcement livrer le projet-test aux clients finaux. Je vous laisse trouver des noms parlant.
  • Créez le projet dans le répertoire de base. C'est un programme classique donc un simple nouveau suffit. Enregistrer l'unité sous le nom de PrincFrm.Pas, puis ajoutez une autre unité PilesTabUnt mais sans form. Terminez en enregistrant le projet.
  • Créez ensuite ou plus tard le projet-test dans son répertoire dédié. Ici, avant d'enregistrer, il faut supprimer l'unité avec fiche pour en ajouter une nouvelle sans form.
  • Modifiez aussi le chemin de recherche dans les options du projet si vous n'avez pas choisi de modifier l'environnement.
  • Enregistrez l'unité sous le nom de TestPilesTabUnt.pas.
  • Ajoutez l'unité PilesTabUnt et déclarez là dans la section uses de l'unité TestPilesTabUnt.
  • Enregistrez le projet-test.

Comme nous avons ajouté l'unité PilesTabUnt qui est pourtant destinée au projet final, nous n'aurons pas besoin de faire la navette entre le projet testé et le projet-test.

Comme le cours ne porte pas sur la construction d'unité manipulant les piles, je vous livre le code tout fait. Il n'y a plus qu'à faire un copier/coller. Vérifiez bien que le nom de l'unité est identique ! Sinon gardez le votre.

 
Sélectionnez
unit PilesTabUnt;

interface

uses
  Classes;

type
  PPileElem = ^TPileElem; // Attention à l'ordre des déclarations

  TPileElem = record
    Elem: string;
    Suiv: PPileElem;
  end;

// Création d'une pile vide
function PTNouvelle: PPileElem;
// Indicateur de pile vide
function PTVide(Pile: PPileElem): Boolean;
// Empilement d'un élément
function PTEmpiler(Pile: PPileElem; S: string): PPileElem;
// Dépilement d'un élément
function PTDepiler(Pile: PPileElem): PPileElem;
// Destruction d'une pile
procedure PTDetruire(Pile: PPileElem);
// Accès au sommet
function PTSommet(Pile: PPileElem): string;
// Affichage du contenu d'une pile
procedure PTAffiche(Pile: PPileElem; Sortie: TStrings);

implementation

function PTNouvelle: PPileElem;
begin
   result := nil;
end;

function PTVide(Pile: PPileElem): Boolean;
begin
   result := Pile = nil;
end;

function PTEmpiler(Pile: PPileElem; S: string): PPileElem;
var
   temp : PPileElem;
begin
   new(temp);
   temp^.Elem := s;
   temp^.suiv := Pile;
   result := temp;
end;

function PTDepiler(Pile: PPileElem): PPileElem;
begin
  if Pile <> nil then
  begin
     result := Pile^.suiv;
     Dispose(Pile);
  end
  else
     result := nil;
end;

procedure PTDetruire(Pile: PPileElem);
begin
   while not PTVide(Pile) do
    Pile := PTDepiler(Pile);
end;

function PTSommet(Pile: PPileElem): string;
begin
  if Pile <> nil then
    result := Pile^.Elem
  else
    result := '';
end;

procedure PTAffiche(Pile: PPileElem; Sortie: TStrings);
var
  temp : PPileElem;
begin
  temp := Pile;
  Sortie.Clear;
  while temp <> nil do
  begin
     Sortie.Add(temp^.Elem);
     Temp := Temp^.suiv;
  end;
end;

end.

Tout ça c'est bien joli mais quel test devons nous mettre en place ? La réponse est facile. La pile va être manipulé uniquement par les fonctions, c'est donc toutes les fonctions qu'il faut tester et vérifier qu'elles font bien ce qu'on attend d'elle.

Ajoutez PilesTabUnt dans la clause uses si ce n'est pas déjà fait. Puis, déclarez une nouvelle classe test TTestPiles qui dérive de TTestCaseDfm (pour inclure la détection de fuite de mémoire). En variable protected déclarez:

 
Sélectionnez
 _MaPile : PPileElem;

Mettez tout de suite la partie initialisation (la section initialization et le code correspondant). Modifiez le source du projet-test conformément à ce que nous avons fait jusqu'à présent. Je vous laisse chercher un peu.

Commençons par le commencement, vérifions la création d'une pile. Dans la classe TTestPiles, déclarez la méthode TestNouveau.

L'implémentation se fera en utilisant la fonction PTNouvelle.

Ce qui donne à peu près ça :

 
Sélectionnez
unit TestPilesTabUnt;

interface

uses
   TestFrameWork,
   TestCaseDfmUnt, // Détection de fuite de mémoire
   PilesTabUnt;

type
   TTestPiles = class(TTestCaseDfm) // dérive de la classe Dfm
   protected
      _MaPile : PPileElem;
   published
      procedure TestNouveau();
   end;

implementation

procedure TTestPiles.TestNouveau;
begin
   //
   _MaPile := PTNouvelle;
   Check(nil = _MaPile);
end;

initialization
  // publication pour exécution
  TestFrameWork.RegisterTest(TTestPiles.Suite);
end.

Nous venons de valider la 'création' d'une nouvelle pile. Entre guillemet car on a fait aucune demande d'allocation. Testons une autre fonction, la fonction qui vérifie si la pile est vide me paraît tout indiqué. En effet, nous savons créer une pile vide, nous avons vérifié qu'elle était bien vide, est-ce que la fonction donne le même résultat ?

Ajoutez la méthode TestVide et voici son implémentation :

 
Sélectionnez
procedure TTestPiles.TestVide;
begin
   CheckEquals(True, PTVide(_MaPile), 'La pile n''est pas vide');
end;

Avant d'empiler, plaçons une vérification sur le dépilement et dans la foulée testons l'empilement.

 
Sélectionnez
procedure TTestPiles.TestDepiler;
begin
   _MaPile := PTNouvelle;
   _MaPile := PTDepiler(_MaPile);
   CheckEquals(True, PTVide(_MaPile) , 'La pile n''est pas dépiler');
end;

procedure TTestPiles.TestEmpiler;
begin
   _MaPile := PTNouvelle;
   _MaPile := PTEmpiler(_MaPile, 'une valeur');
   CheckEquals('une valeur', _MaPile.Elem , 'ça empile pas ! ');
   _MaPile := PTDepiler(_MaPile); // Depiler sinon fuite de mémoire
end;

Si on regarde de plus près le TestEmpiler, on s'aperçoit qu'on utilise la fonction PTDepiler. Heureusement, nous l'avons déjà testé. N'oubliez pas de dépiler la pile si non vous provoquerez une déperdition de Ram. La fonction PTEmpiler fait une allocation de mémoire (operateur new), il nous faut donc la libérer (dispose dans PTDepiler).

Testons la destruction maintenant que l'on sait construire.

 
Sélectionnez
procedure TTestPiles.TestDestruire;
begin
   _MaPile := PTNouvelle;

   // il faudrait dans un premier temps tester la pile nouvellement crée
   //PTDetruire(_MaPile);
   //CheckEquals(True, PTVide(_MaPile), 'La pile vide n''est pas détruite');
   // avant d'ajouter des éléments

   _MaPile := PTEmpiler(_MaPile, 'une valeur');
   _MaPile := PTEmpiler(_MaPile, 'deux valeurs');
   PTDetruire(_MaPile);
   CheckEquals(True, PTVide(_MaPile), 'La pile n''est pas vide');
end;

Ici, la partie en commentaire se passe bien mais on ne peut pas en dire de même pour le reste. Notre Projet-test détecte une perte de mémoire. Honnêtement, je ne l'ai vraiment pas fait exprès et je me suis demandé ce qui se passait. Je vous laisse chercher un peu avec ce petit indice regardez bien la fonction PTDetruire au niveau de sa déclaration.

C'est bon vous avez trouvé ? Les plus perspicaces d'entre vous (ou les plus réveillés) auront remarqué que la pile est passé par valeur ! Une pile local est créée et vidée mais l'original garde son état. Modifions la déclaration :

 
Sélectionnez
PTDetruire(var Pile: PPileElem);

Voilà le problème est résolu. Imaginez le temps passé à chercher ce genre d'erreur sans le Dunit. Car, à part peut être quelque rare élu, peu d'entre vous ont dû voir l'erreur avant que je vous demande de la chercher.

IMPORTANT :
Attention, le pointeur local pointe au départ sur la même adresse que le pointeur original. Donc il pointe sur la même valeur. Si vous modifiez la valeur du pointeur local (sans changer son adresse), c'est bien la valeur du pointeur original que vous modifiez !
Notre problème était tout autre, nous modifions l'adresse du pointeur local. En gros, on lui disait de voir ailleurs mais on ne disait rien au pointeur original.

 
Sélectionnez
procedure TTestPiles.TestSommet;
begin
   _MaPile := PTNouvelle;
   _MaPile := PTEmpiler(_MaPile, 'une valeur');
   CheckEquals('une valeur', PTSommet(_MaPile), 'Le sommet n''est pas le bon');

   _MaPile := PTEmpiler(_MaPile, 'deux valeurs');
   _MaPile := PTEmpiler(_MaPile, 'trois valeurs');
   CheckEquals('trois valeurs', PTSommet(_MaPile),
     'Le sommet n''est pas le bon');

   PTDetruire(_MaPile); // Ne pas oublier
end;

Sans commentaire :)

Ajoutez l'unité Classe dans la clause uses car nous allons utiliser un TStringList.

 
Sélectionnez
procedure TTestPiles.TestAffiche;
var
   Sortie: TStringList;
begin
   _MaPile := PTNouvelle;
   _MaPile := PTEmpiler(_MaPile, 'une valeur');
   _MaPile := PTEmpiler(_MaPile, 'deux valeurs');
   _MaPile := PTEmpiler(_MaPile, 'trois valeurs');

   Sortie := TStringList.Create;
   PTAffiche(_MaPile, Sortie);
   CheckEquals(3, Sortie.count, 'Il manque des éléments');
   CheckEquals('une valeur', Sortie[2], 'Erreur d''éléments 1');
   CheckEquals('deux valeurs', Sortie[1], 'Erreur d''éléments 2');
   CheckEquals('trois valeurs', Sortie[0], 'Erreur d''éléments 3');

   // Ne pas oublier
   Sortie.Free;
   PTDetruire(_MaPile);
end;

Voilà, nous avons terminé de tester l'unité. Notez que pour la déclaration de la variable Sortie, nous n'avons pas utilisé TStrings car c'est une classe abstraite ! A la place, nous utilisons TStringList qui hérite de TStrings.

Notez qu'on aurait pu (ou du) mettre la création et la destruction dans un SetUp et un TearDown. Après les avoir testé bien sûr sinon vous auriez eu le droit à des erreurs dans tous les tests :(

Bon maintenant que l'unité de manipulation de pile est validé, passons à l'interface. Cette partie risque d'être un peu sportive surtout que c'est la première fois que je teste les interfaces. Mais soyons fou, il paraît que c'est une caractéristique du développeur en plus d'être mauvais en orthographe :)

Comme je suis trop bon avec vous, ci dessous le code de l'interface, c'est-à-dire de l'unité PrincFrm. Pour éviter de se fatiguer, ajouter cette unité au projet-test car comme pour l'unité PilesTabUnt, nous allons pouvoir travailler sur son code sans basculer d'un projet à l'autre.
Je vous laisse deviner les composants nécessaires (regardez les propriétés de la classe TfmPrinc pour les trouver) et n'oubliez pas de leur rattacher les gestionnaires d'événement correspondant.

 
Sélectionnez
unit princ;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, PilesTabUnt;

type
  TfmPrinc = class(TForm)
    mePile: TMemo;
    Label1: TLabel;
    btDepile: TButton;
    btEmpile: TButton;
    btVidePile: TButton;
    btQuitter: TButton;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure btEmpileClick(Sender: TObject);
    procedure btDepileClick(Sender: TObject);
    procedure btVidePileClick(Sender: TObject);
    procedure btQuitterClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  fmPrinc: TfmPrinc;
  Pile: PPileElem;

implementation

{$R *.DFM}

procedure MajInterface;
var
  vide: boolean;
begin
  PTAffiche(Pile, fmPrinc.mePile.Lines);
  vide := PTVide(Pile);
  fmPrinc.btDepile.Enabled := not vide;
  fmPrinc.btVidePile.Enabled := not vide;
end;

procedure TfmPrinc.FormCreate(Sender: TObject);
begin
  Pile := PTNouvelle;
end;

procedure TfmPrinc.FormDestroy(Sender: TObject);
begin
  PTDetruire(Pile);
end;

procedure TfmPrinc.btEmpileClick(Sender: TObject);
var
  S: String;
begin
  if InputQuery('Empilement d''une chaîne', 'Saisissez une chaîne à empiler', S) then
    begin
      Pile := PTEmpiler(Pile, S);
      MajInterface;
    end;
end;

procedure TfmPrinc.btDepileClick(Sender: TObject);
begin
  Pile := PTDepiler(Pile);
  MajInterface;
end;

procedure TfmPrinc.btVidePileClick(Sender: TObject);
begin
  while not PTVide(Pile) do
    Pile := PTDepiler(Pile);
  MajInterface;
end;

procedure TfmPrinc.btQuitterClick(Sender: TObject);
begin
  Close;
end;

end.

Enregistrez les changements et revenons à notre projet-test en lui ajoutant une nouvelle unité sans form. Je l'ai nommé TestPrincUnt. Cette unité reprend le même squelette que les autres unités. Comme nous voulons tester l'unité PrincFrm, il faut l'inclure dans la clause uses. La première étape, tester la création de la form. Comme nous utiliserons la classe TTestCaseDfm, nous testerons surtout la gestion de la mémoire.

 
Sélectionnez
procedure TTestPrinc.TestFmCreate;
begin
   fmPrinc := nil;
   try
     fmPrinc:= TfmPrinc.create(nil);
     fmPrinc.ShowModal
   finally
     fmPrinc.Free;
     // obliger pour le test, car il y a un temp de latence pour le free ou
     // release.
     // Si on ne met pas d'attente, on aurait une erreur de détection dfm !
     showmessage('attente : ne cliquez pas trop vite (5~10 sec) !');
   end;
end;

On est obligé de mettre une pause pour éviter une erreur de détection dans la gestion de la mémoire.

VI. Conclusion

Vous auriez tort de vous priver de cet outil de test, tellement il est simple à mettre en place et vous évitera de vous prendre la tête ou celle de vos clients pendant des heures. En plus le détecteur de fuite de mémoire est vraiment performant, finit le squatte des ressources comme c'est trop souvent le cas.

Une dernière chose, chaque test indique le temps qu'il a mis pour s'exécuter. Vous pouvez du coup pointer le doigt sur les parties lentes de vos programmes et tenter de les optimiser.

Pour finir, il existe aussi DUnitWizard 3.0 pour les plus fainéants d'entre vous :) qui simplifie la mise en place.

Ah, je n'y avais pas pensé mais pour ceux qui utilisent Delphi .NET, il existe un Framework dédié, NUnit. Facile à retenir, d'ailleurs, il y a aussi Junit (pour Java et l'origine des autres), CppUnit (pour c et c++), PhpUnit (pour Php), PyUnit (pour python), etc.

En tout cas j'espère vous avoir convaincu :)

Sommaire

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2005 Tony Baheux. Aucune reproduction, même partielle, ne peut être faite de ce site et 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.