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

Chapitre 105

105. Dunit
Chapitre 105  :  Dunit, Pratique

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, Pratique
105.1. Introduction

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 serieux ! Bon d'accord, je plaisantes. Je vais essayer de vous montrer l'intêret de Dunit sur un projet qui pourrait être réel. Remarquez que je n'utiliserai pas Dunit dans le contexte où il censait évoluer à savoir l'extreme programming. Je ne dirais que deux choses à ce sujet. L'extreme programming part des tests pour obtenir le code et pour plus d'info faite appel à votre moteur de recherche préféré (un bon point de départ : Xp-france.net).
Autre point important, ne tardait pas trop à mettre en place vos test. Si vous pensez coder les test une fois le projet terminée, vous allez vite vous découragez. 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 ditactiques, je mets les tests une fois l'unité complètement terminée mais je les attaques une à la fois. Bon trêve de blabla et au boulot.

105.2. Tester vos projets par Dunit

Nous allons maintenant voir comment utiliser Dunit pour contrôler votre projet. J'ai un peu cherché quel genre d'exemple, je pourrais bien vous proposer et je suis tombé sur l'exercice de programmation des piles du guide. Dés que j'ai vu la présence de pointeur, j'ai su que c'était un bon exemple (enfin je l'espère). Le mot pointeur est celui qui effrait le plus le programmeur en herbe, ou lui font briller ses yeux de convoitise :)
J'ai volontairement été moins dictatique sur cette partie histoire de vous apprendre à voler de vos propres ailes. J'annonce ce que vous devez faire et ensuite ce que vous devriez obtenir.

  • Prépararons d'abord les repertoires pour ce nouveau projet. Un de base et un sous repertoire pour accueillir le projet-test. Cette façon de faire permet de bien séparer les choses, en effet vous ne souhaitez pas forcement livrer le projet-test aux clients finaux. Je vous laisse trouver des noms parlant.
  • Créez le projet dans le repertoire 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 repertoire 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 projets si vous n'avez pas choisi de modifier l'environnement.
  • Enregistrez l'unité sous le nom de TestPilesTabUnt.pas.
  • Ajoutez l'unité PilesTabUnt et déclarez là dans la section uses de l'unité TestPilesTabUnt.
  • Enregistrez le projet-test.

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

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

unit PilesTabUnt;

interface

uses
  Classes;

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

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

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

implementation

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

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

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

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

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

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

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

end.

Tout ça c'est bien joli mais quel test devons nous mettre en place ? La réponse est facile. La pile va être manipuler uniquement par les fonctions, c'est donc toutes les fonctions qu'il faut tester et vérifier qu'elle font bien ce qu'on attends d'elle.
Ajoutez PilesTabUnt dans la clause uses si c'est pas déjà fait. Puis, déclarez une nouvelle classe test TTestPiles qui dérive de TTestCaseDfm (pour inclure la détection de fuite de mémoire). En variable protected déclarez _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.
Partons 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 a peu près ça :

unit TestPilesTabUnt;

interface

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

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

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

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

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

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

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

Si on regarde de plus près le TestEmpiler, on s'aperçoit qu'on utilise la fonction PTDepiler. Heureusement, nous l'avons déjà tester. N'oubliez pas de dépiler la pile si non vous provoquerez une déperdition de Ram. La fonction PTEmpiler fait une allocation de mémoire (operateur new), il nous faut donc la libérer (dipose 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 perspicases d'entre vous (ou les plus réveillés) auront remarqué que la pile est passé par valeur ! Une pile local est créée et vidée mais l'original garde son état.
Modifions la déclaration : PTDetruire(var Pile: PPileElem);
Voilà le problème est résolu. Imaginez le temps passé à chercher ce genre d'erreur sans le Dunit. Car, à part peut être quelque rare élu, peu d'entre vous ont dû voir l'erreur avant que je vous demande de la chercher.

IMPORTANT :

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

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 élements');
   CheckEquals('une valeur', Sortie[2], 'Erreur d''élements 1');
   CheckEquals('deux valeurs', Sortie[1], 'Erreur d''élements 2');
   CheckEquals('trois valeurs', Sortie[0], 'Erreur d''élements 3');

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

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

Information :

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

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

Comme je suis trop bon avec vous, ci dessous le code de l'interface, c'est à dire de l'unité PrincFrm. Pour éviter de se fatiguer, ajouter cette unité au projet-test car comme pour l'unité PilesTabUnt, nous allons pouvoir travailler sur son code sans basculer d'un porjet à 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'évenement correspondant.

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 clauses 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;
     // obliger pour le test, car il y a un temp de latence pour le free ou
     // release.
     // Si on ne met pas d'attente, on aurait une erreur de détection dfm !
     showmessage('attente : ne cliquez pas trop vite (5~10 sec) !');
   end;
end;

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

105.4. Conclusion

Vous auriez tort de vous priver de cet outil de test, tellement il est simple à mettre en place et vous évitera de vous prendre la tête ou celle de vos clients pendant des heures. En plus le détecteur de fuite de mémoire est vraiment performant, finit le squatte des ressources comme c'est trop souvent le cas. Une dernière chose, chaque test indique le temps qu'il a mis pour s'exécuter. Vous pouvez du coup pointer le doigt sur les parties lente de vos programmes et tenter de les optimiser.
En tout cas j'espère vous avoir convaincu :)

Retour en haut de la page

<< Chapitre précédent Chapitre suivant >>

© 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