|
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: ![]() 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:
Werden entfernt und durch diese
Application.Initialize;
Application.CreateForm(TForm1, Form1); Application.Run; 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:
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.
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;
Delphi-Quellcode:
Ansonsten kennt DUnit die Testklasse nicht.
initialization
RegisterTest('Dictionary', TDictionaryTest.Suite); end. In SetUp wird jetzt unser Testobjekt erzeugt und in TearDown wieder freigegeben.
Delphi-Quellcode:
Unser erster Test überprüft lediglich, ob das Wörterbuch nachdem es erzeugt wurde auch leer ist:
procedure TDictionaryTest.SetUp;
begin inherited; FDictionary := TDictionary.Create; end; procedure TDictionaryTest.TearDown; begin inherited; FDictionary.Free; end;
Delphi-Quellcode:
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.
procedure TDictionaryTest.testCreation;
begin Check(FDictionary.IsEmpty,'Dictionary muss leer sein'); end; Nun können wir die DictionaryKlasse soweit implementieren, dass der Test erfolgreich durchläuft.
Delphi-Quellcode:
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:
type
TDictionary = class function IsEmpty: boolean; end; function TDictionary.IsEmpty: boolean; begin result := true; end;
Delphi-Quellcode:
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.
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; Also schreiben wir noch einen Test, der 2 Übersetzungen testet und einen der einen deutschen Begriff doppelt belegt.
Delphi-Quellcode:
Damit diese Testfälle erfolgreich durchlaufen erweitert sich das Interface von TDictionary auf folgendes:
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;
Delphi-Quellcode:
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.
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; Hier noch die ziemlich einfach gehaltenen Methoden:
Delphi-Quellcode:
Und das Wörterbuch funktioniert einmal und ist auch durch Tests abgesichert.
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; 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. ![]() grüße, daniel
Testen ist feige!
|
Delphi XE6 Enterprise |
#2
Testen von vererbten Klassen, Exceptions
Dieses Beispiel ist aus einem Java-Buch portiert. Angenommen wir haben eine Klasse TBook und eine fertige Testklasse dazu. Das Buch wird mit seinem Titel, dem Einkaufspreis(FWholeSale) und dem empfohlenen Preis (FRecommended) erzeugt. Das Buch kann jetzt im Rahmen dieser beiden Preise verkauft werden. Nach dem Verkauf kann der Profit abgefragt werden.
Delphi-Quellcode:
Die Testklasse sieht wie folgt aus:
type
TBook = class private FName : string; FWholeSale, FRecommended, FSoldFor : double; public constructor Create(AName : string; AWholeSale, ARecommended : double); function GetName: string; function GetWholeSalePrice: double; function GetRecommendedPrice: double; procedure Sell(APrice : double); virtual; function GetSoldFor: double; function Profit: double; virtual; end;
Delphi-Quellcode:
Der Verkaufspreis des Buches darf nicht kleiner sein als WholeSalePrice (wird durch testSellBelowWholeSalePrice geprüft) und auch nicht größer als RecommendedPrice (-->testSellAboveRecommendedPrice) ansonsten wird eine EPriceOutOfBounds-Exception geworfen.
type
TBookTest = class(TTestCase) private FBook : TBook; FName : string; FWholeSale, FRecommended : double; protected procedure SetUp; override; procedure TearDown; override; public constructor Create(MethodName: String); override; //Hier werden FName, FWholeSale & FRecommended gesetzt published procedure testCreation; procedure testSellAtRecommendedPrice; procedure testSellAtWholeSalePrice; procedure testSellBelowWholeSalePrice; procedure testSellAboveRecommendedPrice; end; Wir nehmen jetzt mal an, diese Klassen sind bereits geschrieben. Jetzt wollen wir ein Fixpreisbuch (TFixedPriceBook) einführen, dass sich nur zum Recommended-Price verkaufen lässt. Diese Klasse wird von TBook abgeleitet. Die meisten Test-Prozeduren aus der BookTest-Klasse können gleich bleiben, nur ist jetzt kein Verkauf zum Einkaufspreis (Wholesaleprice) mehr möglich. Also muss die testSellAtWholeSalePrice überschrieben werden. Bisher wurde in Setup das Buch wie folgt erzeugt:
Delphi-Quellcode:
Diese Funktion kann nicht einfach ohne Aufruf von inherited in TFixedPriceBookTest Überschrieben werden. Mit Aufruf von inherited wird jedoch ein Objekt der falschen Klasse erzeugt. Also überlassen wir die Erzeugung des zu testenden Objektes einer Funktion die ohne weiteres überschrieben werden kann.
procedure TBookTest.SetUp;
begin inherited; FBook := TBook.Create(FName, FWholesale, FRecommended); end;
Delphi-Quellcode:
Außerdem wird testSellAtWholeSalePrice aufgrund des geänderten Verhaltens zum Überschreiben markiert.
procedure TBookTest.SetUp;
begin inherited; FBook := CreateBook(FName, FWholesale, FRecommended); end; function TBookTest.CreateBook(AName: string; AWholeSale, ARecommended: double): TBook; begin result := TBook.Create(AName, AWholeSale, ARecommended); end;
Delphi-Quellcode:
Nun leiten wir die Klasse TFixedPriceBookTest von TBookTest ab.
...
procedure testSellAtWholeSalePrice; virtual; ..
Delphi-Quellcode:
Hier testen wir jetzt den Verkauf unter dem fixen Preis und den Profit vor dem Kauf (steht immerhin schon fest). Außerdem ist nun kein Verkauf zum Einkaufspreis mehr möglich.
type
TFixedPriceBookTest = class(TBookTest) protected function CreateBook(AName: String; AWholeSale: Double; ARecommended: Double): TBook; override; published procedure testSellAtWholeSalePrice; override; procedure testSellBelowRecommendedPrice; procedure testProfitBeforeSale; end; Hier sehen wir schon, dass sich die Klasse TFixedPriceBook in 2 Methoden anders verhalten muss.
Delphi-Quellcode:
Der Profit kann vor dem Verkauf bereits abgefragt werden und der Verkauf ist nur noch zum RecommendedPrice möglich.
type
TFixedPriceBook = class(TBook) public procedure Sell(APrice: Double); override; function Profit: Double; override; end; Zuerst muss einmal ein Objekt von TFixedPriceBook erzeugt werden.
Delphi-Quellcode:
An den fixen Daten in Create sehen wir welchen Profit wir haben
function TFixedPriceBookTest.CreateBook(AName: String; AWholeSale,
ARecommended: Double): TBook; begin result := TFixedPriceBook.Create(AName, AWholeSale, ARecommended); end;
Delphi-Quellcode:
Anschließend versuchen wir, zum Einkaufspreis und unter dem fixierten Preis zu verkaufen. Beides muss mit einer EPriceOutOfBounds-Exception fehlschlagen.
constructor TBookTest.Create(MethodName: String);
begin inherited; FName := 'Ein Testbuch'; FRecommended := 12.0; FWholeSale := 10.0; end; procedure TFixedPriceBookTest.testProfitBeforeSale; begin CheckEquals(2.0, FBook.Profit, 0.00); end;
Delphi-Quellcode:
Tritt hier bei Sell keine EPriceOutOfBounds-Exception auf wird mit Fail eine ETestFailure-Exception ausgelöst, die den Test den Test mit der Meldung 'EPriceOutOfBounds-Exception erwartet' fehlschlagen lässt.
procedure TFixedPriceBookTest.testSellAtWholeSalePrice;
begin try FBook.Sell(FWholeSale); Fail('EPriceOutOfBounds-Exception erwartet'); except on EPriceOutOfBounds do end; end; procedure TFixedPriceBookTest.testSellBelowRecommendedPrice; begin try FBook.Sell(FRecommended - 0.01); Fail('EPriceOutOfBounds-Exception erwartet'); except on EPriceOutOfBounds do end; end; Werden beim Testen absichtlich Exceptions ausgelöst, empfiehlt sich das Stoppen bei Sprach-Exceptions abzuschalten. Kann sonst ziemlich nervig werden. Jetzt fehlt noch die Implementierung von TFixedPriceBook, die die Tests erfolgreich durchlaufen lässt. Der Verkauf sollt nur zum Recommended-Preis durchgeführt werden, ansonsten wird eine Exception geworfen.
Delphi-Quellcode:
Da ein Buch den Fixpreis und den Einkaufspreis bereits im Konstruktor übergeben bekommt, kann der Profit immer abgerufen werden.
procedure TFixedPriceBook.Sell(APrice: Double);
begin if APrice = FRecommended then begin inherited; end else begin raise EPriceOutOfBounds.Create('Preis verletzt erlaubte Grenzen'); end; end;
Delphi-Quellcode:
Die Tests sollten jetzt zu 100% durchlaufen.
function TFixedPriceBook.Profit: Double;
begin Profit := FRecommended - FWholeSale; end; Ich hoffe, meine Gedankengängen konnte gefolgt werden. ![]() grüße, daniel
Daniel
|
![]() |
Ansicht |
![]() |
![]() |
![]() |
ForumregelnEs ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.
BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus. Trackbacks are an
Pingbacks are an
Refbacks are aus
|
|
Nützliche Links |
Heutige Beiträge |
Sitemap |
Suchen |
Code-Library |
Wer ist online |
Alle Foren als gelesen markieren |
Gehe zu... |
LinkBack |
![]() |
![]() |