Page 1 of 2 1 2 LastLast
Results 1 to 15 of 16

Thread: Geautomatiseerde Delphi Unit-testen voor hobbyisten.

  1. #1
    Delphi & OO in Vlaanderen SamWitse's Avatar
    Join Date
    Sep 2007
    Location
    Brussel
    Posts
    833

    Post Geautomatiseerde Delphi Unit-testen voor hobbyisten.

    Geautomatiseerde Delphi Unit-testen voor hobbyisten.

    Verbeter op een realistische wijze de kwaliteit van je programmaÔÇÖs. Voorkom ergerlijke bugs, ergens ÔÇÿdiepÔÇÖ in je code. Maak nooit per ongeluk een bug terwijl je je code elders aanpast.
    En toch moet je maar heel weinig méér werk verrichten dan nu.
    Maak wat tijd om dit artikel te lezen, en stap in de club der Delphi unit-testers.
    In deze rubriek vind je elders reeds een artikel van Marcel, waarin het principe van unit-testen helder wordt uitgelegd. Hier wil ik verder ingaan op het gebruik, het nut en een aantal misverstanden van unit-testen.

    Waarom?
    'Natuurlijk test ik elke Delphi-klasse grondig!' is de minst gehoorde uitspraak bij programmeurs. Zeker bij hobbyisten. We zijn al o-zo-blij als onze klasse zonder fouten compileert. Als hij dan nog doet wat hij moet doen, dan is de klasse af. Op naar het volgend probleem!
    Wel akelig als je tijdens het debuggen ergens nog een foutje vindt in een klasse die je al lang geleden geschreven hebt, en je die klasse als 'goed werkend' had beschouwd. Nog erger wordt het als methods die altijd al gewerkt hebben, plots niet meer werken.
    In een ideale wereld beschrijf je op voorhand exact wat je nieuwe klasse moet kunnen, wat de verwachte resultaten zijn, welke exceptions er moeten opgeroepen worden. De projectmethode eXtreme Programming, afgekort tot XP, bepaalt zelfs dat je eerst je testprogrammaÔÇÖs volledig moet schrijven, vooraleer je één letter code begint te schrijven. In werkelijkheid doe je zowat het tegenovergestelde, maak je een klasse volgens je 'gevoel', en maak je je methods volgens hetgeen je ongeveer denkt wat het moet doen, en welke gevallen berekend of verwerkt moeten worden.
    Kunnen we niet een 'gulden middenweg' vinden die betere resultaten levert dan het boeltje dat we er nu van maken, maar dat niet een hele rompslomp van testprogrammaÔÇÖs, analysedocumenten, documentatie en help-paginaÔÇÖs vereisen? Jawel: Unit-testen.

    Wat?
    Voor Delphi bestaat er een ÔÇÿframeworkÔÇÖ dat DUnit heet. Dit is een open source programma dat testen uitvoert op jouw klasse(n). Marcel heeft hier reeds uitvoerig over geschreven in zijn artikel 'DUnit: een 'extreme testing framework''. Je kunt DUnit vinden op http://dunit.sourceforge.net/. Je moet dus geen eigen testprogramma meer schrijven, maar enkel een testklasse. Die testklasse lijkt wat raar, maar is heel eenvoudig te schrijven. Met het volgend voorbeeld wordt het snel duidelijk:
    Stel, je maakt een klasse voor namen van personen, TNaam:

    delphi Code:
    1. TNaam = class(TObject)
    2. private
    3.   FVoornaam: string ;
    4.   FMiddenletter: string;
    5.   FFamilienaam: string ;
    6.   function VolledigeNaam: string ;
    7. public
    8.   constructor Create;
    9.   destructor Destroy;
    10.   property Voornaam: string read FVoornaam write FVoornaam;
    11.   property Middenletter: string read FMiddenletter write FMiddenletter;
    12.   property Familienaam: string read FFamilienaam write FFamilienaam;
    13.   property VolledigeNaam: string read Get VolledigeNaam;
    14. end;
    15.  
    16. implementation
    17.  
    18. function TNaam.VolledigeNaam: string ;
    19. begin
    20.   if FVoornaam = ÔÇÿÔÇÖ then
    21.     result := FFamilienaam
    22.   else
    23.     if FMiddenletter = ÔÇÿÔÇÖ then
    24.       result := FVoornaam + ÔÇÿ ÔÇÿ + FFamilienaam
    25.     else
    26.       result := FVoornaam + ÔÇÿ ÔÇÿ + FMiddenletter + ÔÇÿ ÔÇÿ + FFamilienaam
    27. end;
    Dit is geen moeilijke klasse, waarvoor je misschien geen unit-testen gaat maken (zie verder), maar die we hier gebruiken als voorbeeld.
    Wat verwacht je van de property VolledigeNaam? Duidelijk 3 mogelijkheden, zoals:
    'Witse' -> ÔÇÿWitseÔÇÖ
    'Paris' 'Hilton' -> 'Paris Hilton'
    'John' 'F' 'Kennedy' -> 'John F Kennedy'
    Hiermee heb je eigenlijk al 3 testcases beschreven. Je zult zien dat je niet veel meer moet doen om gebruik te kunnen maken van DUnit. Schrijf je ÔÇÿverwachtingenÔÇÖ in de volgende statements:

    delphi Code:
    1. Ik := TNaam.Create;
    2. Ik.Familienaam := ÔÇÿWitseÔÇÖ ;
    3. check (Ik.VolledigeNaam =ÔÇÖWitseÔÇÖ,ÔÇÖDe volledige naam van Ik is niet ÔÇÿWitseÔÇÖ) ;
    4. Ik.Free;
    5.  
    6. MyNaam := TNaam.Create ;
    7. MyNaam.Voornaam := ÔÇÿJohnÔÇÖ ;
    8. MyNaam.Middenletter := ÔÇÿFÔÇÖ ;
    9. MyNaam.Familienaam := ÔÇÿKennedyÔÇÖ ;
    10. check ( MyNaam.VolledigeNaam = ÔÇÿJohn F KennedyÔÇÖ,ÔÇÖVolledigeNaam geeft niet het gewenste resultaat voor John F KennedyÔÇÖ);
    11. MyNaam.Free;
    12.  
    13. MyAndereNaam := TNaam.Create ;
    14. MyAndereNaam.Voornaam := ÔÇÿParisÔÇÖ ;
    15. MyAndereNaam.Familienaam := ÔÇÿHiltonÔÇÖ ;
    16. check (MyAndereNaam.VolledigeNaam = ÔÇÿParis HiltonÔÇÖ,ÔÇÖVolledigeNaam geeft niet het gewenste resultaat bij Paris HiltonÔÇÖ);
    17. MyAndereNaam.Free;
    Dit is wat je in je testscenario moet schrijven. Veel cutÔÇÖnÔÇÖpaste-werk, en niet moeilijk dus.

    Waarom niet?
    Er zijn genoeg redenen om geen unit-testen te schrijven. Ik heb ze gehoord van collegaÔÇÖs die het niet zagen zitten om unit-test te maken (n??g meer werk). Ik heb deze argumenten ook gebruikt om mijzelf te overtuigen dat unit-testen niet nodig of nuttig zouden zijn. Ik ben er echter op terug gekomen. Unit-testen zijn wél nuttig. Niet in alle gevallen, maar toch in vele. Hier volgen de redenen waarom je unit-testen niet zou gebruiken, met telkens een kritische maar eerlijke bespreking.
    Te veel werk. Kleine programmaÔÇÖs behoeven geen uitgebreide testen. Eerst testen en dan ontwikkelen? Ik weet niet hoe ik moet testen. Veel te ingewikkeld. Goed voor grote projecten, in grote bedrijven, niet voor eigen ontwikkelingen. Is alleen nodig als je code constant wijzigt. Ik kan geen mooi afgelijnde units maken. MijnÔÇÖomgevingÔÇÖ is té complex om op te zetten voor een test. Ik kan geen GUI testen. Ik kan geen database-afhankelijke routines testen. Ik kan niet alle mogelijke gevallen testen: dat zijn er miljoenen! Heel simpele dingen moeten niet getest worden. Het ziet er niet naar uit dat dit te weerleggen is. Toch hoop ik je hier te kunnen overtuigen van de noodzak van unit-testen.

    Te veel werk.
    Het maken van je allereerste unit-test vergt een half uurtje werk. Daarna gaat het zelfs sneller. Dus veel werk is het niet. Te veel werk zeker niet. Maar win je er ook tijd mee? Echte fans zullen zeggen: 'vast en zeker!'. Volgens mij ga je geen tijd winnen in het voorbeeld met de klasse TNaam. Andere klassen ga je hoe dan ook even testen, al is het enkel met de debugger. Of je schrijft tussenresultaten weg in een logfile, je maakt een popup met tussenresultaten, of je maakt zelfs een geheel eigen test-applicatie. Die tijd kun je nu besparen door een unit-test te schrijven. Meer nog, je kunt je unit-test later terug gebruiken zonder zoekwerk of aanpassingswerk. Ben je na een tijdje niet meer zo zeker of je klasse nog werkt? Voer de unit-test uit en je bent weer zeker.
    Zijn het moeilijke klassen en methods die later nog veel wijzigingen ondergaan, dan win je zeker veel tijd met unit-testen, omdat die zekerheid geven over je aanpassingen, zonder dat je enig bijkomend werk moet verrichten.
    Dus neen, veel tijd vergt zoÔÇÖn unit-test niet, maar je wint alle tijd vrij snel terug.

    Kleine programmaÔÇÖs behoeven geen uitgebreide testen.
    Dat is waar. Schrijf geen unit-testen voor een klasse die (bijna) geen code heeft. Ook kleine of eenmalig te gebruiken programmaÔÇÖs behoeven geen unit-testen. Unit-testen zijn niet verplicht bij al je programmaÔÇÖs. Ze geven je wel zekerheid in het geval je het effect van een method niet direct in je programma ziet, en in het geval je meerdere testcases hebt.
    Gaat het om zeer eenvoudige methods of eenmalige programmaÔÇÖs dan hoef je die niet per sé te testen.

    Eerst testen en dan ontwikkelen?
    Dat is de bedoeling. Maar je hoeft deze regel voor mijn part niet té strikt op te volgen. Normaal heb je op zÔÇÖn minst een ÔÇÿglobaal ideeÔÇÖ over wat je klasse en je methods moeten doen. Wel, dat kun je dat beschrijven in je checkÔÇÖs. Dit laat je verder toe om wat diepgaander na te denken, en exact te vertellen wat je methods moeten doen, en in welke gevallen ze dus juist zullen werken en geldige waarden teruggeven. ÔÇÿMijn method GetVolledigeNaam moet de voornaam, een spatie, de middelletter, een spatie en de familienaam teruggevenÔÇÖ. Deze zin hoef je niet voluit in een document te schrijven, maar schrijf je als de volgende check in je unit-test:

    delphi Code:
    1. MyNaam := TNaam.Create ;
    2. MyNaam.Voornaam := ÔÇÿJohnÔÇÖ ;
    3. MyNaam.Middenletter := ÔÇÿFÔÇÖ ;
    4. MyNaam.Familienaam := ÔÇÿKennedyÔÇÖ ;
    5. check ( MyNaam.VolledigeNaam = ÔÇÿJohn F KennedyÔÇÖ,ÔÇÖVolledigeNaam geeft niet het gewenste resultaat voor John F KennedyÔÇÖ);
    Weet je ondertussen nog andere interessante gevolgen van de method VolledigeNaam, schrijf ze dan ook op als test. Bijvoorbeeld:

    delphi Code:
    1. MyAndereNaam := TNaam.Create ;
    2. MyAndereNaam.Voornaam := ÔÇÿParisÔÇÖ ;
    3. MyAndereNaam.Familienaam := ÔÇÿHiltonÔÇÖ ;
    4. check (MyAndereNaam.VolledigeNaam = ÔÇÿParis HiltonÔÇÖ,ÔÇÖVolledigeNaam geeft niet het gewenste resultaat bij Paris HiltonÔÇÖ);
    Moet ik ALLE testen al beschrijven alvorens de klasse te schrijven? Dus ook ÔÇÿWitseÔÇÖ, en ÔÇÿOsamaÔÇÖ ÔÇÖBinÔÇÖ ÔÇÖLadenÔÇÖ, ÔÇÿPeterÔÇÖ, ÔÇÿZeerlangevoornaamÔÇÖ ÔÇÖZeerveelmiddenlettersÔÇÖ ÔÇÖZeerlangefamilienaamÔÇÖ.?
    Neen, maak minimaal de test waarmee je je method wil debuggen. En eventueel de belangrijke andere testcases. Als je je beperkt tot bovenstaande twee checks, dan is dit reeds mooi. Nogmaals, je moet geen compleet analyse-document schrijven; je moet zekerheid inbouwen voor de werking van je klasse.
    Dus, alleszins één test voorzien, maar beter is vooraf testen voor een aantal kenmerkende gevallen te maken.

    Ik weet niet hoe ik moet testen.
    De echt triviale methods (zoals create en destroy) moet je niet testen. Tenzij het ingewikkelde methods zijn. Heel eenvoudige getters en setters voor properties ook niet. Alle andere methods kunnen wél zinvol getest worden.
    Stel dat je niet exact weet wat de uitkomst zal zijn van je method. Hoe schrijf ik dan de test op voorhand?
    Je hebt bijvoorbeeld een klasse van hemellichamen, met een method die de postitie van het hemellichaam t.o.v. de aarde geeft op een gegeven tijdstip.

    delphi Code:
    1. THemellichaam = class
    2. public
    3.   procedure Positie ( tijdstip: TDateTime; var x, y, z : double ) ;
    Hoe test ik dit?
    Je zult ÔÇÿietsÔÇÖ moeten weten over de berekende positie. Dit kan door in boeken of op het internet een gekende positie te zoeken. Je kunt ook grootte-ordes op voorhand bepalen. Bijvoorbeeld, de afstand tussen de aarde en de maan bedraagt ongeveer 300.000 km. Een zinnige check zou dan kunnen zijn:

    delphi Code:
    1. Nu := EncodeDateTime ( 2008, 01, 01, 12, 00,00,00 ) ;
    2. Maan.Positie ( nu, x, y, z) ; {Uitvoeren van de method}
    3. check ( abs (afstand (x,y,z) ÔÇô 300000 ) < 50000, ÔÇÿDe maan mag niet meer dan 50.000 km verder of dichter liggen dan 300.000 km van de aardeÔÇÖ) ;
    Merk op dat ik hier een vast tijdstip genomen heb, en niet now. Dit voorkomt dat de test ÔÇÿsomsÔÇÖ wel lukt en soms niet, afhankelijk van het tijdstip waarop je de test uitvoert.
    Stel dat ik geheel niet weet wat de uitkomst van mijn method zal zijn, maar dat ik ergens op het internet een betrouwbare formule heb gevonden en ik heb die geïmplementeerd. Dan nog kan ik een test maken om te vermijden dat er waanzinnige resultaten komen (vb. een resultaat (0,0,0) is een waanzinnig resultaat: de maan zou dan in het middelpunt van de aarde liggen). We maken dan een check of het resultaat niet (0,0,0) is. Deze test zal ons waarschuwen als onze formule ÔÇÿplotsÔÇÖ ergens fout loopt, en niets berekent.
    Maar hoe test ik zoÔÇÖn method op een zinnige manier?
    Dit doen we als volgt: maak eerst een test met een willekeurige (wellicht) foute vergelijking; pas daarna de check aan met de juiste waarden.
    We maken dus eerst

    delphi Code:
    1. Maan.Positie ( nu, x, y, z) ;
    2.   check (abs (afstand (x,y,z) ÔÇô 300000 ) < 1,ÔÇÖDe afstand tot de maan is niet juistÔÇÖ);
    Door dit te debuggen, ga je de berekende waarden voor x, y en z vinden, en de afstand van dit punt. Noteer deze afstand (vb. 293766,8904) en voeg deze waarde in de test in plaats van de ÔÇÿdummyÔÇÖ waarde 300000.

    delphi Code:
    1. check (abs (afstand (x,y,z) ÔÇô 293766.8904) < 0.001,ÔÇÖDe afstand tot de maan is niet juistÔÇÖ);
    Deze test gaat nu een groen resultaat geven.
    Maar wat belangrijker is, deze test moet in de toekomst steeds een groen resultaat geven, ook al veranderen we iets (compleet anders) in onze klasse THemellichaam.
    Dus, als we later bijvoorbeeld ook de berekening van posities van sterren gaan toevoegen aan onze klasse THemellichaam, en daardoor een foutje maken bij de berekening van de positie van de maan, dan gaan we dit onmiddellijk ontdekken door het uitvoeren van deze test. Tracht dit maar eens op een andere manier te ontdekken!
    Het is dus de bedoeling dat we steeds ALLE unit-testen uitvoeren, om te vermijden dat er onopgemerkte ÔÇÿcollateral damageÔÇÖ ontstaat bij wijzigingen in andere delen van je klasse.
    Maak dus eventueel de echte testen pas als je het juiste resultaat kent.

    Veel te ingewikkeld.
    In principe moet je per test drie stappen uitvoeren:
    ÔÇó de klasse opvullen met testgegevens
    ÔÇó de method oproepen
    ÔÇó het resultaat omvormen tot iets eenvoudig (boolean) testbaar
    Dit is helemaal geen ingewikkelde opgave. Het DUnit-framework zorgt ervoor dat al de rest van het werk reeds gemaakt is.
    De klasse opvullen gebeurt meestal recht-toe-recht-aan. Soms kan het dat de klasse niet op te vullen is met de juiste testwaarden, omdat er velden private gedeclareerd zijn. Bijvoorbeeld:

    delphi Code:
    1. TMagnetisme = (maPositief, maNegatief, maNeutraal);
    2. TAtoom = class
    3. private
    4.   FMagnetisme: TMagnetisme ;
    5. public
    6.   procedure RandomWijzigenMagnetisme ;
    7.   function Positie ( tijdstip: TDateTime; var x, y, z : double ) ;
    8. end;
    Stel dat de positie afhangt van de waarde van FMagnetisme. Maar FMagnetisme verandert niet voorspelbaar door de routine RandomWijzigenMagnetisme. We kunnen dus niet voorspellen wat de positie gaat zijn omdat we FMagnetisme niet kennen.
    De enige mogelijkheid hier bestaat erin de ÔÇÿinkapselingÔÇÖ te doorbreken, en een method TestSetMagnetisme ( AMagnetisme: FMagnetisme) te maken, enkel en alleen voor testdoeleinden.
    Dan kunnen we de test maken:

    delphi Code:
    1. Nu := EncodeDateTime ( 2008, 01, 01, 12, 00,00,00 ) ;
    2. MijnAtoom .TestSetMagnetisme (maPositief) ;
    3. MijnAtoom.Positie ( nu, x, y,z ) ;
    4. check ( (x= ) and (y=) and (z=),Positief atoom niet op de gewenste positie) ;
    Het doorbreken van inkapseling (encapsulation) is de enige toegeving die je soms moet doen om een klasse te kunnen testen.
    Unit-testen schrijven is niet moeilijk.

    Goed voor grote projecten, in grote bedrijven, niet voor eigen ontwikkelingen.
    Je gaat géén unit-testen schrijven voor programmaÔÇÖs of tools die je éénmalig gebruikt. Unit-testen maak je ook enkel als je je eigen klassen maakt. De bedoeling is dan dat je per klasse een unit-test maakt. Van zodra je klassen hebt die kunnen wijzigen (omdat je later meer of andere functionaliteit wenst in je programma) of methods hebt die nogal wat verschillende resultaten kunnen leveren, kun je best unit-tests schrijven.
    Dit geldt dus niet enkel voor grote projecten, maar ook voor je eigen kleine klassen. Maak er een gewoonte van, en hoe meer je er maakt, hoe eenvoudiger het wordt.
    Unit-testen heeft niets met de grootte van het project te maken.

    Is alleen nodig als je code constant wijzigt.
    Als je code constant wijzigt, bij de ÔÇÿgeboorteÔÇÖ van een nieuwe klasse, kun je inderdaad nuttig gebruik maken van unit-testen. Al was het maar om een startpunt te hebben om te debuggen.
    Als je code constant wijzigt omdat hij ÔÇÿgroeitÔÇÖ bij de uitbreiding van je project, dan zijn unit-testen zeer nuttig om zeker te zijn dat je methods goed blijven werken.
    Als je code helemaal nooit wijzigt, dan zijn unit-testen in principe niet meer nodig. Maar wie beweert dat hij/zij zijn klasse nooit wijzigt? Net als je klasse al maanden of zelfs jaren onaangeroerd is, zul je heel blij zijn als je een unit-test voor handen hebt bij een kleine wijziging aan die klasse. Je voelt je namelijk nooit 100% zeker van je aanpassing. Een unit-test geeft je terug enige zekerheid bij aanpassingen aan oude code.
    Elke klasse wijzigt. Dus is een unit-test nodig.

    Ik kan geen mooi afgelijnde units/klassen maken.
    Helaas is dit een veelgehoord argument. Helaas, want het heeft niets met testen te maken, maar alles met een ÔÇÿniet al te bestÔÇÖ design. Testen geven namelijk aan wat je mag verwachten als antwoord op een heel preciese vraag. Als je code zo verweven is dat een procedure of functie nog heel wat andere neveneffecten heeft, dan zul je toch even een stap terug moeten nemen, en nagaan of je niet beter je design herbekijkt. Of dat je misschien best eens begint met een design te maken.
    Een typisch geval is deze waarbij je snel een applicatie maakt met wat code in OnClick-events van buttons. Na een tijdje blijkt dat die events toch ingewikkeld worden, en getest moeten worden. In plaats van voort te sukkelen en te debuggen, kun je best eens kijken waarmee je bezig bent, en beginnen met aparte klassen te maken. Op dat moment kun je ook onmiddellijk beginnen met het schrijven van je unit-testen. Het vergt wat werk, maar je moet niet alles tegelijk veranderen, en het zet plots de poort open voor grote aanpassingen aan je applicatie.
    Heb je geen klassen, dan heb je ook geen unit-testen, maar heb je wellicht ook een slecht design.

    MijnÔÇÖomgevingÔÇÖ is té complex om op te zetten voor een test.
    Unit-testen zijn wat de naam zegt: het testen van units (of klassen). Unit-testen gaan geen ganse applicaties testen. Beschouw unit-testen als een logische vervolgstap na het compileren van een klasse: je compileert je code totdat de syntaxfouten zijn verdwenen; daarna voer je unit-testen uit totdat je voldoende juiste basis-resultaten verkrijgt.
    Voor het testen van je applicatie zul je anders te werk moeten gaan. Manueel testen is hier voor een hobbyist meestal de enige oplossing. Voor professionelen bestaan er geautomatiseerde tools om gehele applicaties te testen. Mocht je daar interesse voor hebben, zoek maar in Google naar bijvoorbeeld T-Map.
    Unit-testen testen klassen, geen ganse applicaties.

    Ik kan geen GUI testen.
    Een GUI (user interface) kan inderdaad maar moeilijk getest worden. Ook al heb je er zeer mooie klassen van gemaakt. Je kunt wel een aantal functionaliteiten testen op voorwaarde dat het resultaat in een boolean te evalueren is. Stel bijvoorbeeld dat je een component gemaakt hebt met een edit-veld edit1 een panel panel1 en een button button1. Als je op button1 klikt, moet het panel de tekst uit edit1 omgekeerd afbeelden. Dit kun je als volgt testen:

    delphi Code:
    1. MijnComponent := TMijnComponent.create (self) ;
    2. MijnComponent.edit1.text := ÔÇÿHalloÔÇÖ ;
    3. MijnComponent.button1.click  (self) ;
    4. check (MijnComponent.panel1.Caption= ÔÇÿollaHÔÇÖ, ÔÇÿButton1 van MijnComponent werkt niet naar behorenÔÇÖ) ;
    Dit lukt nog.
    Maar als je visuele effecten van je user interface wil testen, bijvoorbeeld, 'wordt een cirkel mooi getekend?', dan zul je dit manueel moeten doen. Unit-testen bieden hiervoor geen oplossing.
    Unit-testen kunnen enkel booleaanse checks doen, niet nagaan of iets ÔÇÿmooiÔÇÖ is.

    Ik kan geen database-afhankelijke routines testen.
    Dit is inderdaad een tricky probleem.
    Ten eerste heb je routines die data ophalen uit een database. Deze kun je wél testen met unit-testen, op voorwaarde dat je dit doet op een database waarvan de inhoud niet verandert. Je kunt hiervoor dus een test-database opzetten. Dan kun je wel zinnige testen uitvoeren.
    Stel dat je een klasse hebt met een functie ZoekNaam die de naam geeft van een bepaald record.
    Je kunt dan de volgende test schrijven:

    delphi Code:
    1. check(MijnDataKlasse.ZoekNaam(1)=ÔÇÖWitseÔÇÖ,ÔÇÖNaam 1 is niet WitseÔÇÖ) ;

    Het heeft wel degelijk zin om zulke testen te schrijven, en meermaals uit te voeren.
    Als dit de eerste keer lukt, dan zal dit alle volgende keren toch ook wel lukken?! En als het niet lukt, dan is dat gewoon omdat record 1 een andere naam bevat?
    Dat zou je denken.
    Maar de method ZoekNaam kan vele taken uitvoeren, vele (interne) functies oproepen, die op hun beurt kans hebben om fout te lopen. Dankzij deze test kun je zulke fouten snel opsporen.
    Verder heb je routines die data in een database wegschrijven. Deze zijn moeilijk te testen. Vooral omdat een creatie van een record veelal maar één maal lukt; de tweede maal geeft dit een duplicate key error. Updates kun je ook maar één maal testen. Als je een tweede maal een update test met dezelfde waarden, hoe weet je dan dat de update-routine iets doet?
    Als het echt belangrijk is om te testen, dan zou ik er zeker een inspanning leveren, en er zorg voor dragen dat de testen te herhalen zijn zonder bijkomende ingreep. Bijvoorbeeld:

    delphi Code:
    1. check(MijnDataKlasse.ZoekNaam(1)=ÔÇÖWitseÔÇÖ,ÔÇÖNaam 1 is niet WitseÔÇÖ) ;
    2. MijnDataKlasse.UpdateNaam(1,ÔÇÖMilquetÔÇÖ) ;
    3. check(MijnDataKlasse. ZoekNaam (1)= ÔÇÖMilquetÔÇÖ,ÔÇÖNaam 1 is niet gewijzigd in MilquetÔÇÖ) ;
    4. MijnDataKlasse.UpdateNaam(1,ÔÇÖWitseÔÇÖ) ;
    5. check(MijnDataKlasse. ZoekNaam (1)= ÔÇÿWitseÔÇÖ,ÔÇÖNaam 1 is niet gewijzigd in WitseÔÇÖ) ;
    Je moet heel goed opletten als de laatste test mislukt. Als de naam gewijzigd is in ÔÇÿMilquetÔÇÖ, maar niet terug in ÔÇÿWitseÔÇÖ, dan zal een herhaling van deze reeks testen niet lukken, omdat je eerst test of de naam ÔÇÿWitseÔÇÖ is.
    Het testen van dataroutines is en blijft dus tricky, maar soms noodzakelijk.

    Ik kan niet alle mogelijke gevallen testen: dat zijn er miljoenen!
    Dat is waar! Maar het doel van unit-testen is NIET om 100% zeker te zijn dat alle mogelijke combinaties werken. Net zo min een succesvolle compilatie zegt dat je programma perfect werkt. Wat unit-testen garanderen, is dat je methods juiste resultaten leveren in een (zeer beperkt) aantal gevallen. Het is nu aan jou om deze gevallen z?? te kiezen dat je de belangrijkste gevallen en de belangrijkste uitzonderingsgevallen test. Typisch test je een ÔÇÿgewoonÔÇÖ geval het eerst. Dan maak je testen voor alle verschillende situaties, niet voor alle combinaties van de parameters. Zeker als je if-then-else in je method staan, dan maak je een test voor een geval dat in het if-gedeelte komt, en een test voor een geval dat in het else-gedeelte komt. Daarna test je nog met ÔÇÿextremeÔÇÖ waarden.
    Wat je heel zeker moet doen, is het toevoegen van combinaties waarmee je bugs ontdekt. Stel dat je in je applicatie een fout ontdekt. Dit blijkt een bug te zijn in je method bij een bepaalde combinatie van je parameters. Om te debuggen kun je een testcase maken met deze combinatie. Zo kun je debuggen vanuit je unit-test in plaats van te moeten vertrekken vanuit je applicatie. Dus in plaats van 10 velden in te moeten vullen in je applicatie, daarna in de juiste volgorde een aantal buttons moet klikken, om dan je method in de debugger te volgen, kun je de waarden van de parameters waarmee je method worden opgeroepen, noteren, en neerschrijven in een bijkomende check. Bijvoorbeeld:

    delphi Code:
    1. check ( MijnKlasse.BerekenSpeciaal ( ÔÇÿSmithÔÇÖ,12.894, clRed, 13, ÔÇÿ****ÔÇÖ) = 1, ÔÇÿDebug-geval van BerekenSpeciaal werkt nietÔÇÖ) ;
    Zo kun je een aantal keer na mekaar je method BerekenSpeciaal debuggen door de unit-test uit te voeren, zonder telkens via je applicatie de nodige waarden te moeten ingeven.
    Deze check kun je gerust laten staan als je de bug opgelost hebt. Zo heb je later bij volgende testruns de zekerheid dat je method ook nog steeds werkt voor een moeilijke combinatie, waarvan je weet dat ze vroeger ooit voor problemen zorgde.
    Voeg dus testen toe voor een aantal ÔÇÿstandaardÔÇÖ gevallen, een aantal ÔÇÿspecialeÔÇÖ gevallen, en voor alle combinaties waarmee je ooit een bug hebt gevonden. Niet voor de 9.999.996 andere combinaties.

    Heel simpele dingen moeten niet getest worden.
    DaÔÇÖs waar. De meeste constructors (create) en destructors (destroy) hebben geen unit-testen nodig. Ook voor eenvoudige getters en setters (de procedures en functions die je gebruikt om properties te lezen en te schrijven) ga je geen unit-testen maken.
    Het is wel altijd handig om een unit-test te hebben, zodat je op een eenvoudige manier testcases kunt toevoegen als je een bug zoekt (en vindt). Een hele unit-test maken omdat je een simpele bug hebt gevonden, ga je niet doen. Dus is het nuttig dat de unit-test ÔÇôeventueel leeg- reeds op voorhand bestaat, zodat je bij een bug steeds vanuit je unit-test kunt vertrekken om ze op te lossen.
    Een lege unit-test kan ook nuttig zijn; doodsimpele methods testen is verloren moeite.

    Wanneer voer ik testen uit?
    Een allereerste test voer je uit als je de check geschreven hebt, en je enkel nog maar een 'lege' method hebt. Dit is test-driven programming: je maakt lege methods, en je schrijft eerst de testen.
    Bijvoorbeeld:

    delphi Code:
    1. procedure TAtoom.Positie ( tijdstip: TDateTime; var x, y, z : double ) ;
    2. begin
    3. end ;
    en je maakt de check

    delphi Code:
    1. MijnAtoom.Positie ( nu, x, y, z ) ;
    2. check ( ( abs(x-0.00457) <0 ) and ( abs(y-0.00658) <0 ) and ( abs(z-0.00125) <0 ), ÔÇÿDe positie van het atoom wordt niet juist berekendÔÇÖ) ;
    En je voert de test uit.
    Je test gaat rood zijn; je method doet namelijk niets. Maar hiermee weet je reeds dat je unit-test goed geschreven is. De bedoeling is nu dat je de method Positie z?? schrijft dat de test groen wordt. Je volgende testen zijn dus tijdens de ontwikkeling van je method totdat alle testen voor die method Positie groen zijn.
    Dan is het niet gedaan met de unit-testen! Dan komt het nut van unit-testen pas echt naar boven.
    Later ga je de unit-testen telkens uitvoeren als je een wijziging in je unit uitvoert, of als je een bug vindt bij een bepaalde waarde of combinatie van waarden.
    Bekijk het zo: bij elke wijziging ga je je unit opnieuw compileren. Als de compilatie lukt, ga je je unit-testen eventueel aanpassen (bijvoorbeeld als je een parameter hebt toegevoegd aan een procedure), of checks bijmaken als je meer functionaliteit hebt toegevoegd (je voegt bijvoorbeeld positieberekening voor sterren toe, dan voeg je ook testen voor sterren toe in je unit-test).
    Dan voer je opnieuw alle unit-testen uit. Deze testen geven aan of alle methods nog werken en of de nieuwe functionaliteit ook naar behoren werkt. Vooral het eerste is een aangenaam effect van unit-testen.
    Beschouw het uitvoeren van unit-testen als een vervolg op de compilatie. Je compileert tot de syntaxisfouten opgelost zijn, en daarna voer je unit-testen uit tot de functionele fouten opgelost zijn. Op deze manier verloopt de ontwikkeling vlekkeloos tot een klasse waarvan je een grote zekerheid hebt dat het functioneel correct werkt.

    Overtuigd?
    Ik niet. Toch niet toen ik de theorie van test-driven-development in een boek over eXtreme Programming las. Jaren later, na het maken van een eenvoudige unit-test voor het inlezen van een csv-bestand, begon ik plots 'zekerheid' te krijgen in mijn method. Dat was een prettig gevoel, zeg!
    Na heel wat gediscussieer met collegaÔÇÖs zag ik in dat de bezwaren tegen het gebruik van unit-testen om te buigen waren tot argumenten om unit-testen juist wél te gebruiken. Als je unit-testen juist en zinvol gebruikt.
    Nu ik begonnen ben aan een method met een bangelijk moeilijke logica, hevige berekeningen, en een complexe setup, heb ik eerst een dummy unit-test geschreven. Tijdens mijn eerste debug-pasjes zag ik kemels in mijn setup (beginwaarden). Ik heb die laten staan als ÔÇÿuitzonderingsÔÇÖtesten. Stilletjesaan heb ik juiste beginwaarden gevonden, en zie ik wat juiste resultaten zijn. Dit zijn de verdere testen. De aartsmoeilijke method begint niet enkel te werken, ik begin zéker te zijn dat ze goed werkt, en ik ben zeker dat ze goed zal blijven werken, ook al vind ik een bug (dit lijkt een contradictie!), en ook al breid ik de method later uit.
    Ik ben plots een heel gerustgestelde Delphi-er.
    Ik ben overtuigd!

    Sam Witse.
    Last edited by Marcel; 22-Nov-07 at 00:39.

  2. #2


    Verder natuurlijk bedankt voor je post, wat een binnenkomer En het is altijd leuk een mede unit-tester te ontmoeten.
    Marcel

  3. #3
    5th member of nldelphi
    Join Date
    Mar 2001
    Location
    Alkmaar
    Posts
    2,127
    Een erg leuk en helder artikel. Het klinkt allemaal zo bekend..
    Vooral je omschrijving van het "gevoel" (erg herkenbaar)
    RLD

  4. #4
    Delphi & OO in Vlaanderen SamWitse's Avatar
    Join Date
    Sep 2007
    Location
    Brussel
    Posts
    833
    Het was de bedoeling om aan te tonen dat Unit-testen bruikbaar zijn.

    Ik heb genoeg artikels en boeken gelezen, waarvan ik dacht: "Mooi, maar WTF ben ik er mee?"
    Ik hoop dat dit artikel niet enkel jou, maar vele andere lezer net het tegengestelde leert:"Niet mooi (grapje), maar het is heel nuttig voor mij".

    Heeft iemand een mooi artikel over het nut van een Factory-pattern in Delphi? En liefst voor een hobbyist...
    Should array indices start at 0 or 1? My compromise of 0.5 was rejected without, I thought, proper consideration.

    Sam Witse.
    Delphi & OO in Vlaanderen

  5. #5
    mov rax,marcov; push rax marcov's Avatar
    Join Date
    Apr 2004
    Location
    Ehv, Nl
    Posts
    10,357
    Mijn grote probleem met unittesten is altijd dat input en exacte event volgorde vaak _veel_ te lastig te simuleren zijn. En om de echte problemen te vinden heb je die nu net nodig. Ik beperk het principe daarom meestal tot wat kern delen van frameworks en wat andere routines die redelijk pure functies zijn.

    Aan de FPC kant werkt het bijvoorbeeld wel geweldig. Maar daar is de input makkelijk te definieeren, en zelfs dan is de kern van het testen niet echt het droogzwemmen van te voren, juist in het onderhoud en toevoegen van problemen gedurende gebruik zit de kracht.
    Last edited by marcov; 27-Nov-07 at 10:49.

  6. #6
    Delphi & OO in Vlaanderen SamWitse's Avatar
    Join Date
    Sep 2007
    Location
    Brussel
    Posts
    833
    Je hebt gelijk. Unit testen dienen ook niet voor complexe opeenvolging van events te testen.
    Wat je wel kunt doen is het volgende:
    vind het schuldig event, noteer de ‘toestand’ van je systeem bij het oproepen van dit event (= de parameters, de klassen die je in je event gebruikt)
    maak een testcase waarbij je deze ‘toestand’ simuleert (handmatig creëren van de klassen, toekennen van de nodige waarden), en voeg een check toe.
    Maar nogmaals, unit-testen kun je niet voor alle gevallen gebruiken, maar wees blij met wat ze wél kunnen, en da’s al heel wat.
    Last edited by GolezTrol; 28-Nov-07 at 11:37.
    Should array indices start at 0 or 1? My compromise of 0.5 was rejected without, I thought, proper consideration.

    Sam Witse.
    Delphi & OO in Vlaanderen

  7. #7
    mov rax,marcov; push rax marcov's Avatar
    Join Date
    Apr 2004
    Location
    Ehv, Nl
    Posts
    10,357
    Klopt, maar dat is niet echt het unittest gebeuren van schrijven tijdens design.

    Dat is meer een traditionele regressie suite. (wat in feite de FPC testsuite ook is, met +/- 3000 tests). (afteraf ipv vooraf)

  8. #8
    Quote Originally Posted by SamWitse View Post
    Je hebt gelijk. Unit testen dienen ook niet voor complexe opeenvolging van events te testen.
    Dat hangt er een beetje vanaf wat je hier met events bedoeld.

    Als ik van de klant een melding krijg dat er een fout optreedt als er x wordt geboekt, y bij wordt opgeteld en dan voor klant z een factuur wordt gemaakt, dan gaat deze volgorde als eerste in de unit test. Als het in de unit test niet fout gaat is het waarschijnlijk een UI probleem, of was de volgorde niet zoals de klant aangaf. Door het schrijven van de unit test concentreer ik me eerst op het zoeken van het probleem. Pas als de unittest dezelfde fout geeft als de melding van de klant weet ik dat ik de fout heb gevonden. Daana ga ik pas aan de oplossing denken.

    Deze werkwijze zorgt dat je je goed kunt focussen op wat je moet doen. Je bent of aan het zoeken naar de fout, of aan het oplossen en nooit beide. En bijkomend voordeel is dat deze fout nooit meer voor gaat komen, die staat immers in de unit tests.
    Marcel

  9. #9
    mov rax,marcov; push rax marcov's Avatar
    Join Date
    Apr 2004
    Location
    Ehv, Nl
    Posts
    10,357
    Ik zou zeggen dat de definitie van events iets is als "een variatie in het geheel van inputs die een alternatief codepath of staat kan veroorzaken".

    Marcel: zie het een beetje zo, hoe relevant is dit als je om dit door te testen een hele db moet opslaan voor iedere regressie test ?

    Let overigens op dat jouw voorbeeld ook meer een regressie voorbeeld is dan een vooraf opgestelde unittest

  10. #10
    Het zou een vooraf opgestelde unit test moeten zijn, maar blijkbaar had ik in de tests een situatie gemist. In sommige gevallen logisch, het is niet altijd mogelijk alle situaties vooraf te verzinnen.

    Maar wat bedoel je met een hele db opslaan? Als ik inderdaad alleen voor deze test de hele database van de klant lokaal zou moeten hosten zijn we aan het doorslaan.
    Marcel

  11. #11
    Toch is dat wel het probleem. Juist met steeds veranderende data is het belangrijk dat je de boel goed test. Het opzetten en bijhouden van een database is echter wel een hoop werk. Het schrijven van testcode is overwegend vrij eenvoudig, maar voor een database moet je ook weer procedures schrijven die de data genereren of herstellen. Dit is een slag extra werk, is lastiger te verifiëren, en zal er mogelijk snel bij in schieten.

    Hoe testen jullie database-gerelateerde zaken, en dan met name units die mutaties doen in de database?
    1+1=b

  12. #12
    mov rax,marcov; push rax marcov's Avatar
    Join Date
    Apr 2004
    Location
    Ehv, Nl
    Posts
    10,357
    Op dit moment is de database alleen een vrij simpele store (geen transacties e.d.), en niet eens productie.

    Toen ik dat wel had in mijn vorige baan: we hadden test scenario's voor de app zelf, en de live database werd nachtelijk geimporteerd op een testserver. Voor sommige tests waren er korte SQL scripts die een object in een bepaalde staat brachten om te testen.

    Niet perfect of waterdicht, maar er was wel wat, het werd gebruikt, en vind een x percentage van de domste rotfoutjes af. En het had een aardige kosten/baten analyze. Al had iets meer cases gemogen, het systeem voldeet. Soms werden er zelfs tests vooruit gemaakt (! :-))

    In de baan ervoor hadden we geen schrijf rechten in de db. Mutaties werden in een gecodeerd formaat aan een administratieve medewerker gegeven. Te veel uitzonderingen, commentaar velden e.d. We hadden wel een soort unittest voor enkele kern gedeelten van het systeem. Grotendeels vooraf opgesteld als gevolg van de opinie's van een andere medewerker. Dat ging op zich redelijk (al waren veel tests een beetje steriel en theoretisch), maar veel van deze kern gedeelten waren relatief klein en geisoleerd maar complex programmeer techinisch. Hierdoor schoolvoorbeelden voor unittests, dus het is geen wonder dat het in dit geval wel meeviel.
    Last edited by marcov; 29-Nov-07 at 12:43.

  13. #13
    Quote Originally Posted by GolezTrol View Post
    Hoe testen jullie database-gerelateerde zaken, en dan met name units die mutaties doen in de database?
    Ik doe in de tests geen aannames. Dus alles wat de test nodig heeft moet de test aanmaken. De basis unit test heeft routines om deze zaken snel aan te maken. De test van een factuur zal dus beginnen met een MaakKlant / MaakArtikel / MaakValuta en gaat daarna pas de eigenlijke factuur aanmaken.

    En als de test klaar is, dan kun je aan de eigenlijke software beginnen.
    Marcel

  14. #14
    Senior Member
    Join Date
    Dec 2003
    Location
    Den Haag
    Posts
    210
    Quote Originally Posted by SamWitse View Post
    Heeft iemand een mooi artikel over het nut van een Factory-pattern in Delphi? En liefst voor een hobbyist...
    Rob Bracken heeft voor The Developers Group een artikel over het
    factory pattern geschreven.

    Groet,
    Erwin

  15. #15
    Delphi & OO in Vlaanderen SamWitse's Avatar
    Join Date
    Sep 2007
    Location
    Brussel
    Posts
    833
    Mooi artikel, begrijpbaar voorbeeld.
    Alleen heb ik het moeilijk te bedenken dat je applicatie niet weet van welke subclasse het een object moet creëren, en het daarom over laat aan en factory. Ik blijf het nog steeds over-kill vinden. Tot iemand mij van het tegendeel kan overtuigen
    Should array indices start at 0 or 1? My compromise of 0.5 was rejected without, I thought, proper consideration.

    Sam Witse.
    Delphi & OO in Vlaanderen

Page 1 of 2 1 2 LastLast

Thread Information

Users Browsing this Thread

There are currently 1 users browsing this thread. (0 members and 1 guests)

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •