DUnit: een 'extreme testing framework'Geplaatst door Marcel op 14-05-04
Testen en programmeurs... over het algemeen is dat niet zo'n erg goede combinatie. Programmeurs zijn meestal niet zo dol op testen en je eigen software testen is sowieso geen goed idee. Toch kan testen ook voor een programmeur heel zinvol zijn. Je opgeleverde software wordt er een stuk stabieler van, maar door je eigen software te testen ga ja vaak ook beter nadenken over de techniek en de functionaliteit. Maar de tests die je als programmeur uitvoert zijn anders dan de tests die een tester doet. Je tests moeten snel zijn en herhalend uitgevoerd kunnen worden zonder dat het je basistaak, het programmeren, beïnvloed. Een manier om dat te bereiken is door middel van unit testing. Een framework dat je daarbij kan helpen is DUnit.
Wat is DUnit?
DUnit is een framework waarmee je automatische tests kunt maken. Je kunt dat doen door een apart project te maken waarin je je tests uitvoert maar je kunt er ook voor kiezen om DUnit in je bestaande project op te nemen.
De volgende stap is dat je testclasses gaat maken waarin je je tests uitvoert. In die testclasse kun je meerdere methods maken die je tests uitvoeren en je kunt zoveel testclasses in een project maken als je maar wilt. In zo'n testmethod gebruik je vervolgens de Check() method om aan te geven of een test is gelukt of niet. Een Check() method kun je vergelijken met de Delphi Assert() method. De eerste parameter bepaalt of de test is gelukt, de tweede parameter geeft de foutomschrijving.
Een check ziet er dus bijvoorbeeld als volgt uit:
Check(DataSet.RecordCount > 0, 'Geen records gevonden');
Deze check geeft een melding 'Geen records gevonden' in de DUnit user interface als er geen records zijn gevonden.
DUnit gebruiken
Laten we eens een project gaan opzetten om één en ander te testen. Uiteraard beginnen we met het downloaden van DUnit, zie de links onderaan dit artikel. De download bevat de source van DUnit, een aantal voorbeeldprojecten en de documentatie. Je kopieert deze map naar de map waar je je Delphi projecten hebt staan. Ik ga in dit voorbeeld uit van D:\Projects\DUnit. Vervolgens maak je een nieuw Delphi project aan waarin we de tests gaan uitvoeren. Om een simpele test uit te voeren voeg je deze unit toe aan je project:
unit NLDStringUtils;
interface
function DeleteFirstChar(S: string): string;
implementation
uses
SysUtils;
{ Verwijderd het eerste karakter van een string }
function DeleteFirstChar(S: string): string;
begin
Delete(S, 1, 1);
Result := S;
end;
end.
Je kunt je voorstellen dat je een soortgelijke unit in je project hebt waar je alle procedures en functies die iets met strings doen hebt verzameld. Juist bij dit soort routines, die door je hele programma worden gebruikt, is het belangrijk dat ze het altijd doen. Als je hier wijzigingen in maakt moet je deze dan ook altijd testen. En juist dit soort tests zijn saai en worden vaak vergeten. Dus laten we voor deze unit een automatische test gaan maken. Dat doe je door de volgende unit aan je project toe te voegen:
unit NLDStringUtilsTST;
interface
uses
TestFrameWork;
type
TNLDStringUtilsTST = class(TTestCase)
published
procedure DeleteFirstCharTST;
end;
implementation
uses NLDStringUtils;
{ TNLDStringUtilsTST }
procedure TNLDStringUtilsTST.DeleteFirstCharTST;
begin
Check(DeleteFirstChar('www.NLDelphi.com') = 'ww.NLDelphi.com',
'Resultaat klopt niet');
end;
initialization
TestFramework.RegisterTest(TNLDStringUtilsTST.Suite);
end.
Je ziet dat er in deze unit een classe TNLDStringUtilsTST is gemaakt die erft van de DUnit classe TTestCase. In de classe heb ik een published procedure DeleteFirstCharTST gemaakt die de test uitvoert. Let op dat het belangrijk is dat je je methods published maakt. DUnit gebruikt RTTI om de procedures te vinden en dat werkt nou eenmaal alleen op published methods. In de Check() van DeleteFirstCharTST roep ik vervolgens DeleteFirstChar aan en controleer het resultaat. Tenslotte gebruik ik de initialization sectie om de testclasse die ik heb aangemaakt te registreren bij DUnit.
Voordat je het project kunt compileren moet je je DUnit map aan het search path van je project toevoegen. In mijn geval was dat de map D:\Projects\DUnit\Src. Uiteraard kun je deze ook in je environment options toevoegen, maar in verband met verschillende versies van projecten houd ik deze altijd zo schoon mogelijk.
Vervolgens moeten we Delphi vertellen dat we bij het starten van onze applicatie niet gewoon het MainForm willen laten zien, maar dat DUnit moet worden gestart. Dat doe je door je project source als volgt aan te passen:
program Project1;
uses
Forms,
Unit1 in 'Unit1.pas' {Form1},
{ Deze twee units moeten worden toegevoegd: }
TestFrameWork,
GUITestRunner,
NLDStringUtils in 'NLDStringUtils.pas',
NLDStringUtilsTST in 'NLDStringUtilsTST.pas';
{$R *.res}
begin
Application.Initialize;
// Application.CreateForm(TForm1, Form1);
{ Form1 is vervangen door deze regel: }
GUITestRunner.RunRegisteredTests;
Application.Run;
end.
En dan is het eindelijk tijd om de eerste test uit te voeren. Compileer en run je project en als alles goed is gegaan zie je het scherm van DUnit. Druk op F9 en je test wordt uitgevoerd. Dit is het resultaat:
Je ziet dat er een boom wordt opgebouwd van de tests die je hebt gedefinieerd en dat je een mogelijkheid hebt om een groep van tests uit te voeren. Je ziet hier ook dat de tests zijn uitgevoerd en dat we een resultaat hebben van 100%, alle tests zijn gelukt.
De tussenstand
Je ziet het, het definiëren van tests is echt supersimpel. We hebben (naast de installatie) de volgende stappen gevolgd:
- Een classe gemaakt die erft van TTestCase
- Een published procedure gemaakt die een routine test en het resultaat in een Check() zet
- De classe geregistreerd
- De projectsource aangepast zodat DUnit wordt gestart
Dat was alles! Als je meerdere tests wil uitvoeren kun je nieuwe methods toevoegen aan deze classe, maar je kunt ook een nieuwe classe maken en je methods daarin plaatsen. Vergeet dan niet dat je ook je nieuwe classe moet registreren.
Tests organiseren
Zodra je wat meer tests hebt gemaakt heb je al snel een manier nodig om deze te organiseren. Je zou bijvoorbeeld voor elke unit een aparte testclasse kunnen maken zodat je een mogelijkheid hebt om een hele unit in één keer te testen. In ons voorbeeld zou je dus alle routines uit NLDStringUtils in de testclasse TNLDStringUtilsTST kunnen zetten.
Een andere mogelijkheid is om meerdere testclasses in één groep te definiëren. Je kunt dan die hele groep testclasses in één keer uitvoeren. Je doet dat door bij de registatie van je classe als eerste een groep op te geven:
initialization
TestFramework.RegisterTest('String tests', TNLDStringUtilsTST.Suite);
Je classe komt dan in zijn eigen groep:
Een derde mogelijkheid is het dynamisch aanmaken van een verzameling tests. Je kunt dan runtime bepalen welke tests je wilt uitvoeren. Dat doe je door een public function te definieëren die de interface ITestSuite teruggeeft, de deze routine te registreren bij DUnit:
function StringTests: ITestSuite;
var
TestSuite: TTestSuite;
begin
TestSuite := TTestSuite.create('Eerste stringtests');
TestSuite.AddTest(TNLDStringUtilsTST.Suite);
Result := TestSuite;
end;
initialization
RegisterTest('String tests dynamisch', StringTests);
Zoals je ziet kun je zo op verschillende manieren je tests organiseren. Overigens mag je een testclasse ook meerdere malen registreren. Zo zou je dus technische en logische combinaties kunnen maken in groepen en zo bepalen wanneer welke test wordt uitgevoerd.
Begin test / eind test
Voor sommige tests kan het nodig zijn dat je een initialisatie nodig hebt, bijvoorbeeld objecten die je moet aanmaken of een databaseconnectie die moet worden geopend. Dat doe je door een override te maken van de SetUp method. Als je deze zaken ook weer wilt vrijgeven of afsluiten doe je dat in een override van de TearDown method.
Let wel op dat de SetUp en TearDown method voor en na elke method worden aangeroepen, het is dus niet te vergelijken met een constructor en een destructor. Die laatste worden immers alleen aangemaakt bij het aanmaken en vrijgeven van het object. SetUp en TearDown zijn daarom veiligere plaatsen om je initialisatie en afsluiting te doen dan de Create / Destroy.
Testen vanaf de commandline
Soms kan het handiger zijn om je tests in een console applicatie te draaien en de resultaten later te controleren. Daar hebben we bouwers van DUnit ook bij stil gestaan. Je maakt dan een nieuwe console applicatie en volgt dezelfde stappen als bij een gewoon project. Alleen in de projectsource roep je nu TextTestRunner aan in plaats van de GUITestRunner. Je project source ziet er dan als volgt uit:
program ConsoleTest;
{$APPTYPE CONSOLE}
uses
SysUtils,
TestFrameWork,
TextTestRunner,
NLDStringUtilsTST in 'NLDStringUtilsTST.pas',
NLDStringUtils in 'NLDStringUtils.pas';
begin
TextTestRunner.RunRegisteredTests;
end.
Als je dit project compileert kun je het vanaf de commandline starten. Ik heb een tweede test toegevoegd die niet lukt zodat we ook dat resultaat kunnen zien. Dat ziet er als volgt uit:
Tests herhalen
Het kan ook zinvol zijn een test te herhalen, bijvoorbeeld om een stresstest uit te voeren of de performance van een routine te meten als deze vaak achter elkaar wordt aangeroepen. Om dit te doen is de testextension TRepeatedTest gemaakt. Deze gebruik je om een hele test (alle methods van een testclasse) meerdere keren uit te voeren, of een specifieke method van die testclasse. Dat doe je door de TestExtensions unit aan je uses toe te voegen en vervolgens deze code te gebruiken:
{ Herhaling van alle methods van een testclasse }
function RepeatedTestSuite: ITest;
begin
Result := TRepeatedTest.Create(TNLDStringUtilsTST.Suite, 10);
end;
{ Herhaling van alle methods van een testclasse }
function RepeatedTestMethod: ITest;
begin
Result := TRepeatedTest.Create(
TNLDStringUtilsTST.Create('DeleteFirstCharTST'), 5);
end;
initialization
TestFramework.RegisterTest('Repeat suite', RepeatedTestSuite);
TestFramework.RegisterTest('Repeat method', RepeatedTestMethod);
Je tests worden dan als volgt geregistreerd:
Als je de test start wordt de hele testklasse 10 keer getest en de specifieke method 5 keer.
Conclusie
Als we zo door gaan wordt testen nog bijna leuk :). Unit testen is zeker een zinvolle bezigheid die je op lange termijn erg veel tijdswinst zal opleveren. Als je van alle routines die je bouwt ook direct een unit test maakt heb je meteen een goede testomgeving. Als je zorgt dat je deze tests dan ook regelmatig draait, bijvoorbeeld in een commandline applicatie elke nacht op je 'latest build', weet je ook zeker dat er geen fouten onstaan in je applicatie door wijzigingen die je per ongeluk maakt. En DUnit is zeker een goed framework dat je helpt de tests op te zetten en overzichtelijk te houden.
Links
DUnit, An Xtreme testing framework for Borland Delphi programs |