Cute Ninja Welcome to Flash the tigre's Web site griffe

Chapitre 105

105. Dunit
Chapitre 105  :  Dunit, Découverte

Chapitre écrit par Tony BAHEUX
Inscrivez-vous ici pour être informé(e) des mises à jour,
poser des questions, répondre à d'autres...

(liste à usage strictement privé et non publicitaire)


Table des matières du chapitre :


105. Dunit, Découverte
105.1. Introduction

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, boite de dialogue et autres interface graphiques. Cependant, ces unités permettent surtout de simuler l'interaction de l'utilisateur avec l'interface. Nous verrons quand même comment tester une unité contenant une form. Le présent tutorial 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.

105.2. Téléchargement et 'installation'

Pour le télechargement et surtout l'installation, reportez-vous au chapitre précédent : 105. Dunit, Installation

Retour en haut de la page

105.3. Utilisation de Dunit : Découverte

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.

105.3.1. Première prise en main

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 repertoir 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,
  • Une première méthode, procedure 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 compté!');

Petite explication, L'unité TestFrameWork contient toutes les procedures 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'intermediaire 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 executer cette méthode. Les concepteurs du framework appelent 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'appli. 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 : TestFramework.RegisterTest(TTestPremierEssai.Suite);
Comme vous pouvez le voir, on publie une suite. Toutes les méthodes que vous avez déclaré published dans la classe seront publié (il y a comme une certaine logique dans le choix des noms :) ).
Vous 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 faineant, moins il en fait mieux il se porte.

Ce qui donne

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 compté!');
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é.

Ce qui donne

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'executable. 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 expections.
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 Dunit 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 :

premier essai

et après exécution :

Resultat de l'essai

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

105.3.2. Quelques tests

Poursuivons, en testant des cas d'erreur histoire de voir de quoi il retourne avec des cas simples. Dans la classe TTestPremier, renommons là en TestArithemtic. N'oubliez pas la procedure également. Ajoutons deux nouvelles méthodes, TestSecond et TestTrois.

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

procedure TTestArithmetic.TestThird();
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é. Pour vous amusez, testez l'appli depuis l'executable puis depuis le RAD. 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 (ie 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 denote 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 developpez 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 mutliplication d'une matrice par l'identité ne donnait pas la même matrice :(

Fort de cette première experience, 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 Dunit, 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 TTestCase). Elles sont appelées avant chaque procedure 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 :

TTestStringList = class(TTestCase) // Test stringlist
   private
      _Fsl : TStringList;
   protected
      procedure SetUp; override;  // executer avant chaque test
      procedure TearDown; override; // executer 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 :

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.
   // verifie 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
   // Verifie que la liste est trié
   Check(_Fsl.Sorted = False, 'Liste déjà trié !');
   Check(_Fsl.Count = 0, 'liste : pas vide !');
   _Fsl.Add('Xtreme');
   _Fsl.Add('Milieu');
   _Fsl.Add('Avant');
   _Fsl.Sorted := True;
   // Verifie 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 test 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 procedure de Test :

Dans la procedure, 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'arithmie 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 resulterait !

105.3.3. Exécuter les tests plusieurs fois

Poussons un peu plus loin, notre exploration. Dunit permet d'executer plusieur 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.

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

function RepetitionTestMath: ITest;
var
  ATestArithmetic : TTestArithmetic;
begin
 ATestArithmetic := TTestArithmetic.create('TestPremier');
 Result := TRepeatedTest.Create(ATestArithmetic, 10);
end;

RepetitionTestMath est le nom choisi pour regrouper les tests qui vont être répêtés. Cette fonction renvoit une interface de test. Portez une attention particulière à la création de l'objet ATestArithmetic, 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 :
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 chiant ! Heureusement, la Dunit offre la possibilitée de définir une suite qui nous permettra de regrouper les tests à répéter dans une seule fonction.

function RepetitionSuite : ITestSuite;
var
  AEnsTestSuite: TTestSuite;
begin
  // Repete 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épete TTestStringList méthodes 10 fois');
  result.addTest(TRepeatedTest.Create(AEnsTestSuite, 10));
end;

Il y a peu de différence avec la répetition simple ce qui n'est pas plus mal :)
Il faut aussi ajouter une ligne dans initialization :
TestFramework.RegisterTest('Repetition 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 :)

Retour en haut de la page

105.4. Utilisation de Dunit : 'Avancée'

105.4.1. Détecter les fuites de mémoire

Voilà la partie qui me paraît la plus intéressante. Lorsqu'une application s'éxé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 abscont (violation d'accès) ou rendant tétraplégique sa machine de compétition.
Et bien, ne revez 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 distincts. La première que nous allons appeler DetecteurFuiteMemoireUnt et la seconde qui s'appelera TestCaseDfmUnt. Attention comme ces unités pourront et devraient être utilisé par tout vos projet-tests présents et avenir, je vous sugère de l'enregistrer dans le src du Dunit plutôt que dans le projet courant. L'unité DetecteurFuiteMemoireUnt contiendra le code permettant de déceler la perte de memoire tandis que l'unité TestCaseDfmUnt permettra de l'utiliser de façon transparente, on est pas là pour s'emmerder.

Commençons par l'unité DetecteurFuiteMemoireUnt. Créons une classe TDetecteurFuiteMemoire, contenant une variable _memoireAlloueeInitiale déclarer dans la section private en tant que 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 destruteur.

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

Le constructeur va nous servir à noter la quantité de memoire à l'instant où il sera appelé (ie 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 nombreuse propriétée. Ici nous n'utiliserons que TotalAllocated.

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.

destructor TDetecteurFuiteMemoire.Free();
var
   _memoireAlloueeActuelle : cardinal;
begin
   _memoireAlloueeActuelle := getHeapStatus.Totalallocated;
   // verifie 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 executé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.

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

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

Pour le TearDown

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écedement 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éfinisons la méthode TestCreationDestruction dans la section published.
Pour l'implémentation de la méthode TestCreationDestruction, nous allons déclaré 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 :

unit TestStringListUnt;

interface

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

type
   TTestStringList = class(TTestCaseDfm) // derive 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 parles de la ligne entière pas seulement du commentaire !
Exécutez de nouveau l'appli. 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 excuté 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 detecté au bon endroit ce qui tout a 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 !

105.4.2. Etude 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 dynamque, doit-on rendre l'espace alloué ? S'il s'agit d'un objet ? Un objet de Delphi comme la stringlist ? Et pour les objets contenu dans d'autre objet ? Il y a aussi la question sur les fiches.
Une fois pour toute, vous saurez par l'intermediaire 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 copie/colle :)

  • 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é?

Tapons le code suivant :

unit TestLibererMaRamUnt;

interface

uses
   TestFrameWork,
   TestCaseDfmUnt; // Détection de fuite de memoire

type
   TTestLibererMaRam = class(TTestCaseDfm) // derive 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 execution
  TestFrameWork.RegisterTest(TTestLibererMaRam.Suite);
end.

Exécutez le test. Comme prévu aucune libération de mémoire n'a été necessaire. Disons qu'on a fait aucune demande d'allocation de mémoire, juste préciser la taille que prennait 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 :

Prennez garde à l'endroit où vous déclarez le tableau ! Rappelez-vous qu'un TearDown est éxécuté à chaque sortie de procedure 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 procedure TestOuiNon. Si vous déclarez le tableau en variable global 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.

  • 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 : PMonPointeur : ^integer;
Modifions la méthode TestOuiNon :

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 :

new(PMonPointeur);

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

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.

  • Les objets

Voilà des bêtes bien plus sympathique 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 raler.

unit TestLibererMaRamUnt;

interface

uses
   TestFrameWork,
   TestCaseDfmUnt; // Détection de fuite de memoire

type
   TMaclass = class
      caract1 : integer;
   end;

   TTestLibererMaRam = class(TTestCaseDfm) // derive 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 procedure create ! En réalité, notre classe dérive d'une autre classe même si c'est transparent.

Ajoutons la ligne liberatrice :

MonObjet.Free;

  • 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 :

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

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

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.

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é, faisons une classe qui contient des objets. Ajoutons une classe qui utilise des objet de la classe déjà ecrite.
Attention, je ne parle pas d'hériter de la classe !

unit TestLibererMaRamUnt;

interface

uses
   TestFrameWork,
   TestCaseDfmUnt; // Détection de fuite de memoire

type
   TMaClass = class
      caract1 : integer;
   end;

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

   TTestLibererMaRam = class(TTestCaseDfm) // derive 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 execution
  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.

unit TestLibererMaRamUnt;

interface

uses
   TestFrameWork,
   TestCaseDfmUnt; // Détection de fuite de memoire

type
   TMaClass = class
      caract1 : integer;
   end;

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

   TTestLibererMaRam = class(TTestCaseDfm) // derive 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 execution
  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.

  • 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 envoit à tous ses enfants l'ordre de libérer la mémoire. Ses enfants repercutent 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, prennez 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é sera différente.

Pour libérer les fiches depuis un gestionnaire d'évenement 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.

Reprennons 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 rajouté un ShowMessage pour faire une tempo. 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 :

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 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 (environ 5 sec)!');
   end;
end;

Notez que j'ai déclaré une variable local 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 monopiliser ont bien été rendue. 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é dans le cadre de ce tutorial. Elle contient un ensemble de fonction qui permet de stimuler et tester l'interface (la fiche).

105.4. Conclusion

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.

Retour en haut de la page


© Copyright par Tony BAHEUX. Tous droits de reproduction réservés.

Menu Principal


Liens

  • Brevet logiciel what a f@#$k !
  • Zone hors AGCS
 
Site créé le : 05-09-2003 - Mise à jour : 01-09-2005 - Tous droits réservés - Version beta : 0.2