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.
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.
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 ! |
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;
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.
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 :)
<< Chapitre précédent | Chapitre suivant >> |