Le framework DUnit, les tests sous Delphi

Tony Baheux

DUnit est un framework de test dérivé de Junit qui est son pendant pour le langage Java. Mais qu'est-ce 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 bogue (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'Extreme programming, une méthode de développement de logiciels basée sur les tests et d'autres points tout aussi innovants. Je n'aborderai pas l'Extreme Programming, si cela vous intéresse demandez à 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émoire que provoque votre application.

- Titre : Le framework DUnit, les tests sous Delphi

- Auteur : Tony Baheux

- Parution : 22 mai 2005

- Mise à jour : 26 décembre 2019

- Licence : 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.

Sommaire

- I. Téléchargement du framework

- II. Installation

- II-A. Préambule

- II-B. Installation pour tout projet

- II-C. Installation pour projet unique

- III. Utilisation de base de DUnit

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

- III-B. Quelques tests

- III-C. Exécuter les tests plusieurs fois

- IV. Utilisation 'Avancée' de DUnit

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

- IV-B. Étude de quelque cas avec Dfm

- IV-B-1. Les tableaux dynamiques

- IV-B-2. Les pointeurs

- IV-B-3. Les objets

- IV-B-4. Les objets d'objets

- IV-B-5. Les fiches

- V. DUnit, pratique

- V-A. Tester vos projets par DUnit

- VI. Conclusion

I - Téléchargement du framework

Tout d'abord, téléchargez 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 simples fichiers. Ce n'est pas une bibliothèque ou un composant. En termes clairs, 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, ce n’est pas compliqué à faire, un inconvénient il faut le faire à chaque fois !

Bien sûr 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 garder ces tests actifs !

Dans l'archive que vous allez récupérer, il y a un certain nombre de choses. 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ée).

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 copies (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 classes 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 égal à 2.

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 bogue 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 bogue 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érieures à 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 comparer 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 bogue.

II-B - Installation pour tout projet

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

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

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 cours. 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 :

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îtes de dialogue et autres interfaces 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 fuites 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 projets tests.

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

- Nouveau projet ;

- Retirez l'unité par défaut ;

- menu Nouveau, choisissez unité (sans forme).

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

Dans l'unité :

- ajoutez l'unité TestFrameWork ;

- déclarez une nouvelle classe, TTestPremierEssai, dérivant de la classe TtestCase ;

- ajoutez une première méthode, procédure de la classe, TestPremier ;

- dans la partie implémentation de TestPremier, tapez (ou copiez/collez 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 tests se fait par l'intermédiaire de méthodes qui n'acceptent 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écisez 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 a aussi la section initialization qui est parcourue à 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 :

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 :

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 si pour l'ordinateur 1+1 est bien égal à 2.

Afficher la source du projet et modifiez-la 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 :

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 dû à 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 écrite d'une autre façon qui permet de spécifier le nom de la suite de tests. RegisterTest('Ma suite de Tests', TTestPleinDeChose.Suite); par exemple affichera le libellé 'Ma suite de Tests' dans l'interface.

Une fois lancé, vous obtenez ceci :

et après exécution :

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.

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 amuser, 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 égal à 3),

Rouge pour une exception, si le fait qu'une égalité ne soit pas vérifiée peut être un problème, l'exception dénote d'une erreur plus sérieuse !

Dans la pratique, vous utiliserez ce genre de tests pour vérifier les initialisations et les affectations. Si vous développez des unités mathématiques (ou toutes fonctions réalisant des opérations), vous pourrez tester que vos nouveaux opérateurs calculent correctement (multiplication de matrices 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 amusantes.

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. Ces 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 :

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 :

procedure TTestStringList.TestStringListHabiter();
var
i : integer;
begin
// Vérifie le nombre d'éléments 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 remplie avec le bon nombre d'éléments
Check(_Fsl.Count = 50, 'Pas le bon nombre d''éléments');
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 utilisé 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 utilisé 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éments dans une liste. Ensuite nous ajoutons la vérification dans le tri de la liste parce qu'il serait idiot et source d'erreurs de trier 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és), il faut faire une classe par type de tests. 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ées mal initialisées). 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.

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 :

TestFramework.RegisterTest('repeat test',UnitRepeatTests);

Vous remarquerez que le nombre de répétitions 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.

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érences avec la répétition simple ce qui n'est pas plus mal :). Il faut aussi ajouter une ligne dans la section initialization :

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

Pas très original, 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 messages 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ées par tous vos projets tests présents et à venir, 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 n’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.

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.

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ée 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;
// 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.

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 hérité 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 fuites 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-ci :

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 soucier. Au passage vous aurez remarqué que l'erreur n'est pas détectée au bon endroit ce qui est 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 demandent quand ils doivent libérer la mémoire que leur projet a requise 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'autres objets ? Il y a aussi la question sur les fiches.

Une fois pour toutes, 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 voulue par l'intermédiaire de la commande SetLength. Alors une fois terminé, doit-on rendre cet espace demandé ?

Saisissons le code suivant :

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 n’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 cours 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 :

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ée alors qu'on a demandé une allocation (opérateur 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.

IV-B-3 - Les objets

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

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

MonObjet.Free;

IV-B-4 - Les objets d'objets

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

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

Je ne vous ferai 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 dynamiques. 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, essayez 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é, 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 !

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.

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éé 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éé dans le constructeur
// de TMaClassDeClass
Check(MonObjet.sousObjet.caract1 = 2, 'La valeur affectée à la création '
+ 'n''est pas retrouvée');
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éral 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ées.

Libération manuelle

Dans le cas où vous devez libérer manuellement les ressources, fiches ou composants 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 leurs composants, vous devez utilisez la méthode release. « Tous les gestionnaires 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énement 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-la 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 simples (dans le sens qu'ils ne doivent pas inclure de source d'erreurs).

En plus, chose amusante si vous êtes un peu trop pressé, le détecteur de fuites 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;
// obligé 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ée, toutes les ressources qu'elle a pu monopoliser ont bien été rendues. Je pense notamment au composant créé 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 fonctions qui permettent 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 censé évoluer à savoir l'extreme programming. Je ne dirai que deux choses à ce sujet.

L'extreme programming part des tests pour obtenir le code et pour plus d'infos faites appel à votre moteur de recherche préféré (un bon point de départ :  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é, 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 pointeurs, 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 fait briller ses yeux de convoitise :)

J'ai volontairement été moins didactique 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 forcément livrer le projet test aux clients finals. Je vous laisse trouver des noms parlants.

- 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-la 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és 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 vôtre.

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;
temps^.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ée 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'elles.

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 fuites de mémoire). En variable protected déclarez :

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

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 guillemets, car on n’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ée. 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 :

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.

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

procedure TTestPiles.TestEmpiler;
begin
_MaPile := PTNouvelle;
_MaPile := PTEmpiler(_MaPile, 'une valeur');
CheckEquals('une valeur', _MaPile.Elem , 'ça n’empile pas ! ');
_MaPile := PTDepiler(_MaPile); // Dépiler 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ée. N'oubliez pas de dépiler la pile sinon 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.

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ée par valeur ! Une pile locale est créée et vidée, mais l'original garde son état. Modifions la déclaration :

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 quelques rares élus, 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.

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.

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 ! À la place, nous utilisons TStringList qui hérite de TStrings.

Notez qu'on aurait pu (ou dû) mettre la création et la destruction dans un SetUp et un TearDown. Après les avoir testés 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ée, 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 fous, 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 correspondants.

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.

procedure TTestPrinc.TestFmCreate;
begin
fmPrinc := nil;
try
fmPrinc:= TfmPrinc.create(nil);
fmPrinc.ShowModal
finally
fmPrinc.Free;
// obligé 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 (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, fini le squat 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 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