Da ich mich vor kurzem etwas mit DUnit beschäftigt hab, dachte ich mir, dass könnte vielleicht noch jemanden interessieren.
DUnit ist Test-Framework, das vor allem im Test-First-Ansatz verwendet wird(XP), es ist aber genauso möglich
Unit-Tests im nachhinein zu schreiben.
Hier kann mans downloaden:
https://sourceforge.net/projects/dunit/
Ich möchte DUnit mal grundsätzlich anhand eines einfachen Beispiels beschreiben.
Begonnen wird mit einem leeren Projekt, dem wir zuerst die Units GUITestRunner und Testframework hinzufügen. Beide Units befinden sich im src-Ordner von DUnit.
Die Zeilen:
Delphi-Quellcode:
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
Werden entfernt und durch diese
TGUITestRunner.RunRegisteredTests;
ersetzt.
Als kleines Beispiel dient eine simple Wörterbuchklasse. Im Test-First-Ansatz läuft die Entwicklung so ab, dass zuerst das Verhalten der Klasse durch Testfälle festgelegt und anschließend so implementiert wird, dass die Tests erfolgreich durchlaufen.
Zuerst werden 2 Units hinzugefügt. Eine beinhaltet die Klasse, die getestet werden soll und eine die Testklasse. Wobei ich erstere Dictionary und zweitere DictionaryTest genannt habe.
In der DictionaryTest-
Unit erstellen wir die TestKlasse TDictionaryTest, die von TTestCase abgeleitet ist.
So sieht der Interface-Abschnitt dieser
Unit aus:
Delphi-Quellcode:
uses TestFramework, Dictionary;
type TDictionaryTest = class(TTestCase)
private
FDictionary : TDictionary;
protected
procedure SetUp; override;
procedure TearDown; override;
published
procedure testCreation;
procedure testOneTranslation;
procedure testTwoTranslation;
procedure testTranslationWithTwoEntries;
end;
Die Membervariable FDictionary ist ein Objekt der Klasse, die wir testen wollen. Die beiden Prozeduren SetUp und TearDown werden vom TestFrameWork zur Verfügung gestellt. SetUp wird immer ausgeführt bevor eine Testprozedur durchgeführt wird und TearDown, wenn die Testprozedur durchgeführt wurde. Standardmäßig wird bei einem Test jede parameterlose published-Prozedur, die mit test beginnt ausgeführt. Um diese Testklasse zu registrieren, muss folgendes in den Initialization-Abschnitt geschrieben werden.
Delphi-Quellcode:
initialization
RegisterTest('Dictionary', TDictionaryTest.Suite);
end.
Ansonsten kennt DUnit die Testklasse nicht.
In SetUp wird jetzt unser Testobjekt erzeugt und in TearDown wieder freigegeben.
Delphi-Quellcode:
procedure TDictionaryTest.SetUp;
begin
inherited;
FDictionary := TDictionary.Create;
end;
procedure TDictionaryTest.TearDown;
begin
inherited;
FDictionary.Free;
end;
Unser erster Test überprüft lediglich, ob das Wörterbuch nachdem es erzeugt wurde auch leer ist:
Delphi-Quellcode:
procedure TDictionaryTest.testCreation;
begin
Check(FDictionary.IsEmpty,'Dictionary muss leer sein');
end;
Mit Check wird ein Ausdruck auf true überprüft. Schlägt die Überprüfung fehl, wird der String im 2ten Parameter (ist optional) an der Oberfläche ausgegeben.
Nun können wir die DictionaryKlasse soweit implementieren, dass der Test erfolgreich durchläuft.
Delphi-Quellcode:
type
TDictionary = class
function IsEmpty: boolean;
end;
function TDictionary.IsEmpty: boolean;
begin
result := true;
end;
Zu dem Zeitpunkt reicht diese Implementierung um einen erfolgreichen Test zu gewährleisten. Also ist die Klasse noch nicht ausreichend getestet. Deshalb testen wir zunächst eine Übersetzung:
Delphi-Quellcode:
procedure TDictionaryTest.testOneTranslation;
var Translation : string;
begin
FDictionary.AddTranslation('Buch', 'book');
Check(not FDictionary.IsEmpty, 'Dictionary darf nicht leer sein');
Translation := FDictionary.GetTranslation('Buch');
CheckEquals('book', Trans, ‘Übersetzung Buch’);
end;
Es wird also eine Übersetzung hinzugefügt, überprüft, ob das Wörterbuch noch leer ist und die Übersetzung geholt. Mit CheckEquals wird die Richtigkeit der Übersetzung geprüft. CheckEquals überprüft, ob 2 Parameter übereinstimmen. Die Implementierung der zu testenden Klasselasse ich hier mal raus, weil sie wieder nicht vollständig ist. Beim jetzigen Stand muss IsEmpty einfach false zurückliefern, nachdem AddTranslation einmal aufgerufen wurde. GetTranslation müsste einfach 'book' zurückliefern.
Also schreiben wir noch einen Test, der 2 Übersetzungen testet und einen der einen deutschen Begriff doppelt belegt.
Delphi-Quellcode:
procedure TDictionaryTest.testTwoTranslation;
begin
FDictionary.AddTranslation('Buch', 'book');
FDictionary.AddTranslation('Auto', 'car');
Check(not FDictionary.IsEmpty, 'Dictionary darf nicht leer sein');
CheckEquals('book', FDictionary.GetTranslation('Buch'),'Übersetzung Buch');
CheckEquals('car', FDictionary.GetTranslation('Auto'),'Übersetzung Auto');
end;
procedure TDictionaryTest.testTranslationWithTwoEntries;
begin
FDictionary.AddTranslation('Buch', 'book');
FDictionary.AddTranslation('Buch', 'volume');
CheckEquals('book, volume', FDictionary.GetTranslation('Buch'));
end;
Damit diese Testfälle erfolgreich durchlaufen erweitert sich das Interface von TDictionary auf folgendes:
Delphi-Quellcode:
type
TDictionary = class
private
FEntries : TStringList;
public
constructor Create;
destructor Destroy;
function IsEmpty: boolean;
procedure AddTranslation(AGerman, ATranslation : string);
function GetTranslation(AGerman : string): string;
end;
Zum Speichern der Einträge hab ich mich für eine Stringliste entschieden. Create und Destroy sind hier lediglich zum Erzeugen bzw. Freigeben von FEntries verantwortlich.
Hier noch die ziemlich einfach gehaltenen Methoden:
Delphi-Quellcode:
function TDictionary.IsEmpty: boolean;
begin
result := FEntries.Count = 0;
end;
procedure TDictionary.AddTranslation(AGerman, ATranslation: string);
var OldStr : string;
idx : integer;
begin
AGerman := KillSp(NoSpace(AGerman));
ATranslation := KillSp(NoSpace(ATranslation));
OldStr := FEntries.Values[AGerman];
if OldStr = '' then begin
FEntries.Add(AGerman + '=' + ATranslation);
end else begin
idx := FEntries.IndexOf(AGerman + '=' + OldStr);
FEntries[idx] := AGerman + '=' + OldStr + ', ' + ATranslation;
end;
end;
function TDictionary.GetTranslation(AGerman: string): string;
begin
result := FEntries.Values[AGerman];
end;
Und das Wörterbuch funktioniert einmal und ist auch durch Tests abgesichert.
Allerdings existiert noch keine Testprozedur, die irgendwelche Ausnahmen testet. Was passiert, wenn es leere Einträge gibt? In der Praxis möchte man ziemlich sicher die Übersetzungen irgendwo speichern und wieder laden können. Dieser Vorgang muss getestet werden. Was passiert wenn in der Datei fehlerhafte Einträge stehen? Wenn man alles testet, was einem einfällt kann dieser Prozess ziemlich langwierig werden. Die Kunst ist es genau soviel zu testen wie man muss.
Hoffe, das Tutorial war einigermaßen Verständlich und ich habe mindestens einem damit geholfen.

Das war immerhin mein erstes Tutorial.
grüße, daniel