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.
Pour le télechargement et surtout l'installation, reportez-vous au chapitre précédent : 105. Dunit, Installation
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.
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é :
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.
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 :
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 :
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
|
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 ! |
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 :)
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;
procedure TTestCaseDfm.setUp();
begin
inherited; // important herité en premier l'ancien code avant de mettre le nouveau !
detecteurFuiteMemoire := TDetecteurFuiteMemoire.Create;
end;
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.
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 !
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 :)
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. |
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.
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;
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.
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).
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.