Software testing Dictaat
Embedded Systems Engineering Groep: ES1, ES1V
Oorspronkelijk dictaat van F. Feldbrugge, bewerkt door J.G. Rouland 11 november 2009
Inhoud 1 2 3 4
Software testen: Wat? Waarom? ....................................................................... 2 De 10 geboden voor software testen ................................................................. 4 De plaats van testen in het softwareontwikkeltraject ...................................... 7 Code inspectie ................................................................................................... 10 4.1 Desk Checking ............................................................................................ 10 4.2 Code Review/Inspection ............................................................................. 10 4.3 Code Walkthrough ...................................................................................... 14 4.4 Peer Rating ................................................................................................. 15 5 Functioneel testen ............................................................................................ 17 5.1 Black Box testcase ontwerp ........................................................................ 17 5.1.1 Equivalence Partitioning ................................................................... 17 5.1.2 Boundary Value Analysis ................................................................. 22 5.1.3 Cause-Effect Graphing ..................................................................... 24 5.1.4 Error Guessing ................................................................................. 39 5.2 White Box testcase ontwerp ....................................................................... 40 5.2.1 Statement Coverage ........................................................................ 40 5.2.2 Decision Coverage ........................................................................... 42 5.2.3 Condition Coverage.......................................................................... 44 5.2.4 Decision/Condition Coverage ........................................................... 45 5.2.5 Multiple Condition Coverage ............................................................ 46 5.2.6 Multiple condition coverage toegepast op "Raad het getal" ............. 50 5.2.7 Minimale set testcases bij "Raad het getal" ...................................... 59 5.3 Modulair testen ........................................................................................... 61 5.3.1 Incrementeel en niet-incrementeel testen ........................................ 61 5.3.2 Top-Down en Bottom-Up testen ....................................................... 64 5.4 Het uitvoeren van testcases........................................................................ 67 5.5 Andere vormen van functioneel testen ....................................................... 68 5.6 Test stopcriteria .......................................................................................... 71 6 Niet-functioneel testen...................................................................................... 74 Bijlage 1: SearchTable ............................................................................................ 76 Bijlage 2: MeanOfTable ........................................................................................... 78 Bijlage 3: Triangle .................................................................................................... 80 Bijlage 4: Mean ........................................................................................................ 81 Bijlage 5: Gcd........................................................................................................... 82
1
1
Software testen: Wat? Waarom?
Software speelt een steeds belangrijker rol in onze samenleving. Niet allen computers zijn uitgerust met software, maar ook veel alledaagse gebruiksvoorwerpen zoals auto's, televisies, horloges, etc. zijn voor hun goede werking afhankelijk van software. Het gaat hier om software in de breedste zin van het woord: niet alleen gewone computerprogramma's, maar ook "versteende" software: software die is ingebakken in chips, ROM's, etc. Indien software fouten bevat, en dat is helaas meestal het geval, dan kan dat leiden tot kleinere of grotere rampen. Er zijn enorm veel kosten gemoeid met softwarefouten. Niet alleen omdat een softwarefout kan leiden tot het ontploffen van een kerncentrale, het verloren raken van een satelliet of het neerstorten van een vliegtuig, maar ook omdat het verbeteren van een fout een zeer kostbare aangelegenheid kan zijn. Denk maar eens aan de kosten die gemoeid zijn met het vervangen van een foute chip in enkele miljoenen TV-toestellen of auto's. In het streven naar kwalitatief goede software speelt testen een cruciale rol. Ieder zichzelf respecterend softwarebedrijf investeert veel in een goede testafdeling met goed geschoolde testers. Ook in het softwareontwikkelingstraject dient voldoende tijd gereserveerd te worden voor het testen en de onvermijdelijk daaruit voortkomende softwareaanpassingen. Het beschouwen van testen als een achteraf-activiteit, die inferieur is aan de ontwikkelactiviteit, is zonder meer onprofessioneel en herbergt grote gevaren in zich. Daarom mag op de flinke investeringen, die met goed testen gemoeid zijn, niet worden bezuinigd en temeer niet als een falend product tot rampzalige situaties kan leiden. Het beroep van testontwikkelaar is een volwaardige professionele bezigheid, waarin zeker sprake is van een uitdaging. Op een zowel methodische als creatieve manier wordt een essentiële bijdrage geleverd aan de softwarekwaliteit, door het genereren van testgevallen (testcases), die de software zo grondig mogelijk aan de tand voelen. Een goede testontwikkelaar zal proberen zo economisch verantwoord mogelijk te werken door het genereren van een zo klein mogelijke verzameling testcases met een zo hoog mogelijke opbrengst. Daarbij maakt hij niet alleen gebruik van ervaring en een zesde zintuig, maar ook van een aantal standaardtechnieken. In dit dictaat wordt een aantal standaardtechnieken behandeld. Eerst behandelen we enkele Black Box technieken, die gebaseerd zijn op de specificatie van het te testen systeem. Vervolgens komen White Box technieken aan de orde, die gebaseerd zijn op de gegenereerde code. Het gebruik van deze technieken draagt bij aan het genereren van een economisch verantwoorde verzameling testcases. Het ideaal van elke softwareontwikkelaar is een foutvrij systeem. In de meeste gevallen is dit een illusie. De meeste softwaresystemen zijn zo complex, dat het ondoenlijk is om alle fouten eruit te halen. Wel kunnen zodanige ontwikkelmethoden worden gebruikt, dat het aantal fouten sterk wordt gereduceerd. Een goede testfase brengt vervolgens het merendeel van de toch aanwezige fouten aan het licht. Maar aangezien het opsporen van elke nog niet ontdekte fout steeds kostbaarder wordt, trekt men in de praktijk een grens van wat economisch verantwoord is. Minder testen is te kostbaar vanwege de financiële gevolgen van teveel fouten. Meer testen is te kostbaar vanwege de te grote testinspanning en de te lange doorlooptijd van het product. Het economisch optimum wordt bepaald door factoren als: Hoe kritisch is het pro2
duct? Wat zijn de gevolgen van een fout? Hoe gemakkelijk is een fout te herstellen? Etc. Wat is "testen" eigenlijk? De activiteit die erop gericht is om aan te tonen dat het systeem goed werkt? Dat het systeem foutvrij is? Dat zijn geen goede definities, want in het algemeen is het ondoenlijk om aan te tonen dat een systeem geen fouten maakt. Je zou dan immers alle mogelijke testinputs in alle mogelijke systeemtoestanden moeten uitproberen. Bij een relatief klein systeem leidt dat al tot een astronomisch aantal testcases. Ook definities in de trant van "het aantonen dat het systeem aan de specificaties voldoet" leidt aan hetzelfde euvel. Een goede definitie gaat precies van het omgekeerde uit, namelijk dat testen de activiteit is die erop gericht is om aan te tonen dat het systeem niet aan de specificaties voldoet. Een goede tester ziet het dus niet als zijn taak aan te tonen dat het programma goed werkt, maar hij is erop gericht om fouten op te sporen. Hij beschouwt een testcase dan ook als succesvol als met die testcase een of meer nog niet ontdekte fouten worden opgespoord. In feite zou je testen dus kunnen zien als een destructieve bezigheid, gericht op het "kraken" van het onderhavige programma. In deze zin hebben de softwareontwikkelaar en de testontwikkelaar strijdige belangen: de testontwikkelaar is verheugd als het hem lukt om "gaten in het programma te schieten" terwijl de softwareontwikkelaar verheugd is als dat niet lukt. Maar beiden hebben wel hetzelfde algemene doel voor ogen: het produceren van kwalitatief hoogwaardige software.
3
2
De 10 geboden voor software testen
Er volgt nu een tiental testprincipes. Deze zijn in de vorm gegoten van "10 geboden". Dit is bewust gedaan om te benadrukken hoe wezenlijk deze principes zijn. De meeste "geboden" zijn misschien intuïtief vanzelfsprekend, toch wordt er in de praktijk maar al te vaak niet naar gehandeld. Zij vormen de basis van een goede testhouding. 1. Gij zult als doel van uw Testen zien: het vinden van fouten. In het vorige hoofdstuk kwam de definitie van testen reeds ter sprake. Hier wordt nogmaals benadrukt, dat alle testinspanningen erop gericht moeten zijn om het programma te "kraken", niet om te proberen aan te tonen dat het programma goed werkt. Als we een "fout" definiëren als "gedrag dat niet in overeenstemming is met de specificaties", dan komt het eerste gebod ook neer op het volgende: "Het doel van uw Testen is: het aantonen dat het programma niet aan de specificaties voldoet". 2. Gij zult een testcase slechts als goed beschouwen als die testcase een hoge waarschijnlijkheid heeft om een nog niet ontdekte fout te detecteren. Evenzo zult gij een testcase slechts als succesvol beschouwen als er een nog niet ontdekte fout mee wordt gedetecteerd. Dit is in lijn met het eerste gebod. Het zal duidelijk zijn, dat we niets hebben aan een testcase die leidt tot de correcte programma-uitvoer. We tonen er dan immers geen fout mee aan! Een dergelijke testcase hadden we ons kunnen besparen. Nu kun je natuurlijk niet voorkomen, dat testcases falen (= geen fouten aantonen). Je zult maar een zeer goede programmeur treffen! Het gaat daarom om de waarschijnlijkheid om fouten te detecteren. Met andere woorden, een verzameling testcases is als goed te beschouwen als die verzameling minimaal is en er de meest voorkomende fouten mee worden aangetoond. 3. Gij zult uw testcase niet alleen laten bestaan uit waarden voor de testinvoer (+ systeemtoestand), maar ook uit een aanduiding van de verwachte uitvoer (+ nieuwe systeemtoestand). Als de waarden van de testinvoer en de toestand van het systeem bekend zijn, dient men uit de specificaties te kunnen afleiden wat de uitvoer en de nieuwe toestand van het systeem zullen zijn. Deze dienen vermeld te worden, anders is voor degene die de tests uitvoert niet eenvoudig te bepalen of er wel of geen fout gedetecteerd is. Bovendien komt de tester niet in de verleiding om het resultaat als goed te beschouwen, omdat het op het eerste gezicht goed lijkt. 4. De Tester van een programma zij nimmer de Programmeur van dat programma. Evenzo zij de testinstantie van een systeem nimmer de instantie die de software heeft vervaardigd. In het vorige hoofdstuk kwam reeds het verschil in houding tussen Programmeur en Tester aan de orde. De een stelt er een eer in dat er zo weinig mogelijk fouten worden aangetoond, de ander dat er juist zoveel mogelijk fouten worden aangetoond. Het is ook voor de Programmeur moeilijk om zich een goede testhouding aan te meten: hij moet immers zijn geesteskind proberen te kraken. Iemand zijn 4
eigen programma laten testen is even fout als bijvoorbeeld een schrijver zijn eigen roman te laten bekritiseren. Ten slotte is er nog een zeer belangrijke reden om het programma door een ander te laten testen. Het is zeer waarschijnlijk dat dezelfde denkfout zich in de testcases herhaalt als de Programmeur een denkfout maakt (bv. de specificaties verkeerd interpreteert). Het gevolg is dat de testcases dan falen, waar ze succesvol hadden moeten zijn. Het bovenstaande wil niet zeggen, dat het onmogelijk is voor een programmeur om zijn eigen programma te testen. De meeste programmeurs zullen dat toch wel doen om zichzelf ervan te overtuigen dat ze een goed product afleveren. We willen hier alleen aangeven, dat het testen door een ander dan de programmeur veel effectiever en succesvoller is. Overigens is het wel zo dat, wanneer een tester een fout heeft aangetoond, juist de programmeur de beste persoon is om de oorzaak van de fout op te sporen en te herstellen (debugging). Wat op microniveau geldt voor een programmamodule en een programmeur, geldt evenzo voor een groot softwaresysteem en het bedrijf, dat dit systeem heeft vervaardigd. Weliswaar zal een goede systeemtest mogelijk zijn doordat programmeurs en testers verschillende mensen zijn en er zelfs sprake kan zijn van verschillende afdelingen. Het feit blijft dat het strijdig met het bedrijfsbelang kan zijn om te grondig te testen, bijvoorbeeld in verband met een te late releasedatum. Als de betrouwbaarheid van een softwaresysteem van kritiek belang is voor een afnemer, dan doet die afnemer er goed aan om zelf het systeem nog eens grondig te testen (of te laten testen door een onafhankelijke instantie). 5. Gij zult uw testresultaten grondig inspecteren. Het ontwikkelen van testcases is een kostbare aangelegenheid. Elke testcase beoogt een eigen klasse van fouten af te dekken. Nu bestaat de testuitvoer vaak uit lange lijsten met resultaten. De neiging bestaat dan ook om snel door zo'n lijst heen te scannen met het risico dat bepaalde fouten, die wel gedetecteerd zijn, alsnog over het hoofd worden gezien. Het gevolg is: de testcase wordt niet benut en het programma blijft met extra fouten zitten. 6. Gij zult niet alleen testcases opstellen voor geldige inputs en verwachte outputs, ook voor ongeldige inputs en onverwachte outputs. Zo zult gij niet alleen testen wat het programma geacht wordt te doen, maar ook wat het niet geacht wordt te doen. De neiging bestaat om alleen te letten op de standaard functionaliteit. Als je een leeftijd moet invullen, vul je een getal in. Maar wat als er een negatief getal wordt ingevuld? Of een invoer die geen getal is? Of als er helemaal niets wordt ingevuld? Al die mogelijkheden zullen ook getest moeten worden. Ook een ongewenste uitvoer (of nieuwe toestand) kunnen we trachten te forceren, bv. een deadlock of file overflow. Juist dit soort situaties levert bij uitstek foutbronnen, dus testcases die zich hierop richten hebben een grote kans om fouten te detecteren. Het is op grond van het derde gebod alleen mogelijk om dergelijke testcases op te stellen als er in de specificaties staat, hoe het systeem zich in dergelijke gevallen moet gedragen. In de praktijk blijken de specificaties hier vaak in gebreke te blijven. Het zal dan ook vaak zo zijn, dat de testafdeling een wezenlijke bijdrage levert aan het completeren van de specificaties!
5
7. Gij zult uw testcases bewaren voor hergebruik. Het ontwikkelen van testcases is een kostbare aangelegenheid. De meeste softwaresystemen kennen een aantal opeenvolgende releases waarbij het overgrote deel van de functionaliteit overeind blijft. Die zal steeds opnieuw getest moeten worden, want door veranderingen kunnen er ongewild ook fouten in de oude functies binnensluipen. Het is daarom belangrijk om de oude testcases te bewaren om ze naderhand opnieuw uit te kunnen voeren (regressietest). 8. Gij zult er bij de planning van het testtraject niet van uitgaan, dat er nauwelijks fouten gevonden zullen worden. Veel onervaren planners van een softwareontwikkeltraject ruimen relatief veel meer tijd in voor de implementatie dan voor het testen. De ervaring leert echter, dat juist het testtraject (inclusief herstellen van fouten en opnieuw testen) juist de meeste tijd kost! Soms worden implementatie- en testtraject op één hoop gegooid. Dat is uiterst verraderlijk, want de programmeurs denken dan te veel tijd voor hun implementatie te hebben, terwijl de eerste versie al in een veel vroeger stadium vereist is om voldoende tijd voor het testen te hebben. Bovendien dienen de trajecten gescheiden te zijn omdat het de activiteiten van verschillende personen (zie gebod 4) en afdelingen betreft. 9. Gij zult, naarmate gij meer fouten in een programmamodule vindt, des te meer andere fouten in die module verwachten. Dit is nogal contra-intuïtief. Immers, hoe meer fouten er gevonden (en hersteld) zijn, des te beter de module wordt. Maar, hoe meer fouten er gevonden worden, des te lager is de kwaliteit van die module. Of, om het anders te stellen: een kwalitatief slechte module zal al snel een heleboel fouten genereren. En omdat die module kwalitatief slecht is, is de kans groot, dat er nog een heleboel meer fouten inzitten! Het is zelfs raadzaam om te overwegen om een module opnieuw te ontwikkelen, als die module relatief veel fouten blijkt te bevatten. 10. Gij zult Testen niet geringer achten dan Programmeren. De meeste software-experts ontwikkelen graag nieuwe modules en halen hun neus wat op voor degenen, die hun modules testen. Ten onrechte. Testen is een vak apart, waar heel wat creativiteit en inventiviteit bij komt kijken. Ook blijken er in de praktijk testers met "gouden handen" rond te lopen, mensen met een zesde zintuig voor het kraken van programma's. Ook "hacken" (bv. het ongeoorloofd proberen binnen te dringen van computersystemen) is een vorm van testen, namelijk het testen van de beveiliging van zo'n systeem. De inventiviteit en het plezier van de hackers geeft goed weer welke arbeidsvreugde in het testwerk mogelijk is. Een ander aspect is nog, dat een goede tester vaak veel betere code schrijft dan een programmeur. En een goede programmeur weet vaak ook juist aardige tests te verzinnen. Kortom, het is goed als één persoon zowel programmeur als tester is. Niet van hetzelfde programma natuurlijk, Dat zou in strijd zijn met het vierde gebod, maar roulerend dan eens een module programmeren, dan weer een andere module testen, etc.
6
3
De plaats van testen in het softwareontwikkeltraject
Het ontwikkelen van een complex systeem, dus ook een complex softwaresysteem, is meestal een recursief proces (zie fig. 1). Uitgaande van de specificatie wordt het systeem geïmplementeerd. Vervolgens wordt het geïmplementeerde systeem getest, d.w.z. er wordt gecheckt in hoeverre het systeem niet aan de specificaties voldoet. Bij een simpel softwaresysteem kan de implementatie direct coderen betekenen. De meeste systemen zijn echter te complex om direct te coderen. Dan vindt er eerst een ontwerpfase plaats, waarin het systeem wordt gedecomponeerd in een aantal deelsystemen, waarbij hun relaties met de buitenwereld (externe interfaces) en hun onderlinge relaties (interne interfaces) worden gedefinieerd. De deelsystemen zijn ofwel bestaande modules, ofwel ze moeten verder worden ontwikkeld. In dat laatste geval worden er voor zo'n deelsysteem weer specificaties opgesteld en het boven beschreven proces herhaalt zich op een dieper niveau.
Req. spec.
Design
Sub Req. Spec.
Testing
Design Structure
Testing
System
Integration
Sub System
Implementation
Fig. 1 Ontwikkeltraject Tijdens het ontwikkelproces zal men willen checken of men op de goede weg is. Zo zal men het ontwerp willen valideren, d.w.z. aan de hand van de specificaties checken of het ontwerp goed is. Validatie heeft als doel het bepalen in welke mate een model of simulatie met de bijbehorende data nauwkeurige representaties zijn van de
7
echte wereld, vanuit het perspectief van het beoogde gebruik. Validatie behoort niet tot de stof van deze module en daarom zal daar verder niet op worden ingegaan. Als de deelsystemen van een niveau gereed zijn (d.w.z. geïmplementeerd en elk getest tegen hun afzonderlijke specificaties), dan kunnen ze samengenomen worden (integratie) waarna de aldus geïmplementeerde hoger-niveau module getest kan worden tegen zijn specificatie. We zullen in een later hoofdstuk zien, dat dit samennemen van modules tot een hoger-niveau module niet ineens hoeft plaats te vinden, maar ook stap voor stap kan gebeuren (zgn. "incrementeel testen"). Zo zien we dat op de lagere niveaus alle deelsystemen (modules) afzonderlijk ontwikkeld en getest worden. Dit noemen we moduletests. Wanneer uiteindelijk het gehele systeem gereed is, vindt de test op het hoogste niveau plaats. Dit noemen we de systeemtest. Deze vindt meestal plaats door de uitleverende instantie, de softwareproducent. Zoals in het vierde gebod van het vorige hoofdstuk is duidelijk gemaakt, kan het voor de afnemer van het systeem niet voldoende zijn om te vertrouwen op deze systeemtest. Hij zal daarom vaak ook zelf een test van het gehele systeem willen uitvoeren of uit laten voeren door een onafhankelijke instantie. In dat geval spreken we van een acceptatietest. De afnemer zal het product accepteren als de acceptance test geen fouten (afwijkingen van de specificatie) aan het licht brengt. In het V-model worden de diverse niveaus van de ontwikkeling van software in beeld gebracht (fig. 2). Hierbij wordt evenwichtig aandacht besteed aan de ontwikkeling en aan de verificatie. Verificatie heeft tot doel om te bepalen dat een model of simulatie en implementatie met de bijbehorende data nauwkeurig de beschrijving en specificaties van de ontwikkelaar representeren. Elke fase kan worden geïmplementeerd door de gedetailleerde documentatie van de vorige fase. Testactiviteiten starten al aan het begin van het project, ver voor het coderen. Dit levert een aanzienlijke winst op in de doorlooptijd van het project. requirements
systeemontwerp
architectuurontwerp
moduleontwerp
acceptatietestontwerp
systeemtestontwerp
integratietestontwerp
moduletestontwerp
modulebouw (coderen) Fig. 2 V-model
8
acceptatietest
systeemtest
integratietest
moduletest
Uit de requirements (het pakket van eisen) wordt de acceptatietest ontworpen. Requirements en acceptatietestontwerp vormen de input voor het systeemontwerp. Het systeemontwerp beschrijft hoe de organisatie van het te realiseren systeem eruit gaat zien. Te denken valt hierbij aan menustructuren, datastructuren, enz. Bij het systeemontwerp wordt ook desysteemtest ontworpen. Systeemontwerp en systeemtestontwerp zijn input voor het architectuurontwerp. Dit bevat de beschrijving van de modules die ontwikkeld moeten worden met hun samenhang en interfaces. In deze fase wordt ook het ontwerp voor de integratietest gemaakt. Architectuurontwerp en ontwerp van de integratietest zijn input voor het moduleontwerp. Hier worden alle modules in detail ontworpen. Tijdens de fase van de modulebouw worden alle modules ten slotte gebouwd (gecodeerd) op basis van het moduleontwerp en de ontwerpen van de moduletesten. Hier wordt de software geschreven. De modules worden vervolgens getest met behulp van het moduletestontwerp. Hierna worden de interfaces tussen de modules getest met de integratietest. Bij de systeemtest wordt de systeemspecificatie vergeleken met het werkelijke systeem. Ten slotte wordt nagegaan of het systeem zich ook werkelijk gedraagt volgens de requirements. Dit gebeurt bij de acceptatietest.
9
4
Code inspectie
Vele jaren lang beschouwden programmeurs hun producten als een privéaangelegenheid. Er bestond een grote weerzin tegen het laten bekijken van je code door anderen. Als het programma maar goed werkte, dan werd het als goed beschouwd. Deze houding is in de zeventiger jaren langzamerhand veranderd, mede onder invloed van Weinberg's boek "The psychology of Computer Programming". Weinberg gaf aan hoe belangrijk het was om code juist wél door anderen te laten bekijken. Het blijkt namelijk een zeer effectieve manier om fouten op te sporen en de kwaliteit van programma's sterk te verhogen. Sindsdien zijn programmeurs het meer en meer als normaal gaan zien om hun product als "public domain" te beschouwen, althans binnen de grenzen van hun bedrijf. Veel softwarebedrijven gebruiken een of meer van de in dit hoofdstuk beschreven technieken als vast onderdeel van hun software ontwikkelproces. De technieken worden toegepast tijdens of na het coderen, maar voordat het testen begint. Ook al zijn de hier behandelde technieken niet formeel van karakter, ze zijn wel uiterst effectief. Fouten worden in een vroeg stadium opgespoord en zijn dus goedkoop te herstellen. Omdat de code zelf bekeken wordt, is de plaats van de fout direct bekend. Foutzoeken (debugging) is dus niet nodig en de kans dat de fout goed wordt hersteld is groot. Onderstaande technieken worden toegepast vóór de officiële testfase.
4.1
Desk Checking
Desk Checking is een reeds decennia lang toegepaste techniek, waarbij de programmeur zelf zijn of haar eigen programma grondig inspecteert op fouten alvorens de testfase wordt ingegaan. Daarbij kan een checklist van veelgemaakte fouten worden gehanteerd. Zo'n lijst zal straks uitgebreider aan bod komen bij de Code Review/Inspection techniek (zie §4.2). Ook is het mogelijk om van de testafdeling reeds enkele testcases aangereikt te krijgen en deze "met de hand" door het programma te laten uitvoeren, waarbij de "werking" van de code kritisch bekeken wordt. In de meeste gevallen zal Desk Checking weinig productief zijn. Een reden is het onsystematische karakter ervan. Een veel belangrijkere reden is het reeds in hoofdstuk 2 genoemde principe, dat het weinig effectief is om een programmeur zijn of haar eigen programma te laten testen. Daarom ligt het meer voor de hand om een andere persoon dan de programmeur te vragen om het Desk Checking proces uit te voeren. Desk Checking blijft echter een solitaire activiteit. Veel effectiever is het om een dergelijk proces in teamverband uit te voeren. Een paar zien meer dan één. Er ontstaat een soort competitiesfeer waarin men erop gebrand is fouten te vinden en ook door de onderlinge interactie kom je op ideeën. Deze activiteiten in teamverband worden Code Review, Code Inspection dan wel Code Walkthrough genoemd en komen hierna uitgebreider aan de orde.
4.2
Code Review/Inspection
Code Review, ook wel Code Inspection genoemd, is een methode die direct afstamt van de ideeën van Weinberg. Het houdt in dat code visueel wordt geïnspecteerd
10
door een team van personen, die ieder vooraf de code hebben bestudeerd. Het doel van de Code Review sessie is in principe om fouten te vinden, niet om ze op te lossen. Teamsamenstelling Een Code Review team is samengesteld uit een viertal personen. Eén van hen is de programmeur. Een andere persoon speelt de rol van moderator. De moderator is een ervaren programmeur, veelal een Quality Control functionaris, die echter niet op de hoogte hoeft te zijn van alle details van het te reviewen programma. Tot de taken van de moderator behoort het plannen van de Code Review sessie, het verspreiden van het materiaal voor de sessie, het voorzitten van de sessie, het registreren van de gevonden fouten en het achteraf checken of de gevonden fouten ook daadwerkelijk hersteld zijn. De overige teamleden zijn meestal de programmaontwerper (als dit niet dezelfde persoon is als de programmeur) en een testspecialist. Procedure De algemene procedure is als volgt: De moderator stuurt ruim op tijd de specificatie en de listing van het programma toe aan de teamleden, die zich op hun beurt vóór de sessie in het materiaal verdiepen. De sessie zelf bestaat uit twee gedeelten. Eerst presenteert de programmeur het programma en legt statement voor statement de logica van het programma uit. Intussen stellen de andere teamleden vragen om achter mogelijke fouten te komen. De ervaring leert, dat het juist de programmeur zelf is die, op grond van de gestelde vragen, de meeste fouten ontdekt! Het simpelweg "voordragen" van eigen werk aan een publiek blijkt dus een zeer effectieve foutdetectietechniek te zijn. Het tweede gedeelte van de sessie bestaat uit het afwerken van een checklist met veelgemaakte fouten. De inhoud van zo'n checklist zal straks afzonderlijk worden behandeld. De moderator bewaakt tijdens de sessie het proces, bv. dat de deelnemers zich richten op het vinden van fouten en niet op het oplossen daarvan. De sessie moet in totaal niet langer duren dan zo'n anderhalf à twee uur. Aangezien het een vrij inspannend werk is, leveren langere sessies nauwelijks meer op. De verwerkingssnelheid blijkt in de praktijk zo'n 150 statements per uur te bedragen. Dit houdt in dat grote programma's in meer dan één sessie behandeld dienen te worden, bij voorkeur een of meer gehele modules per sessie. Na de sessie geeft de moderator aan de programmeur een lijst met gevonden fouten. Als het aantal fouten groot is of als een fout een flinke aanpassing van het programma vereist, dan wordt een afspraak gemaakt voor een vervolgsessie voor het reviewen van de gemodificeerde code. Ook zal de foutenlijst worden geanalyseerd voor het verder verbeteren van de checklist. Checklist Hieronder volgt een voorbeeld van een (deel van) een checklist, die gebruikt kan worden in het tweede deel van een Code Review sessie. De items zijn gerubriceerd naar onderwerp. A
Declaraties A1 Zijn alle variabelen expliciet gedeclareerd? In sommige talen (bv. Basic varianten) worden variabelen impliciet gedeclareerd op het moment dat ze voor het eerst gebruikt worden of door naar de
11
A2
A3
A4
A5
B
eerste letter van de variabelenaam te kijken. Dit is een bron van fouten. Het komt bijvoorbeeld nogal eens voor, dat er een typefout wordt gemaakt in de variabelenaam. Dan is er opeens sprake van een heel andere variabele dan de bedoelde; de werking van het programma is dan onverklaarbaar omdat de foute spelling met het oog niet gemakkelijk wordt opgemerkt. Zijn er variabelen globaal gedeclareerd, die eigenlijk locaal gedeclareerd hadden moeten zijn? Alleen die variabelen dienen globaal te worden gedeclareerd die onmisbaar zijn voor de communicatie tussen programmamodules. Tellervariabelen, die in meer dan één module voorkomen (bv. de bekende i) dienen nimmer globaal gedeclareerd te worden! Als variabelen impliciet worden geïnitialiseerd op het moment van declaratie, komt die initiële waarde dan overeen met wat in het programma wordt verwacht? Het verdient de voorkeur om variabelen expliciet te initialiseren. Temeer omdat de initiële waarden tussen de verschillende compilers of compilerversies van een taal nog kunnen verschillen! Is het waardebereik van de gedeclareerde variabelen voldoende? Afhankelijk van compiler of compilerversie is een integer bv. 2 of 4 bytes, waarmee resp. 65.000 of 4.000.000.000 waarden mogelijk zijn. Ook moet worden gelet op zaken als precisie van floating-point getallen, default lengten van strings, etc. Kloppen eventuele declaratie-qualifiers? In verschillende talen kan behalve het type ook een nadere qualifier worden aangegeven, bv. in C: static, extern, register, etc.
Gebruik van variabelen B1 Heeft elke variabele een waarde gekregen voordat de waarde ervan wordt gebruikt? Dit is een van de grootste foutoorzaken. Toon (evt. informeel) voor elke referentie naar een variabele, array-element, recordveld aan dat ze een waarde hebben. B2 Ligt elke array-index binnen de grenzen? B3 Worden array-indices consequent vanaf 0 of vanaf 1 genummerd? B4 Ligt elke referentie aan de elementen van een string binnen de lengte van die string? B5 Voor elke referentie via een pointervariabele: wijst die pointervariabele inderdaad naar een gealloceerd stuk geheugen? Dit staat bekend als het "dangling reference" probleem. Het ontstaat vooral in situaties waarin de levensduur van een pointer langer is dan die van de dynamische variabele waaraan gerefereerd wordt. B6 Als de inhoud van een geheugengebied verschillend geïnterpreteerd kan worden, heeft het gebied dan een waarde die past bij elke referentie aan dat gebied? Dit treedt bijvoorbeeld op bij buffers voor dataconversie of bij variant records (unions in C). B7 Klopt de aan een variabele toegekende waarde met zijn type? Sommige talen passen geen sterke typechecking toe, maar converteren zo mogelijk het type automatisch (bv. C, Visual Basic). Zo kunnen fouten onopgemerkt blijven.
12
B8 Klopt de waarde van een geheugengebied waarnaar via een pointervariabele verwezen wordt met het verwachte datatype? Vooral met generische pointers (bv. Turbo Pascal type Pointer, C type void *) kan het voorkomen dat de pointer wijst naar de waarde van een ander type dan wat wordt verwacht als die waarde wordt gebruikt. B9 Als een datastructuur in meerdere programmamodules wordt gebruikt, is die datastructuur dan in al die modules gelijk gedefinieerd? C
Rekenfouten C1 Bevatten expressies variabelen met inconsistente types? C2 Vinden er onbedoelde of ongewenste afrondingen plaats? In een expressie met een mix van integers, enkele en dubbele precisie floating-point variabelen kan, afhankelijk van de compiler, onbedoeld tussentijds afronding naar integer of enkele precisie optreden. Ook kan een tussentijdse afronding in een expressie een grote invloed hebben op de eindwaarde, bv. bij tussentijdse deling door een zeer klein getal. C3 Klopt de expressie met de prioriteits- en associativiteitsregels van de taal? Gebruik liever wat teveel haakjes om een goede werking zeker te stellen. C4 Kan er tijdens de evaluatie van een expressie overflow of underflow optreden? Een tussentijdse waarde kan te groot of te klein zijn gegeven de datarepresentatie van de machine. C5 Bij expressies met een deling: is deling door nul mogelijk?
D
Fouten in conditionele expressies D1 Zijn de beide leden van een vergelijking van een zelfde of compatibel type? D2 Klopt de vergelijkingsoperator? Vaak staat er "groter dan" of "kleiner dan" teken waar er een "groter of gelijk" of "kleiner of gelijk" teken had moeten staan of omgekeerd. D3 Klopt de logische expressie (booleaanse operatoren)?
Houding van de teamleden Om het Code Review proces effectief te laten verlopen is het van groot belang dat de programmeur de juiste houding heeft. Als de opmerkingen tijdens de sessie worden opgevat als een aanval op de persoon, dan leidt dit tot een defensieve houding, die de effectiviteit van het proces ondermijnt. De programmeur zal daarentegen het proces juist als positief en constructief moeten zien, als een bijdrage aan de kwaliteit van het uiteindelijke product. Om deze houding te ondersteunen verdient het de voorkeur dat de Code Review sessie een besloten karakter heeft. Ook mogen de resultaten niet door het management worden misbruikt om een beeld te krijgen van de kwaliteit van de programmeurs. Ook de andere sessieleden moeten hun opmerkingen maken op een neutrale, zakelijke manier, in de wetenschap dat ook zij zelf in de rol van programmeur fouten zouden hebben gemaakt. Effectiviteit Dat Code Review zoveel effectiever is dan Desk Checking heeft te maken met wat in het "vierde gebod" is gesteld: juist anderen dan de programmeur zelf zijn beter in staat om objectief de kwaliteit van het programma te beoordelen.
13
De ervaring heeft aangetoond dat in standaard programma's met Code Review 30% tot 70% van de logische ontwerp- en codeerfouten wordt gevonden! Dit percentage heeft betrekking op het totaal aantal fouten, dat uiteindelijk gevonden wordt, niet het werkelijke aantal fouten in het programma, want zoals eerder is uitgelegd kunnen we het werkelijke aantal fouten niet kennen. Deze hoge effectiviteit betekent een enorme kostenbesparing, immers hoe vroeger fouten worden gedetecteerd, des te gemakkelijker en goedkoper kunnen zij worden hersteld. Minder effectief is Code Review met betrekking tot het detecteren van hoog-niveau ontwerpfouten. Deze probeert men veelal op te sporen met een Design Review sessie als afsluiting van de ontwerpfase. Deze validatietechniek heeft veel met Code Review gemeen. Als Code Review wordt toegepast, is er dan nog wel een testfase gebaseerd op Black en White Box technieken nodig? Het zal duidelijk zijn, dat met Code Review vooral bepaalde typen fouten gevonden zullen worden, namelijk die voor het menselijk oog gemakkelijk waarneembaar zijn. Andere soorten fouten zullen echter gemakkelijker door de Black/White Box testtechnieken worden opgespoord. Deze technieken sluiten elkaar dus niet uit maar zijn juist complementair. De Code Review techniek is niet alleen van belang voor nieuwe programma's, maar evenzeer of zelfs nog meer voor het aanbrengen van veranderingen in bestaande programma's. Het is immers gebleken, dat juist bij het aanpassen van bestaande software de meeste fouten worden gemaakt. De techniek heeft voorts nog enkele interessante zijeffecten naast het hoofddoel: het vinden van fouten. De programmeur leert van de gemaakte fouten, krijgt feedback met betrekking tot zijn programmeerstijl en de gekozen oplossingen. Ook de andere sessieleden leren van de goede oplossingen en stijl van de programmeur. Ten slotte wordt het tijdens de sessie duidelijk welke delen van het programma het meest kritiek en foutgevoelig zijn. Daar kan men vervolgens bij het ontwikkelen van testcases in het bijzonder de aandacht op richten.
4.3
Code Walkthrough
Code Walkthrough heeft veel overeenkomsten met Code Review en wordt daar ook vaak mee verward. Een overeenkomst is dat ook bij Code Walkthrough er sprake is van een team, dat voorafgaand aan de sessie de code bestudeert. Ook hier is het doel van de sessie in principe om fouten te vinden, niet om ze op te lossen. Net als bij Code Review is er sprake van een follow-up proces en van neveneffecten zoals het ontdekken van foutgevoelige programmadelen en van tekortkomingen in stijl of programmeertechniek. Het verschil zit echter in de procedures en de techniek die gebruikt wordt om fouten op te sporen. Teamsamenstelling Een Code Walkthrough team is samengesteld uit drie tot vijf personen. Eén van hen is de programmeur. Een andere persoon speelt de rol van moderator, net als bij Code Review. Tot de taken van de moderator behoort het plannen van de Code Walkthrough sessie, het verspreiden van het materiaal voor de sessie, het voorzitten van de sessie en het achteraf checken of de gevonden fouten ook daadwerkelijk hersteld zijn. Een derde teamlid is secretaris; deze persoon registreert de gevonden fouten. De overige teamleden kunnen allerlei personen zijn, bv. een zeer ervaren programmeur, een programmeertaalexpert, een junior programmeur (met mogelijk een nieu-
14
we inbreng), degene die het programma zal gaan onderhouden of iemand uit hetzelfde of juist een ander project. Procedure De algemene procedure is als volgt: De moderator stuurt ruim op tijd de specificatie en de listing van het programma toe aan de teamleden, die zich op hun beurt vóór de sessie in het materiaal verdiepen. Ook wordt iemand aangezocht als tester. Deze persoon vervaardigt een aantal representatieve maar niet te moeilijke testcases. Gedurende de sessie zelf spelen de deelnemers voor computer. Ze gebruiken daarbij de aangeleverde testcases. Deze testcases worden met de hand uitgevoerd, dus stap voor stap worden de statements van het programma uitgevoerd terwijl de inhoud van de variabelen wordt bijgehouden. De testcases zelf spelen hierbij geen cruciale rol. Het gaat er slechts om dat de code doorlopen wordt en dat aan de programmeur de juiste vragen omtrent de code worden gesteld. De ervaring wijst uit dat er meer fouten gevonden worden door de programmeur te bevragen dan door de testcases zelf. Houding van de teamleden Ook bij Code Walkthrough is een juiste houding van de teamleden van het grootste belang. Opmerkingen dienen gericht te zijn aan het adres van het programma, niet van de programmeur. Of, om het anders te zeggen, fouten moeten niet worden gezien als een tekortkoming van de programmeur, maar als iets wat onontkoombaar hoort bij het vervaardigen van complexe programma's.
4.4
Peer Rating
Peer Rating is geen techniek die gericht is op het vinden van fouten en hoort daarom in dit overzicht van technieken eigenlijk niet thuis. Toch is het een nauw verwante techniek omdat hij gebaseerd is op het kritisch beschouwen van geproduceerde code. Peer Rating omvat het evalueren van onder meer de volgende aspecten van de code: algehele kwaliteit onderhoudbaarheid uitbreidbaarheid bruikbaarheid structuur Het doel van de techniek is om programmeurs een spiegel voor te houden. Op grond van deze zelfevaluatie kunnen de programmeurs een betere wijze van programmeren ontwikkelen. Een van de beschikbare programmeurs wordt aangewezen als administrateur van het proces. Deze administrateur selecteert vervolgens minstens 6 en maximaal zo'n 20 deelnemers aan het proces. De deelnemers dienen allen een vergelijkbare achtergrond te hebben. Het zijn bijvoorbeeld allen C-programmeurs. Aan iedere deelnemer wordt gevraagd om twee eigen programma's uit te kiezen om te laten beoordelen. Eén programma dient door de programmeur zelf als een prima werkstuk beschouwd te worden en het andere programma beschouwt de programmeur zelf als van mindere kwaliteit. Als de programma's verzameld zijn, worden ze random ver15
deeld over de deelnemers. Elke deelnemer krijgt vier programma's te beoordelen: twee "prima" programma's en twee "mindere" programma's. De beoordelaar krijgt echter niet te horen welke van de programma's als "prima" of "minder" worden beschouwd. Elke deelnemer verdiept zich ongeveer 30 minuten in elk programma en vult dan een evaluatieformulier in. Nadat alle programma's beoordeeld zijn, kent de beoordelaar aan elk programma een aantal cijfers toe. Daartoe wordt op een aantal vragen een antwoord gegeven dat kan variëren van 1 (betekenis: "absoluut wel") tot en met 7 (betekenis: "absoluut niet"). Een voorbeeld van zo'n vragenlijst is: Was het programma eenvoudig te begrijpen? Was het systeemontwerp helder van opzet? Was het moduleontwerp helder van opzet? Denk je dat je gemakkelijk een wijziging in het programma zou kunnen aanbrengen? Zou je trots zijn op dit programma als je het zelf geschreven had? Verder wordt de beoordelaar gevraagd om nog algemene opmerkingen en eventuele verbeteringen op te schrijven. Na de beoordelingsronde worden de (anonieme) evaluaties gegeven aan de betreffende programmeurs. Ook krijgen zij een statistisch overzicht, waarin staat aangegeven: hoe hun programma's scoren in vergelijking met alle andere programma's; in hoeverre de door henzelf uitgevoerde beoordelingen afwijken van de andere beoordelaars van dezelfde programma's. Kortom, het statistische overzicht is een maat voor de kwaliteit van zowel de programmeervaardigheden als de beoordelingsvaardigheden van de deelnemers. Op grond van de resultaten kunnen de programmeurs hun vaardigheden verbeteren. Deze techniek is zowel bruikbaar in een industriële ontwikkelomgeving alsook in het onderwijs.
16
5
Functioneel testen
5.1
Black Box testcase ontwerp
Het is mogelijk om een krachtige verzameling testcases te ontwikkelen puur op basis van de specificaties van een systeem of module. Deze activiteit kan starten zodra die specificaties gereed zijn en kan dus parallel lopen met de implementatie. Aangezien uitputtend testen vrijwel altijd onhaalbaar is, maken we gebruik van technieken die een beperkte maar krachtige verzameling testcases opleveren. Dit zijn achtereenvolgens: Equivalence Partitioning, Boundary Value Analysis, Cause-Effect Graphing en Error Guessing. Elk van deze technieken produceert een verzameling bruikbare testcases die echter op zichzelf niet voldoende dekking bieden. Daarom worden de technieken gecombineerd toegepast in volgorde waarbij het zinnig is om Boundary Value Analysis na Equivalence Partitioning toe te passen en Error Guessing als laatste.
5.1.1
Equivalence Partitioning
Aangezien het onmogelijk is om een programma uitputtend te testen, dus om alle mogelijke inputcombinaties aan te bieden, moeten we ons beperken tot een (kleine) deelverzameling van alle mogelijke inputs. Die deelverzameling moet dan krachtig genoeg zijn om toch zoveel mogelijk fouten aan te tonen. Het genereren van zo'n krachtige deelverzameling wordt in belangrijke mate ondersteund door de Equivalence Partitioning techniek. Bij deze techniek wordt de verzameling mogelijke inputs onderverdeeld ("gepartitioneerd") in een beperkt aantal equivalentieklassen. Onder zo'n equivalentieklasse verstaan we een zodanige verzameling van inputs dat we redelijkerwijs kunnen aannemen, dat het testen met één representatieve input voldoende is om de hele klasse te testen. Het testen met een andere input uit dezelfde klasse wordt zinloos geacht omdat daarmee in principe geen andere fouten worden aangetoond. Met andere woorden, als twee testcases a en b tot dezelfde equivalentieklasse behoren dan zal een fout die met a wordt aangetoond ook door b worden aangetoond. Bovendien zal een fout die door a niet wordt aangetoond ook door b niet worden aangetoond. Voorbeeld Gegeven de specificatie van programma MeanOfTable (zie bijlage 2). Het heeft geen zin om de volgende verzameling testcases allemaal uit te proberen: Nr: Inputs: Verwachte outputs: 1 aiTable = <2,3,3> 2,6667 2 aiTable = <2,3,4> 3,0000 3 aiTable = <2,3,5> 3,3333 4 aiTable = <2,3,6> 3,6667 5 aiTable = <2,3,4,5> 3,5000 6 aiTable = <2,3,4,5,6> 4,0000 7 aiTable = <2,3,4,5,6,7> 4,5000 Immers als testcase 1 geen fout aan het licht brengt, zal dat waarschijnlijk ook niet het geval zijn met testcase 2 t/m 4, die slechts in de waarde van het derde getal ver17
schillen. Evenmin is het waarschijnlijk, dat testcase 5 t/m 7 opeens een fout zullen onthullen, waarbij steeds één getal is toegevoegd aan het vorige aantal. Daarom is het aannemelijk, dat testcase 1 volstaat voor de gehele verzameling 1 t/m 7. Er is hier sprake van twee equivalentieklassen; de ene klasse heeft te maken met de waarde van de getallen in de tabel, de andere met het aantal getallen. Testcase 1 behoort tot beide klassen en is daarom voldoende voor het testen van beide klassen. In tegenstelling tot het mathematische begrip "equivalentieklasse" kunnen Equivalence Partitioning klassen elkaar wel overlappen. Dat wordt ook in bovengenoemd voorbeeld duidelijk. De oorzaak is gelegen in het feit, dat er equivalentieklassen geformuleerd worden voor verschillende aspecten (zoals boven voor "waarde" en "aantal"). Het kan gemakkelijk voorkomen dat een testcase tegelijkertijd meer dan één aspect test. Als meerdere klassen elkaar overlappen is het daarom voordelig om een testcase te kiezen die tot zoveel mogelijk klassen behoort. De Equivalence Partitioning techniek bestaat uit twee stappen: 1. het identificeren van de equivalentieklassen, 2. het genereren van een testcase per equivalentieklasse. Stap 1: Identificeren van de equivalentieklassen We kunnen equivalentieklassen afleiden door de specificaties nauwkeurig te lezen en daaruit invoercondities te destilleren op grond waarvan er twee of meer invoergroepen te onderscheiden zijn. Zo'n invoerconditie is vaak niet meer dan een enkele zin of zinsnede. Op grond van deze analyse vullen we de tabel met equivalentieklassen in (zie fig. 3). externe conditie
geldige klassen
ongeldige klassen
Fig. 3 Tabel met equivalentieklassen Merk op dat er twee soorten equivalentieklassen zijn. Geldige klassen hebben betrekking op normale, geldige invoerwaarden. Ongeldige klassen hebben betrekking op foutieve invoerwaarden, die vaak aanleiding zullen geven tot foutmeldingen (overeenkomstig hetgeen in de specificaties is aangegeven). Het overlappen van equivalentieklassen zal voornamelijk optreden bij geldige klassen. Bij een ongeldige klasse is het van belang om een aparte testcase te spenderen aan juist die ene ongeldige situatie! Het vergt enige oefening voordat men in staat is om goed equivalentieklassen te onderscheiden. Er zijn geen regels voor te geven, hoogstens enkele richtlijnen: 1. Als een invoerconditie betrekking heeft op een waardenbereik, bv. "integer parameter p kan de waarde 0 t/m 999 hebben", dan geeft dit aanleiding tot één geldige equivalentieklasse (0 t/m 999) en twee ongeldige equivalentieklassen (<0 en >999). Sommige testontwikkelaars rekenen de waarden 0 en 999 niet tot de geldige klasse omdat het randgevallen zijn (zie de Boundary Value Analysis techniek). In dat geval vormen de waarden 0 en 999 zelfstandige geldige klassen. Of
18
2.
3.
4.
5.
dergelijke randgevallen wel of niet als aparte klassen gerekend moeten worden, kan de testontwikkelaar het beste beoordelen aan de omstandigheden, bijvoorbeeld een vermoeden hoe de programmeur de code waarschijnlijk schrijft. Als een invoerconditie een aantal waarden aangeeft, bv. "de tabel T kan 1 tot maximaal 100 records bevatten", dan leidt ook dit tot één geldige equivalentieklasse (1 t/m 100 records) en twee ongeldige klassen (geen records en meer dan 100 records). Als een invoerconditie een verzameling waarden aangeeft, vorm dan een equivalentieklasse voor elke deelverzameling waarvoor het programma eenzelfde gedrag behoort te vertonen. Als een invoerconditie een dient-te-zijn-situatie aangeeft, bv. "het eerste teken dient een letter te zijn", vorm dan een geldige equivalentieklasse (eerste teken is een letter) en een ongeldige equivalentieklasse (eerste teken is geen letter). Als er reden is om aan te nemen dat het programma de elementen van een equivalentieklasse niet gelijkelijk behandelt, splits de equivalentieklasse dan in kleinere klassen totdat dit per klasse wel het geval is.
Stap 2: Genereren van testcases De volgende stap is nu het genereren van een testcase per equivalentieklasse: 1. Wijs aan elke equivalentieklasse een uniek nummer toe. Initieel zijn alle equivalentieklassen "niet afgedekt". 2. Genereer een testcase die zoveel mogelijk van de nog niet afgedekte geldige klassen afdekt. Herhaal dit totdat alle geldige klassen zijn afgedekt. 3. Genereer een testcase die precies één van de nog niet afgedekte ongeldige klassen afdekt. Herhaal dit totdat alle ongeldige klassen zijn afgedekt. Het is belangrijk dat ongeldige klassen elk door een eigen testcase worden afgedekt, immers ongeldige situaties kunnen elkaar maskeren. Elke ongeldige klasse leidt vaak tot een eigen foutboodschap en als we met één testcase twee ongeldige klassen zouden afdekken is de kans groot dat slechts één van beide foutboodschappen wordt gegeven. Voorbeeld Gegeven de volgende specificatie van het programma 'Raad het getal': Het programma kiest zelf random een te raden getal. Elke invoer moet worden gelezen als string en afgesloten met enter. Het programma vraagt om je naam in te voeren. Deze naam moet uit minstens 1 en hoogstens 30 karakters bestaan. Hierin mogen geen cijfers voorkomen. Bij een verkeerde naam wordt de foutboodschap "Ongeldige naam" getoond. Bij een correcte naam vraagt programma vervolgens om het getal te raden. Het raadgetal moet minstens 10 en hoogstens 300 zijn en een geheel getal. Bij een verkeerde invoer wordt de foutboodschap "Ongeldig getal" getoond. Het programma geeft als uitvoer de boodschap "Te klein" als raadgetal < te raden getal, "Te groot" als raadgetal > te raden getal of "Geraden" als raadgetal = te raden getal. Als het getal na 6 keer nog niet is geraden, wordt de boodschap "Niet geraden" getoond. Hierna wordt gevraagd of de gebruiker nog een keer wil spelen. Het antwoord hierop mag alleen 'j' of 'n' zijn (dus 1 enkele letter). Bij een foutief antwoord gebeurt er niets en wordt er gewacht op een goed antwoord.
19
externe conditie naam
geldige klassen (e1) alle karakters, behalve cijfers (e2) 1 … 30 karakters lang
raadgetal
(e6) geheel getal (e7) waarden 10 … 300 (e8) raadgetal < te raden getal (e9) raadgetal > te raden getal (e10) raadgetal = te raden getal (e11) te raden getal = random (e17) één karakter (e18) "n" (e19) "j" (e23) maximaal 6 keer per speelronde
antwoord op nog eens
aantal keer raden
ongeldige klassen (e3) minstens één cijfer aanwezig (e4) geen enkel karakter (e5) meer dan 30 karakters lang (e12) niet-geheel getal (e13) minstens één niet-cijfer karakter (e14) kleiner dan 10 (e15) groter dan 300 (e16) te raden getal is niet random (e20) geen karakter (e21) meer dan één karakter (e22) ander karakter dan n of j (e24) meer dan 6 keer per speelronde
Fig. 4 Equivalentieklassen bij 'Raad het getal' De testcases e11 en e16 zijn niet afzonderlijk uit te voeren, immers als de te raden getallen random zijn, dan impliceert dit automatisch dat ze niet niet-random zijn en omgekeerd. Er zijn drie mogelijkheden: 1. Het te raden getal is telkens hetzelfde. Dit wordt geconstateerd door enkele raadrondes te spelen. 2. De reeks te raden getallen is steeds dezelfde. Hiertoe moet het programma een tweetal keren achter elkaar worden opgestart om na te gaan of de reeksen niet hetzelfde zijn. Mogelijk moeten beide keren een aantal raadrondes worden gespeeld. 3. De reeks te raden getallen is niet hetzelfde. Zie punt 2. De testcases e21 en e22 zijn niet afzonderlijk te testen, omdat na 6 keer foutief raden de speelronde stopt met de boodschap "Niet geraden" en er wordt gevraagd of er nog een keer gespeeld moet worden. Is dit niet het geval en wordt er een 7 e keer gespeeld, dan is meteen bekend dat er een fout in het programma zit. De 'e' staat voor equivalence partitioning.
20
testcase e1,e2 e3 e4 e5 e6,e7,e8 e9 e10 e11, e16
invoer verwachte uitvoer naam = _Rouland- ?.; programma vraagt om raadgetal naam = Rouland2 foutboodschap "Ongeldige naam" naam = foutboodschap "Ongeldige naam" naam = Docent_Joseph_Gerardus_Rouland++ foutboodschap "Ongeldige naam" naam = Jos en raadgetal < te raden getal * boodschap "Te klein" naam = Jos en raadgetal > te raden getal * boodschap "Te groot" naam = Jos en raadgetal = te raden getal * boodschap "Geraden" start het programma twee keer en voer per keer een twee verschillende reeksen te raden getallen aantal raadspellen uit e12 naam = Jos en raadgetal = 12.5 foutboodschap "Ongeldig getal" e13 naam = Jos en raadgetal = 15a foutboodschap "Ongeldig getal" e14 naam = Jos en raadgetal = 5 foutboodschap "Ongeldig getal" e15 naam = Jos en raadgetal = 320 foutboodschap "Ongeldig getal" e17,e18 antwoord = n programma stopt e19 antwoord = j programma vraagt om raadgetal e20 antwoord = er gebeurt niets; programma wacht op goed antwoord e21 antwoord = ja er gebeurt niets; programma wacht op goed antwoord e22 antwoord = N er gebeurt niets; programma wacht op goed antwoord e23,e24 naam = Jos en geef 6 x raadgetal ≠ te raden getal * boodschap "Niet geraden"; vraagt om nog een spel * Om deze test uit te voeren is het handig als het te raden getal (tijdens de testfase) wordt afgedrukt. Fig. 5 Testcases volgens de equivalentieklassen bij 'Raad het getal'
21
5.1.2
Boundary Value Analysis
De ervaring leert dat er erg veel fouten gemaakt worden op grenswaarden, zowel wat betreft invoer- als uitvoerwaarden. Vaak wordt aan de rand net een waarde te veel of te weinig meegenomen. Boundary Value Analysis beoogt om de waarden op en net buiten de grenswaarden te testen. Voor wat betreft invoergrenswaarden zijn vaak de Equivalence Partitioning klassen geschikt om deze te bepalen. Daarnaast is het ook van belang om, zo mogelijk, te testen op uitvoergrenswaarden, dat wil zeggen uitvoerwaarden die op of net buiten een grenswaarde liggen. Ook voor Boundary Value Analysis geldt dat er geen vaste regels voor zijn te geven, hoogstens enkele richtlijnen: 1. Als een invoerconditie betrekking heeft op een waardenbereik, bv. "integer parameter p kan de waarde 0 t/m 999 hebben", genereer dan testcases voor de grenswaarden zelf (0 en 999) en voor de waarden net buiten dit bereik (-1 en 1000). 2. Als een invoerconditie een aantal waarden aangeeft, bijvoorbeeld "de tabel T kan 1 tot maximaal 100 records bevatten", genereer dan testcases voor het minimale en het maximale aantal (resp. 1 en 100) en zo mogelijk voor waarden één lager dan het minimum (0) en één hoger dan het maximum (101). 3. Pas richtlijn 1 toe op elke uitvoerconditie. Bijvoorbeeld: "de functie GiftenAftrek berekent het voor de inkomstenbelasting aftrekbare bedrag voor giften. Dit aftrekbare bedrag bestaat uit het totale bedrag aan giften voor zover het 1% van het onzuivere inkomen te boven gaat, met een maximum van 10% van het onzuivere inkomen." De grenzen zijn nu te testen door zodanige invoerwaarden te kiezen dat: a. 1% van het onzuiver inkomen precies bereikt wordt; verwachte uitvoerwaarde: 0 euro. b. 1% van het onzuiver inkomen net niet bereikt wordt; verwachte uitvoerwaarde: 0 euro. c. 1% van het onzuiver inkomen net overschreden wordt; verwachte uitvoerwaarde: 1 euro. d. 11% van het onzuiver inkomen precies bereikt wordt (1% drempel + 10% maximale aftrek); verwachte uitvoerwaarde: 10% van het onzuivere inkomen. e. 11% van het onzuiver inkomen net overschreden wordt; verwachte uitvoerwaarde: 10% van het onzuivere inkomen. 4. Pas richtlijn 2 toe op elke uitvoerconditie. Bijvoorbeeld: "Het programma toont een lijst met namen. Als het aantal namen groter is dan 10, dan zal de lijst per 10 namen getoond worden, waarbij elke toetsindruk de volgende 10 namen laat zien (of minder als er geen 10 meer te tonen zijn)." We kunnen hier zodanig testcases genereren, dat we precies 10 namen te tonen hebben of net één meer (11). N.B. Het testen van 20, 21, 30, 31, ... namen heeft weer geen zin op grond van Equivalence Partitioning. 5. Als de invoer of uitvoer van een programma bestaat uit een geordende verzameling (bijvoorbeeld een sequentiële file, een tabel of een geordende lijst), richt dan testcases op het eerste en het laatste element van die verzameling. Naast deze richtlijnen zal het gebruik van het gezonde verstand mogelijk ook nog enkele interessante randcondities aan het licht brengen die de moeite van het testen waard zijn.
22
testcase b1 b2 b3 b4 b5 b6 b7 b8 b9 b10 b11 b12 b13 b14 b15
invoer verwachte uitvoer naam = foutboodschap "Ongeldige naam" naam = R programma vraagt om raadgetal naam = Docent_Joseph_Gerardus_Rouland programma vraagt om raadgetal naam = Docent_Joseph_Gerardus_Rouland+ foutboodschap "Ongeldige naam" naam = Jos en raadgetal = 9 foutboodschap "Ongeldig getal" naam = Jos en raadgetal = 10 boodschap "Te klein" (of "Geraden") naam = Jos en raadgetal = 300 boodschap "Te groot" (of "Geraden") naam = Jos en raadgetal = 301 foutboodschap "Ongeldig getal" naam = Jos en raadgetal = te raden getal – 1 * boodschap "Te klein" naam = Jos en raadgetal = te raden getal * boodschap "Geraden" naam = Jos en raadgetal = te raden getal + 1 * boodschap "Te groot" antwoord = er gebeurt niets; programma wacht op goed antwoord antwoord = n programma stopt antwoord = ne er gebeurt niets; programma wacht op goed antwoord naam = Jos, geef 5 keer raadgetal ≠ te raden getal en boodschap = "Geraden" dan het te raden getal * b16 naam = Jos en geef 6 keer raadgetal ≠ te raden getal * boodschap = "Niet geraden"; vraagt om nog een spel * Om deze test uit te voeren is het handig als het te raden getal (tijdens de testfase) wordt afgedrukt. Fig. 6 Testcases volgens Boundary Value Analysis bij 'Raad het getal' De letter 'b' in het nummer van de testcase staat voor boundary value analysis.
23
Het programma Triangle (bijlage 3) geeft goed aan hoe belangrijk Boundary Value Analysis is. Om een geldige driehoek te vormen moet aan een aantal voorwaarden voldaan zijn: a. De zijden moeten allemaal groter zijn dan nul. b. De som van elk tweetal zijden moet groter zijn dan de derde zijde. Op grond van equivalentieklassen hebben we mogelijkerwijs reeds de invoer 3-4-5 getest (geldige klasse) en de invoer 1-2-4 (ongeldige klasse). We missen nu echter een mogelijke fout, namelijk als in het programma de test A + B >= C staat in plaats van A + B > C, dan zal het programma ten onrechte 1-2-3 als geldige driehoek beschouwen. Boundary Value Analysis zal zo'n grenswaarde nu juist wel testen! Als voorbeeld van de boundary value analysis gebruiken we weer het programma 'Raad het getal" (zie fig. 6).
5.1.3
Cause-Effect Graphing
Hoe krachtig Equivalence Partitioning en Boundary Value Analysis ook mogen zijn, ze hebben toch ook een duidelijke tekortkoming: ze testen niet invoercombinaties. Zo kan het zijn dat een procedure twee parameters A en B heeft, elk met een eigen bereik. Het kan gebeuren dat het testen van het bereik van A geen fouten oplevert, het testen van het bereik van B evenmin, terwijl wanneer A en B beide op hun maximale waarde gezet worden er een overflow situatie optreedt. Het kan gemakkelijk gebeuren dat deze situatie niet door de voorgaande technieken wordt gedetecteerd. Cause-Effect Graphing beoogt nu juist invoercombinaties te testen. Deze techniek is dan ook alleen interessant, als er van dergelijke combinaties sprake is. In dat geval levert zij een krachtige verzameling testcases. Een belangrijk neveneffect van deze techniek is dat onvolkomenheden in de specificaties genadeloos worden blootgelegd. Een Cause-Effect (CE) graaf is een graaf waarin wordt gemodelleerd hoe reacties van het te testen systeem (effects) afhangen van de invoercombinaties (causes). Daartoe vertalen we de specificaties naar een soort combinatorische digitale schakeling, waarbij we echter geen digitale techniek notatie gebruiken maar een simpeler notatie met AND-, OR en NOT-knopen. Om een dergelijke graaf op te zetten en te begrijpen is enig begrip van Booleaanse logica onontbeerlijk. De volgende procedure leidt tot een verzameling testcases: 1. Splits zo mogelijk de specificatie in een aantal zelfstandige delen die onderling weinig met elkaar te maken hebben. Dit is vaak nodig omdat de CE-graaf anders onwerkbaar groot wordt. Ook kan het helpen om de specificaties in een aantal niveaus op te splitsen, die elk afzonderlijk bekeken worden. Voorbeelden: Bij het testen van een compiler stellen we voor elk type statement een CEgraaf op. Afhankelijk van een parameterwaarde heeft een programma verschillende werkingen.
24
2.
3.
4.
5.
6.
We stellen dan een graaf op waarin tot uiting komt hoe elke parameterwaarde een ander gedrag geeft en verder per gedragsvariant een afzonderlijke graaf. Een programma leest een file en voert per regel een actie uit. We maken dan een aparte graaf voor de programma-aanroep (waarbij de file al dan niet gevonden en geopend wordt) en een aparte graaf voor de verwerking van de fileregels. Vervolgens lezen we zeer nauwkeurig de specificaties om de causes en de effects in kaart te brengen. Een cause is een afzonderlijke invoerconditie (of een equivalentieklasse van invoercondities) en/of een systeemtoestand. Een effect is een uitvoerconditie en/of verandering van de systeemtoestand. Voorbeeld: Als een invoercombinatie ertoe leidt, dat het programma een bepaalde mode ingaat, dan is dit een verandering van de systeemtoestand. Een bevestigingsboodschap (of foutmelding) is echter een uitvoerconditie. Het zijn echter allemaal effects. De causes en effects worden verkregen door de specificatie woord voor woord en zin voor zin te lezen en die woorden en zinsneden te onderstrepen waarbij sprake is van een cause of effect. Elke cause of effect krijgt daarbij een eigen uniek nummer. Nu wordt de CE-graaf opgesteld, waarbij we links de causes onder elkaar plaatsen en rechts de effects, ook onder elkaar. De knopen worden rechts en links met elkaar verbonden door een Booleaanse graaf die weergeeft hoe de effects van de causes afhangen. Links worden randvoorwaarden gemodelleerd. Dat houdt in dat we aangeven hoe causes onderling gerelateerd zijn, bijvoorbeeld welke invoercombinaties niet kunnen optreden. Per effect wordt geanalyseerd wanneer dit wel en wanneer dit niet mag optreden. Dit leidt per effect tot twee logische formules, die we noteren in de distributieve vorm. Ten slotte vertalen we elke productterm uit de logische formules in een testcase.
Een CE-graaf is opgebouwd uit elementen zoals gegeven in fig. 7. Iedere knoop heeft een waarde 0 (onwaar) of 1 (waar). Voor elke functie geldt dat de linkerknopen de ingangsknopen zijn en de rechterknoop de uitgangsknoop. De identity-functie geeft aan: als a gelijk is aan 1, dan is b gelijk aan 1 en als a gelijk is aan 0, dan is b gelijk aan 0. De not-functie geeft aan: als a gelijk is aan 1, dan is b gelijk aan 0 en als a gelijk is aan 0, dan is b gelijk aan 1. De or-functie geeft aan, dat de rechterknoop 0 is als alle linkerknopen 0 zijn, anders is de rechterknoop gelijk aan 1. Een or-functie kan een willekeurig aantal ingangsknopen hebben. De and-functie geeft aan, dat de rechterknoop 1 is als alle linkerknopen 1 zijn, anders is de rechterknoop gelijk aan 0. Een and-functie kan een willekeurig aantal ingangsknopen hebben.
25
a
b
!
a
identity
b
not
a
a
b
d
b
d ●
+
c
c or
and
Fig. 7 CE-graaf symbolen
Eerste teken is C
! E1
Foutmelding "Incorrect partition"
E2
Programma stopt
E3
Foutmelding "No digit"
●
E4
Bewerking op partitie C
●
E5
Bewerking op partitie D
C1
●
N1
+ ! Eerste teken is D
C2
! Tweede teken is cijfer
!
C3
Fig. 8 Voorbeeld van een CE-graaf. Voorbeeld Gegeven de volgende specificatie van een zeer klein programma. Het programma vraagt om twee toetsen in te drukken, eerst een letter (C of D) om een diskpartitie te selecteren en dan een cijfer (0-9). Als dit correct is ingegeven, voert het programma een bewerking uit met betrekking tot partitie C of partitie D. Als
26
de eerste toets geen C of D is, dan wordt de foutmelding "Incorrect partition" gegeven en stopt het programma. Als de tweede toets geen cijfer is, dan wordt de foutmelding "No digit" gegeven en stopt het programma. De CE-graaf van deze specificatie staat gegeven in fig. 8. Merk op dat een tussen-knoop (N1) gevormd is met de intuïtieve betekenis "de eerste letter is geen C en geen D". Opdracht:
Ga na of de CE-graaf van fig. 8 inderdaad overeenstemt met bovenstaande specificatie door alle invoercombinaties uit te proberen en na te gaan wat de effecten zijn.
a
a
b
E
a
b
I
c
b
O
c
exclusive
c
inclusive
a
one-and-only-one
x
R
M b
y
requires
masks (effects)
Fig. 9 Beperkingssymbolen (constraints). Hoewel de graaf de specificaties goed weergeeft, zijn bepaalde invoercombinaties onmogelijk. Bijvoorbeeld de knopen C1 en C2 kunnen niet tegelijkertijd 1 zijn. Voor de meeste systemen gelden dergelijke beperkingen. Deze leiden tot een verminderd aantal invoercombinaties en reduceren daarom ook het aantal zinvolle testcases. De geldende beperkingen (constraints) worden in de graaf zichtbaar gemaakt door middel van speciale tekens. Een overzicht van deze tekens is gegeven in fig. 9. Hierbij staat het symbool "E" voor "exclusive", d.w.z. van alle knopen mag er ten hoogste één 1 zijn. Het is ook toegestaan dat alle knopen 0 zijn. Het symbool "I" staat voor "inclusive": minstens één van de knopen moet 1 zijn. Het is dus niet toegestaan dat alle knopen 0 zijn. Het symbool "O" geeft aan "one-and-only-one" ofwel er moet altijd precies één van de knopen 1 zijn. Het symbool "R" betekent "requires": als knoop a 1 is dan moet ook knoop b 1 zijn.
27
Merk op dat als knoop a niet 1 is, er geen enkele eis is met betrekking tot knoop b (deze mag dan 1 of 0 zijn). We kunnen er wel uit afleiden dat als knoop b 0 is dan knoop a ook 0 moet zijn. Ook tussen effects kunnen er constraints zijn. We gebruiken het symbool "M" om aan te geven dat twee effecten elkaar maskeren. Als effect x 1 is, dan kan effect y onmogelijk 1 zijn. Merk op dat hieruit ook het omgekeerde afleidbaar is: als effect y 1 is, dan kan effect x onmogelijk 1 zijn. Voor de CE-graaf van fig. 8 leidt het toevoegen van beperkingssymbolen tot de graaf van fig. 10. Eerste teken is C
!
C1
●
E1
Foutmelding "Incorrect partition"
E2
Programma stopt
E3
Foutmelding "No digit"
●
E4
Bewerking op partitie C
●
E5
Bewerking op partitie D
N1
E
+ !
Eerste teken is D
C2
! Tweede teken is cijfer
!
C3
Fig. 10 Voorbeeld van een CE-graaf met beperkingssymbolen (constraints). Merk op, dat in dit voorbeeld sprake is van een onduidelijkheid in de specificaties: Wat gebeurt er als het eerste teken geen "C" of "D" is en bovendien het tweede teken geen cijfer is? Wordt dan de boodschap "Incorrect partition" gegeven en vervolgens het programma verlaten of worden beide foutmeldingen gegeven alvorens het programma verlaten wordt? Hier wordt duidelijk dat het opstellen en analyseren van een CE-graaf vaak leidt tot het blootleggen van tekortkomingen (of zelfs strijdigheden) in de specificaties. In onze graaf zijn we uitgegaan van de tweede uitleg. Als echter de eerste uitleg bedoeld was, moeten we de graaf bijstellen. Dat laten we als oefening aan de lezer over! De volgende stap in het proces is het afleiden van de invoercombinaties die leiden tot het al dan niet optreden van elk der effecten. We gaan daarvoor per knoop afleiden wanneer die knoop 1 is en wanneer 0. De regels voor het doorrekenen van de CE-graaf zijn als volgt: 1. Als een AND-knoop 1 moet worden, dan moeten alle ingangen 1 worden. Al deze 1-ingangen moeten worden doorgerekend naar alle mogelijke invoercombinaties. 2. Als een AND-knoop 0 moet worden, dan beperken we ons tot alle mogelijke in-
28
voercombinaties waarbij slechts één ingang 0 is en de andere ingangen 1. Dit wordt path sensitizing genoemd en is bedoeld om te voorkomen dat ingangen elkaar maskeren. Dit leidt tot een belangrijke reductie van het aantal te beschouwen invoercombinaties (en dus het aantal testcases) door de minder zinvolle combinaties buiten beschouwing te laten. Alleen als beperkingsregels (constraints) dergelijke invoercombinaties onmogelijk maken kan het zin hebben om meerdere ingangen 0 te maken. Een tweede belangrijke reductie treedt op door voor elke 1-ingang slechts één invoercombinatie te kiezen, dus niet alle mogelijkheden waarbij deze ingang 1 wordt, hoeven te worden doorgerekend. De 0-ingang moet echter wel op alle mogelijk (zinvolle) invoercombinaties worden doorgerekend. 3. Als een OR-knoop 1 moet worden, dan beperken we ons tot alle mogelijke invoercombinaties waarbij slechts één ingang 1 is en de andere ingangen 0. Ook hier wordt voorkomen dat ingangen elkaar maskeren. Alleen als beperkingsregels (constraints) dergelijke invoercombinaties onmogelijk maken kan het zin hebben om meerdere ingangen 1 te maken. Voorts kiezen we voor elke 0-ingang slechts één invoercombinatie, dus niet alle mogelijkheden waarbij deze ingang 0 wordt hoeven te worden doorgerekend. De 1-ingang moet echter wel op alle mogelijk (zinvolle) invoercombinaties worden doorgerekend. 4. Als een OR-knoop 0 moet worden, dan moeten alle ingangen 0 worden. Al deze 0-ingangen moeten worden doorgerekend naar alle mogelijke invoercombinaties. 5. Als alle effecten zijn teruggerekend naar invoercombinaties leiden we per effect (zowel voor de waarde '0' als '1') per invoercombinatie een testcase af. De verwachte uitvoer (effecten) verkrijgen we door voor elke invoercombinatie ook na te gaan welke andere effecten erdoor optreden. We lichten dit toe aan de hand van het voorbeeld van fig. 10. Daarbij maken we gebruik van formules met de volgende betekenis: = Het rechterlid van deze operator geeft de voorwaarden waaronder het linkerlid geldt. # Slechts één invoercombinatie vereist. _ (onderstreping) term vervalt wegens constraints Voorbeelden C1.C3 C1.!N1 E5#.E6#.!E12
C1.C2.!C5.C8 !E18 = C1.C4 + !C3.C7.C8
Knopen C1 en C3 zijn beide '1'. Knoop C1 is '1' en knoop N1 is '0'. Knopen E5 en E6 zijn beide '1' en knoop E12 is '0'. Om de knopen E5 en E6 '1' te maken kunnen we ons beperken tot slechts één invoercombinatie. Voor knoop E12 moeten we echter alle (zinvolle) invoercombinaties doorrekenen die de knoop '0' maken. Invoercombinatie vervalt in verband met constraints. Knoop E18 wordt '0' als de knopen C1 en C4 beide '1' zijn of als knoop C3 '0' is en de knopen C7 en C8 beide '1'.
We keren nu terug naar het voorbeeldprogramma van fig. 10 en proberen daar de zinvolle invoercombinaties af te leiden. We berekenen eerst de tussenknoop en ten slotte de eindknopen.
29
N1= !C1.!C2 !N1 = C1.!C2 + !C1.C2
Volgens het theorema van De Morgan is !N1 gelijk aan C1 + C2. Ten behoeve van testen mogen we dit theorema echter niet gebruiken. Immers bij testen moeten altijd alle invoercombinaties worden geschouwd waarvoor een knoop 0 (of 1) wordt. In dit geval wordt !N1 1 (dus N1 wordt 0) voor de combinaties C1.!C2, !C1.C2 en C1.C2. Path sensitizing leidt ertoe dat de combinatie C1.C2 niet getest hoeft te worden (nog afgezien van het feit dat de E-constraint deze combinatie niet toestaat). De formule C1 + C2 zou ertoe kunnen leiden dat als testcase C1=C2=1 wordt gebruikt en juist deze testcase is niet zinvol, omdat er maskering van fouten kan optreden. Het symbool # is niet nodig omdat het hier niet om tussenknopen gaat die verder teruggerekend zouden moeten worden. E1= N1 = !C1.!C2 Zie beslissingstabel kolom 1 (fig. 11). !E1 = !N1 = C1.!C2 + !C1.C2 Zie beslissingstabel kolom 2 en 3. E2 = N1.C3 + !N1#.!C3 = Merk op dat hier niet moet worden genoteerd E2 = N1 + !C3, maar combinaties van de beide ingangen, waarbij telkens slechts één ingang 1 is. Path sensitizing leidt ertoe dat de combinatie N1.!C3 niet getest hoeft te worden. Voor het '0' maken van knoop N1 in de tweede term is maar één invoercombinatie vereist; we kiezen bv. de term C1.!C2: = !C1.!C2.C3 + C1.!C2.!C3 Zie beslissingstabel kolom 4 en 5. !E2 = !N1.C3 = = C1.!C2.C3 + !C1.C2.C3 Zie beslissingstabel kolom 6 en 7. E3 = !C3 Zie beslissingstabel kolom 8. !E3 = C3 Zie beslissingstabel kolom 9. E4 = C1.C3 Zie beslissingstabel kolom 10. !E4 = !C1.C3 + C1.!C3 Zie beslissingstabel kolom 11 en 12. E5 = C2.C3 Zie beslissingstabel kolom 13. !E5 = !C2.C3 + C2.!C3 Zie beslissingstabel kolom 14 en 15.
causes:
effects:
1 C1 0 C2 0 C3 E1 1 E2 E3 E4 E5
2 1 0 0
3 0 1
4 0 0 1
5 1 0 0
6 1 0 1
7 0 1 1
1
1
0
0
8
0
9 10 11 12 13 14 15 1 0 1 1 0 1 1 1 1 0 1 1 0
1
0
0
1
0
0 1
Fig. 11 Beslissingstabel bij de CE-graaf van fig. 10.
30
0
0
We kunnen de resultaten in een beslissingstabel plaatsen (zie fig. 11). De bovenste regels komen overeen met de causes (invoercondities), de onderste regels met de effects. De beslissingstabel kan verder worden aangevuld met waarden, die volgen uit de constraints. Zo volgt uit de E-constraint van fig. 10 bijvoorbeeld, dat als cause C1 gelijk is aan 1, dat dan cause C2 gelijk moet zijn aan 0 en omgekeerd (zie fig. 12).
causes:
effects:
1 C1 0 C2 0 C3 E1 1 E2 E3 E4 E5
2 1 0
3 0 1
0
0
4 0 0 1
5 1 0 0
6 1 0 1
7 0 1 1
1
1
0
0
8
0
9 10 11 12 1 0 1 0 0 1 1 1 0
1
0 1
0
13 14 15 0 0 1 0 1 1 1 0
0 1
0
0
Fig. 12 Beslissingstabel bij de CE-graaf van fig. 10 met constraints. Vervolgens kunnen we verschillende kolommen samennemen, waarvan de waarden van de causes overeenkomen. Dit leidt tot minder kolommen en dus tot een reductie van het aantal testcases. De ontbrekende effecten zouden kunnen worden ingevuld vanuit de CE-graaf, maar dit laten we achterwege omdat het leidt tot extra werk bij het uitvoeren van de test, terwijl de meeropbrengst gering is. Zo kunnen in fig. 12 bijvoorbeeld de volgende kolommen worden samengenomen: 1, 4, 9, 11 en 14 (in fig. 13 wordt dit tesamen kolom 1) 2, 5, 8 en 12 (in fig. 13 wordt dit tesamen kolom 2) 2, 6, 9, 10 en 14 (in fig. 13 wordt dit tesamen kolom 3) 3, 7, 9, 11 en 13 (in fig. 13 wordt dit tesamen kolom 4) 3, 8 en 15 (in fig. 13 wordt dit tesamen kolom 5)
causes:
effects:
C1 C2 C3 E1 E2 E3 E4 E5
1 0 0 1 1 1 0 0 0
2 1 0 0 0 1 1 0
3 1 0 1 0 0 0 1 0
4 0 1 1 0 0 0 0 1
5 0 1 0 0 1 0
Fig. 13 Gecomprimeerde beslissingstabel van fig. 12. Ten slotte leidt iedere kolom tot een testcase (zie fig. 14).
31
1
2
3
4
5
invoer verwachte uitvoer A3 Foutmelding "Incorrect partition" Programma stopt Geen foutmelding "No digit" Geen bewerking m.b.t. partitie C of D CX Foutmelding "No digit" Programma stopt Geen foutmelding "Incorrect partition" Geen bewerking m.b.t. partitie C C3 Bewerking m.b.t. partitie C Geen foutmeldingen Programma stopt niet D3 Bewerking m.b.t. partitie D Geen foutmeldingen Programma stopt niet DX Foutmelding "No digit" Geen foutmelding "Incorrect partition" Geen bewerking m.b.t. partitie D
Fig. 14 Testcases bij de beslissingstabel van fig. 13. We passen nu de techniek van cause-effect graphing toe op het programma "Raad het getal" voor het bepalen van een set testcases. We volgen hierbij de volgende 7 stappen: 1. Stel de Cause-Effect graaf op. 2. Voeg de eventuele constraints toe aan de CE-graaf. 3. Leid de invoercombinaties af die leiden tot het al dan niet optreden van elk van de effecten. 4. Zet de resultaten hiervan in een beslissingstabel. 5. Vul de beslissingstabel aan met waarden die volgen uit de eventuele constraints. 6. Reduceer de beslissingstabel zoveel mogelijk door kolommen samen te nemen. 7. Leid hieruit een set testcases af. Bij het afleiden van een set testcases met behulp van cause-effect graphing komen vaak onduidelijkheden en/of onvolledigheden in de specificatie aan het licht. Dit is ook het geval gebleken bij 'Raad het getal'. Bij het opstellen van de CE-graaf blijkt al direct dat de effects onduidelijk en/of onvolledig geformuleerd zijn. Het vragen naar resp .de naam, een getal en nog een spel is niet exact geformuleerd. Dit wordt in de verbeterde specificatie aangevuld met resp. "Geef je naam", "Geef raadgetal" en "Nog een spel? (j/n)". Verder is niet gezegd wat er gebeurt na de boodschap "Ongeldige naam". Stopt het spel dan of wordt opnieuw naar de naam gevraagd? Dit wordt in de nieuwe versie gespecificeerd. Ook wordt niet expliciet gezegd wat er na de melding "Ongeldig getal" gebeurt. Stopt het spel of wordt er opnieuw om een raadgetal gevraagd? Evenmin is duidelijk of een incorrect raadgetal meetelt als raadbeurt. In de vorige specificatie lijkt het alsof er alleen om een nieuw spel wordt gevraagd als het getal niet geraden is. Dat is echter niet de bedoeling. Evenmin is expliciet gezegd dat het aantal raadbeurten telkens bij een nieuw spel gereset wordt. Het is niet duidelijk of na het herstarten van het spel opnieuw om de naam gevraagd wordt. We besluiten dit niet te doen. Hier volgt een verbeterde specificatie.
32
Verbeterde functionele specificatie van 'Raad het getal' Het programma kiest zelf random een te raden getal. Elke invoer moet worden gelezen als string en afgesloten met enter. Het programma vraagt om je naam in te voeren: "Geef je naam". Deze naam moet uit minstens 1 en hoogstens 30 karakters bestaan. Hierin mogen geen cijfers voorkomen. Bij een verkeerde naam wordt de foutboodschap "Ongeldige naam" getoond en vraagt het programma opnieuw om je naam. Als een correcte naam is ingevoerd, vraagt het programma om een raadgetal in te voeren: "Geef raadgetal". Raadbeurt wordt op 1 gezet. Het raadgetal mag uitsluitend uit decimale cijfers bestaan en moet minstens 10 en hoogstens 300 bedragen. Bij de invoer van een ongeldig of onjuist getal wordt de foutboodschap "Ongeldig getal" getoond en vraagt het programma opnieuw om een raadgetal. Alleen geldige raadgetallen tellen mee als raadbeurt. Wordt echter bv. na een te groot getal de volgende keer een nog groter getal ingevoerd, dan telt dit dom raden gewoon mee (raadbeurt wordt dan verhoogd). Het programma geeft als reactie op een correct raadgetal de boodschap "Te klein" als raadgetal < te raden getal, "Te groot" als raadgetal > te raden getal of "Geraden" als raadgetal = te raden getal. Alleen in de eerste twee gevallen wordt opnieuw om een raadgetal gevraagd en wordt raadbeurt met 1 verhoogd. Als het getal na 6 keer nog niet is geraden, wordt de boodschap "Niet geraden" getoond. Raadbeurt wordt nu niet verhoogd. Na de boodschap "Geraden" of "Niet geraden" wordt gevraagd of de gebruiker nog een keer wil spelen: "Nog een spel? (j/n)". Het antwoord hierop mag alleen "j" of "n" zijn (dus 1 enkele letter, gevolgd door enter). Bij "j" start het programma met het kiezen van een nieuw te raden getal, het 1 maken van raadbeurt en het vragen van een raadgetal. Bij "n" stopt het programma. Bij een foutief antwoord gebeurt er niets en wordt er gewacht op een correct antwoord. Bij het opstellen van de beslissingstabel blijkt dit al snel te leiden tot testcases waarbij bv. de naam en het antwoord "j" of "n" tegelijk in één testcase worden ingevuld, terwijl dat natuurlijk niet kan. Het antwoord kan pas worden gegeven als een spel is uitgespeeld. Dit leidt tot het splitsen van de cause-effect graaf in 4 grafen. Dit heeft bovendien als voordeel dat het probleem om de CE-grafen op te stellen aanzienlijk kleiner wordt. We onderscheiden de volgende 4 situaties die elkaar niet beïnvloeden: 1. het starten van het programma 2. het invullen van de naam 3. het invoeren van het raadgetal 4. het beantwoorden van de j/n-vraag (na het goed raden of na 6 raadbeurten) start het programma E1 = C1 !E1 = !C1
C1
E1
C1 E1
"Geef je naam"
1 2 1 0 1 0
Fig. 15 CE-graaf, formules en beslissingstabel bij het starten van het programma Opdracht:
Ga dat de CE-grafen, formules en beslissingstabellen van de figuren 15 t/m 20 (horende bij het programma "Raad het getal") kloppen.
33
correcte naam
C2
E1 = !C2 !E1 = C2 E2 = !C2 !E2 = C2 E3 = C2 !E3 = !C2 E4 = C2 !E4 = !C2
!
E1
"Geef je naam"
!
E2
"Ongeldige naam"
E3
raadbeurt = 1
E4
"Geef raadgetal"
3 4 5 6 7 8 9 10 0 1 0 1 1 0 1 0 1 0 1 0 1 0 1 0
C2 E1 E2 E3 E4
Fig. 16 CE-graaf, formules en beslissingstabel bij het invullen van de naam ongeldig raadgetal
C3
E5
"Ongeldig getal" M
raadgetal < C4 te raden getal
● N1
raadgetal > te raden getal C5
● N2
+ E4 "Geef raadgetal" + E6 raadbeurt + 1
O
aantal raadbeurten <= 6
E7 "Te klein" E8 "Te groot"
!
C6
+ E9 "Nog een spel? (j/n)"
! raadgetal = te C7 raden getal
● E10 "Niet geraden"
!
E11
"Geraden"
Fig. 17 CE-graaf bij het invoeren van het raadgetal Fig. 17 toont de CE-graaf bij het invoeren van het raadgetal. Omdat we de grenzen van de invoer bij boundary value analysis al hebben getest, maken we hier geen onderscheid meer tussen de diverse soorten van ongeldige raadgetallen. Het is duidelijk dat altijd één en slechts één van de causes C3, C4, C5 en C7 geldig is. Bij een ongeldig raadgetal wordt raadbeurt niet verhoogd. Dit betekent dat effect E5 effect E6 maskeert.
34
Uit de CE-graaf worden de formules voor het 1 en 0 worden van de effecten afgeleid. Vervolgens worden die in de beslissingstabel (fig. 18) opgenomen. Begonnen wordt met het effect de waarde 1, resp. 0 te geven en daarna de hierbij horende waarden van de causes in te vullen (per kolom). De volgende formules gelden bij fig. 17: N1 = C4.C6 !N1 = !C4.C6 + C4.!C6 N2 = C5.C6 !N2 = !C5.C6 + C5.!C6 E4 = C3.!N1#.!N2# + !C3.N1.!N2# + !C3.!N1#.N2 = C3.!C4.C6.!C5.C6 + !C3.C4.C6.!C5.C6 + !C3.!C4.C6.C5.C6 = C3.!C4.!C5.C6 + !C3.C4.!C5.C6 + !C3.!C4.C5.C6 !E4 = !C3.!N1.!N2 = !C3.(!C4.C6 + C4.!C6).(!C5.C6 + C5.!C6) = !C3.!C4.!C5.C6 + !C3.C4.C5.!C6 Nu kunnen C4 en C5 niet tegelijk 1 zijn, zodat voor !E4 alleen de term !C3.!C4.!C5.C6 overblijft. Echter voor het 0-zijn van een AND-knoop moeten alle mogelijke invoercombinaties waarvoor één ingang 0 is en alle andere ingangen 1 zijn, worden doorgerekend. Dit betekent dat we voor het 0 zijn van N1 de combinaties !C4.C6 en C4.!C6 moeten nemen. Voor N2 gelden evenzo de combinaties !C5.C6 en C5.!C6. Daarbij moeten N1 en N2 tegelijk 0 zijn. Dit betekent dat we voor !E4 kiezen: !E4 = !C3.!C4.!C5.C6 + !C3.C4.!C5.!C6 + !C3.!C4.C5.!C6 E5 = C3 !E5 = !C3 E6 = N1.!N2# + !N1#.N2 = C4.C6.!C5.C6 + !C4.C6.C5.C6 = C4.!C5.C6 + !C4.C5.C6 !E6 = !N1.!N2 = (!C4.C6 + C4.!C6).(!C5.C6 + C5.!C6) = !C4.!C5.C6 + C4.C5.!C6 Hier treedt hetzelfde probleem op als bij !E4, zodat voor !E6 alleen de term !C4.!C5.C6 overblijft. Ook hier moeten we zoeken naar de gewenste combinaties voor het 0 worden van N1 en N2. We vinden nu op dezelfde wijze als bij !E4: !E6 = !C4.!C5.C6 + C4.!C5.!C6 + !C4.C5.!C6 E7 = N1 = C4.C6 !E7 = !N1 = !C4.C6 + C4.!C6 E8 = N2 = C5.C6 !E8 = !N2 = !C5.C6 + C5.!C6 E9 = !C6.!C7 + C6.C7 !E9 = C6.!C7 E10 = !C6.!C7 !E10 = C6.!C7 + !C6.C7 E11 = C7 !E11 = !C7 Fig. 18 laat de bijbehorende beslissingstabel zien. De vetgedrukte cijfers zijn de toegevoegde constraints. Zo moet van C3, C4, C5 en C7 er altijd precies één cause 1
35
zijn. In de kolommen 18, 21, 25, 28, 30 en 32 t/m 37 is geen van de causes C3, C4, C5 en C7 1. We moeten er na het samennemen van de kolommen op toezien dat er wel aan de genoemde constraint wordt voldaan, d.w.z. dat één van de vier bijbehorende causes telkens 1 moet zijn en alle andere 0. Verder geldt dat effect E5 en E6 niet beide 1 kunnen zijn.
C3 C4 C5 C6 C7 E4 E5 E6 E7 E8 E9 E1 0 E1 1
1 1 1 0 0 1 0 1
1 2 0 1 0 1 0 1
1 3 0 0 1 1 0 1
1 4 0 0 0 1 1 0
1 5 0 1 0 0 0 0
1 6 0 0 1 0 0 0
1 1 1 2 2 2 7 8 9 0 1 2 1 0 0 0 0 0 1 0 0 1 0 0 1 0 0 1 1 1 0 0 0 0 0
2 3 0 0 1 0 0
2 4 0 1 0 1 0
2 2 2 5 6 7 0 0 0 1 0 0 1 1 0 1 0 0
2 2 3 8 9 0 0 0 0 1 1 0 0 0 0
3 1 0 0 0 1 1
3 3 3 3 3 3 2 3 4 5 6 7 0 0 0 0 0 0 1 0 1 0 0 0 0 1 1 0
1 0 0 0 0 1 1 0 0 0 1 0 0 1 0 0 1 1 0 1 0 0 1 0
Fig. 18 Beslissingstabel bij het invoeren van het raadgetal (met constraints) E3 antwoord is "j"
C8
E4
!
E
M
! antwoord is "n"
raadbeurt = 1 "Geef raadgetal" M
● E12 geen reactie M
C9
E13
programma stopt
Fig. 19 CE-graaf bij het beantwoorden van de j/n-vraag Als er geen reactie is, mag er verder geen ander effect optreden. E12 maskeert dus E3, E4 en E13 (zie fig. 19). Hierbij horen de volgende formules (zie fig. 20 voor de beslissingstabel): E3 = C8 !E3 = !C8 E4 = C8 !E4 = !C8 E12 = !C8.!C9 !E12 = C8.!C9 + !C8.C9 E13 = C9 !E13 = !C9 36
C8 C9 E3 E4 E12 E13
38 39 40 41 42 43 44 45 46 1 0 1 0 0 1 0 0 0 0 0 0 1 1 0 1 0 0 1 0 0 0 0 1 0 0 0 0 1 0
Fig. 20 Beslissingstabel bij het beantwoorden van de j/n-vraag (met constraints) We gaan nu het aantal testcases verkleinen door onmogelijke of niet zinvolle testcases weg te laten en door kolommen uit de beslissingstabellen samen te nemen (fig. 21). Testcase 2 (fig. 15) is niet zinvol en zal worden weggelaten. Natuurlijk gebeurt er niets als het programma niet wordt opgestart. We houden dus hier slechts één testcase c1 over (zie fig. 21). Van fig. 16 kunnen we de kolommen 3, 5, 8 en 10 samennemen en 4, 6, 7 en 9. Hiermee zijn de 8 testcases teruggebracht naar 2: c2 en c3. In fig. 18 nemen we de kolommen 11, 17, 21, 25, 28, 32, 34 en 37 samen tot testcase c4. Zo ook bestaat testcase c5 uit de kolommen 12, 18, 19 en 24, c6 uit 13, 20 en 27, c7 uit 14, 31 en 36, c8 uit 15, 22, 26, 30 en 33, c9 uit 16, 23 en 29 en c10 uit 35. In fig. 20 nemen we de kolommen 36, 38, 41 en 44 samen tot testcase c11, de kolommen 37, 39 en 40 tot c12 en 42 en 43 tot c13. In fig. 21 zien we dat het aantal testcases is teruggebracht tot 13. Aanvankelijk waren het er 44, een aanzienlijke besparing dus. c1 C1 1 E1 1
C2 E1 E2 E3 E4
c2 c3 0 1 1 0 1 0 0 1 0 1
c4 c5 c6 c7 c8 c9 c10 C3 1 0 0 0 0 0 0 C4 0 1 0 0 1 0 0 C5 0 0 1 0 0 1 0 C6 1 1 1 1 0 0 0 C7 0 0 0 1 0 0 1 E4 1 1 1 0 0 0 E5 1 0 0 E6 0 1 1 0 0 E7 0 1 0 E8 0 1 0 E9 0 1 1 E10 0 1 0 E11 0 1
C8 C9 E3 E4 E12 E13
c11 c12 c13 1 0 0 0 0 1 1 0 1 0 0 1 0 0 0 1
Fig. 21 Gereduceerde beslissingstabellen bij het programma "Raad het getal" In fig. 21 blijkt nu inderdaad telkens één en slechts één van de causes C3, C4, C5 en C7 1 te zijn. Uit deze beslissingstabellen volgen de testcases van fig. 22. Bij het invoeren van een ongeldige naam (test c2) is de reactie dat raadbeurt niet op 1 gezet wordt, weggelaten. Dit is immers niet te testen, omdat deze mogelijk van tevoren al 1 is.
37
testcase c1 c2
invoer start programma naam = Jos2
c3
naam = Jos
*
c4
raadgetal = 12c en raadbeurt <= 6
*
c5
raadgetal = 10 (< te raden getal) en raadbeurt <= 6 raadgetal = 300 (> te raden getal) en raadbeurt <= 6 raadgetal = te raden getal en raadbeurt <= 6 geef 6 keer raadgetal = 10 (< te raden getal)
c6 c7 c8 c9 c10
geef 6 keer raadgetal = 300 (> te raden getal) geef 5 keer raadgetal ≠ te raden getal en daarna raadgetal = te raden getal antwoord = j
* * * * *
verwachte uitvoer "Geef je naam" "Ongeldige naam" en "Geef je naam"; niet "Geef raadgetal" "Geef raadgetal" en raadbeurt = 1; geen andere melding "Ongeldig raadgetal " en "Geef raadgetal"; raadbeurt blijft gelijk en geen andere melding "Te klein", "Geef raadgetal" en raadbeurt wordt 1 hoger; niet "Ongeldig getal" "Te groot", "Geef raadgetal" en raadbeurt wordt 1 hoger; niet "Ongeldig getal" "Geraden", "Nog een spel? (j/n)"; niet "Geef raadgetal" "Niet geraden", "Nog een spel? (j/n)" en raadbeurt = 6; niet "Te klein" en niet "Geef raadgetal" raadbeurt = 6; niet "Te groot" en niet "Geef raadgetal" niet "Niet geraden"
* * "Geef raadgetal" en raadbeurt = 1; programma stopt niet c12 antwoord = w geen reactie, programma stopt niet c13 antwoord = n programma stopt * Om deze test uit te voeren is het handig als (tijdens de testfase) het te raden getal en raadbeurt worden afgedrukt. c11
Fig. 22 Set testcases volgens Cause-Effect Graphing bij 'Raad het getal' De letter 'c' staat voor cause-effect graphing.
38
5.1.4
Error Guessing
Bij Error Guessing gaat het om het aanvullen van de reeds aanwezige verzameling testcases met een aantal nieuwe testcases die verzonnen worden op grond van intuïtie en ervaring. Echte testers hebben vaak een "zesde zintuig" voor het vinden van manieren om een programma te kraken. Zij voelen aan waar de zwakke plekken van een programma kunnen liggen. In die zin is het niet echt een "techniek" te noemen, maar het levert wel vaak een waardevolle uitbreiding van de met de eerdere technieken ontwikkelde verzameling testcases. Met nadruk wordt vermeld dat het aanvullingen op de reeds bestaande testcases betreft. Het heeft dus geen zin om bij Error Guessing testcases te genereren, die reeds bij een van de andere Black Box technieken gegenereerd zijn. Error Guessing wordt dus ook als laatste toegepast. Om te voorkomen dat Error Guessing leidt tot willekeurig wilde testcases is het belangrijk dat van elke testcase gemotiveerd wordt waarom de tester vermoedt dat de testcase mogelijk succesvol is. Die vermoedens kunnen gebaseerd zijn op de specificaties, maar ook op mogelijke implementaties. Voorbeeld 1: Gegeven de specificatie van functie SearchTable (zie bijlage 1). De tester vermoedt dat de kans groot is dat de programmeur gebruik maakt van het Binary Search algoritme om het element in de tabel te zoeken. Daarom construeert hij zowel een testcase waarin de tabel een even aantal elementen heeft als een tabel waarin de tabel een oneven aantal elementen heeft. Voorbeeld 2: Gegeven de specificatie van programma MeanOfTable (zie bijlage 2). De tester vermoedt dat alle getallen waarschijnlijk bij elkaar worden opgeteld en daarna gedeeld door het aantal getallen. Als voor de som ook een integer genomen wordt, dan kan gemakkelijk het integerbereik overschreden worden. Daarom voert hij een tabel in met enkele getallen ter grootte van de maximale integer waarde. Voorbeeld 3: De tester vermoedt dat een formule gebruikt is, waar deling door nul mogelijk is. Hij genereert een of meer zodanige testcases dat deling door nul waarschijnlijk is. Voorbeeld 4: Bij een programma dat van een memory stick leest, richt de tester een testcase op het verwijderen van de stick tijdens het lezen. Voorbeeld 5: Bij het invullen van een numeriek veld op een formulier genereert de tester een testcase waarbij niet-numerieke invoer wordt ingevuld, een testcase waarbij het veld leeg wordt gelaten en een testcase waarbij de numerieke waarde wordt voorafgegaan door een aantal nullen. Voorbeeld 6: Bij een programma dat een file als invoer gebruikt, probeert de tester een lege file in te voeren. In veel gevallen zal het moeilijk zijn om bij de aldus gegenereerde testcases de "verwachte uitvoer" te bepalen. Dat komt omdat veel specificaties incompleet zijn, zeker waar het uitzonderlijke situaties betreft. Toch dienen de specificaties uitsluitsel te ge-
39
ven en de tester zal dan ook initiatief moeten nemen om de specificaties aangevuld te krijgen. Vanzelfsprekend heeft ook de programmeur baat bij dergelijke aanvullingen op de specificaties, omdat hij nu weet hoe hij dergelijke situaties moet afhandelen.
5.2
White Box testcase ontwerp
In tegenstelling tot Black Box technieken wordt er bij White Box technieken wél gekeken naar de programmacode. We behandelen vijf methoden om testcases te genereren, die zo goed mogelijk alle programmaonderdelen aan de tand voelen. In een later hoofdstuk zullen nog enige technieken behandeld worden die niet bedoeld zijn om testcases te genereren, maar om fouten op te sporen door de programmacode kritisch te bekijken. De White Box testcase generatietechnieken zijn gebaseerd op het zoveel mogelijk uitvoeren van alle onderdelen van het programma. Het is verleidelijk om te denken dat het goed zou zijn om alle mogelijke programmapaden van het programma te doorlopen. Maar net als de pogingen om bij Black Box technieken het systeem uitputtend te testen, struikelen we ook bij relatief simpele programma's over het astronomisch grote aantal mogelijke paden. Enkele loops zijn al voldoende om de pogingen om alle mogelijke paden te doorlopen op te moeten geven. Bovendien worden met een uitputtende padentest niet alle mogelijke fouten opgespoord. Zo kunnen er bepaalde paden in het programma ontbreken. Of het optreden van een fout kan afhankelijk zijn van de toevallige waarden van bepaalde variabelen. Bv. als het volgende statement if (fA - fB < fMarge) { ... } eigenlijk het volgende had moeten zijn: if (abs (fA - fB) < fMarge) { ... } dan wordt deze fout niet ontdekt als fA >= fB ook al worden alle paden doorlopen. We richten ons daarom op minder uitputtende methoden waarbij de combinatie met Black Box technieken ervoor moet zorgen dat zoveel mogelijk fouten van allerlei soort worden ontdekt.
5.2.1
Statement Coverage
De eerste poging om aan de hand van de programmacode een krachtige verzameling testcases te genereren is de testcases zodanig op te stellen, dat alle statements van het programma ten minste eenmaal worden uitgevoerd. Het zal duidelijk zijn dat,
40
als dit lukt, veel fouten gepakt kunnen worden. Toch is deze techniek te mager zoals blijkt uit een analyse van de functie Gcd (zie bijlage 5), waarvan de implementatie hier nogmaals gegeven is, nu voorzien van regelnummers: /* 1 */ int Gcd (int iA, int iB) { /* 2 */ int iResult = iA; /* 3 */
if (iB < iResult)
/* optimalisatie */
{ /* 4 */
iResult = iB; }
/* 5 */
while (iA % iResult > 0 || iB % iResult > 0) { iResult--; } return iResult;
/* 6 */ /* 7 */ }
Om alle statements te doorlopen moet gelden: in regel 5: (iA % iResult > 0) || (iB % iResult > 0) ofwel iA of iB is niet deelbaar door iResult; dan wordt het statement van regel 6 uitgevoerd. in regel 3: iB < iResult dan wordt het statement van regel 4 uitgevoerd. Aan deze beide voorwaarden is voldaan als bij het aanroepen van de functie is voldaan aan de eis dat iA groter is dan iB maar niet deelbaar door iB. Dit is af te leiden door de voorwaarden vanuit de regels naar boven te "propageren" (een vergelijkbare techniek wordt toegepast bij formele correctheidsbewijzen van programma's gebaseerd op Floyd's Assertion Method). Gegeven het pad 2, 3, 4, 5 gaat deze afleiding stapsgewijs als volgt: Aan het begin van regel 5 geldt: (iA % iResult > 0) || (iB % iResult > 0) Aan het begin van regel 4 geldt (iResult vervangen door iB): (iA % iB > 0) || (iB % iB > 0) ofwel (omdat iB % iB > 0) FALSE is: iA % iB > 0 Aan het begin van regel 3 geldt (teneinde aan de if-voorwaarde te voldoen): (iA % iB > 0) && (iB < iResult) Aan het begin van regel 2 geldt (iResult vervangen door iA): (iA % iB > 0) && (iB < iA) De laatste voorwaarde geldt dus ook bij aanroep van de functie. Dit leidt bijvoorbeeld tot de volgende testcase (één testcase is in dit geval voldoende voor statement coverage): Invoer: Verwachte uitvoer: iA = 3, iB = 2 1
41
Opdracht:
Check dat deze testcase er inderdaad voor zorgt dat alle statements minstens eenmaal doorlopen worden.
Dat deze techniek niet erg krachtig is, blijkt uit het feit dat bijvoorbeeld de volgende mogelijke fouten niet gevonden zullen worden: A.
Regel 3 is abusievelijk: if (iB > iResult) /* optimalisatie */ Dit leidt overigens wel tot de afleiding van een andere testcase. Aan het begin van regel 2 geldt nu: (iA % iB > 0) && (iB > iA) zodat we de bijvoorbeeld de volgende testcase genereren: Invoer: iA = 2, iB = 3 Ga dit na!
Verwachte uitvoer: 1
De volgende fouten geven geen verschil m.b.t. de gegenereerde testcase. De testcase brengt de fouten niet aan het licht: B.
Regel 5 is abusievelijk: while (iA % iResult > 0 || iB % iResult < 0)
C.
Regel 5 is abusievelijk te kort: while (iA % iResult > 0)
D.
Regel 5 is abusievelijk: while ((iA % iResult > 0) != (iB % iResult > 0)) (In C komt de != operator overeen met de exclusive-OR functie).
E.
Regel 5 is abusievelijk: while (iA % iResult > 0 && iB % iResult >= 0)
Opdracht:
5.2.2
Ga dit alles na.
Decision Coverage
Een wat verdergaande aanpak is om ervoor te zorgen dat bij iedere beslissing (decision) de uitkomst een keer TRUE en een keer FALSE is. Op deze wijze wordt elk beslissingspad minstens één keer doorlopen. Hieruit volgt vanzelfsprekend dat elk statement ook minstens één keer doorlopen wordt. Met andere woorden, Decision Coverage impliceert Statement Coverage. Voor meervoudige beslissingsstructuren zoals het switch-statement van C geldt dat elke mogelijke switch-waarde minstens één keer moet optreden. We gaan even terug naar het voorbeeld, gebruikt bij Statement Coverage. Voor Decision Coverage geldt dat de volgende decisions: /* 3 */ iB < iResult /* 5 */ (iA % iResult > 0) || (iB % iResult > 0) beide minstens één keer TRUE en minstens één keer FALSE moeten zijn.
42
We beginnen achteraan (regel 5) en kijken dan hoe de betreffende testcase zich gedraagt bij regel 3. Eventueel vullen we daarna nog testcases aan om aan de eis m.b.t. regel 3 te voldoen. Merk op dat er minstens twee testcases nodig zijn om de ifdecision van regel 3 resp. TRUE of FALSE te maken. Allereerst maken we de decision van regel 5 TRUE. Dat lijkt een handige keuze, want als het programma eindigt zal de decision ook vanzelf een keer FALSE moeten worden. En als het programma niet eindigt maar in een oneindige loop terechtkomt, dan merken we dat ook. We lijken dus twee vliegen in één klap te slaan. Toch bestaat het gevaar, dat effecten elkaar "maskeren". Het is beter afzonderlijk op de waarde TRUE en FALSE te testen, dan hebben we zelf in de hand wat er gebeurt. Als we er bv. voor kiezen dat de voorwaarde van regel 3 FALSE is en die van regel 5 TRUE, dan geldt aan het begin van regel 5: (iA % iResult > 0) || (iB % iResult > 0) en regel 4 wordt niet doorlopen (decision in regel 3 = FALSE). Aan het begin van regel 3 geldt: ((iA % iResult > 0)||(iB % iResult > 0))&&(iB >= iResult) Aan het begin van regel 2 geldt (iResult vervangen door iA): [(iA % iA > 0)||(iB % iA > 0)] && (iB >= iA) ofwel (omdat iA % iB FALSE is) (iA % iB > 0) && (iB >= iA) Dit leidt bv. tot de volgende testcase: Invoer: iA = 2, iB = 3
Verwachte uitvoer: 1
Vervolgens zorgen we ervoor, dat de while-decision een keer FALSE wordt gemaakt, niet via de loop, maar direct de eerste keer dat we het while-statement bereiken. We kiezen er nu voor om wel aan de if-decision (regel 3) te voldoen, want dan hebben we direct aan de Decision Coverage eisen voldaan. Aan het begin van regel 5 geldt: !((iA % iResult > 0) || (iB % iResult > 0)) ofwel (omdat een modulo berekening geen negatief getal kan opleveren): (iA % iResult == 0) && (iB % iResult == 0) Aan het begin van regel 4 geldt (iResult vervangen door iB): (iA % iB == 0) && (iB % iB == 0) ofwel (omdat iB % iB == 0) TRUE is: (iA % iB == 0) Aan het begin van regel 3 geldt (teneinde aan de if-voorwaarde te voldoen): (iA % iB == 0) && (iB < iResult) Aan het begin van regel 2 geldt (iResult vervangen door iA): (iA % iB == 0) && (iB < iA) hetgeen bijvoorbeeld leidt tot de volgende testcase: Invoer: iA = 4, iB = 2
Verwachte uitvoer: 2
43
Aangezien zowel de if-decision als de while-decision zowel een keer TRUE als FALSE zijn gemaakt, zijn beide gegenereerde testcases voldoende voor Decision Coverage. Decision Coverage is krachtiger dan Statement Coverage, maar nog altijd niet krachtig genoeg. Dat blijkt onder meer uit een evaluatie van de onder Statement Coverage genoemde fouten. A t/m D geven geen verschil m.b.t. de gegenereerde testcases. A.
Regel 3 is abusievelijk: if (iB > iResult) De fout zal niet worden ontdekt.
/* optimalisatie */
B.
Regel 5 is abusievelijk: while (iA % iResult > 0 || iB % iResult < 0) De fout zal worden ontdekt.
C.
Regel 5 is abusievelijk te kort: while (iA % iResult > 0) De fout zal worden ontdekt.
D.
Regel 5 is abusievelijk: while ((iA % iResult > 0) != (iB % iResult > 0)) De fout zal niet worden ontdekt.
E.
Regel 5 is abusievelijk: while (iA % iResult > 0 && iB % iResult >= 0) Dit leidt tot een andere testcase bv. iA = 3 en iB = 2 (met verwachte uitvoer = 1) i.p.v. iA = 4 en iB = 2. De fout zal dan worden ontdekt.
Opdracht:
5.2.3
Ga dit alles na.
Condition Coverage
Een alternatief, dat soms krachtiger maar soms ook minder krachtig is dan Decision Coverage is Condition Coverage. Daarbij wordt elke conditie van een beslissing afzonderlijk TRUE en FALSE gemaakt. De extra kracht komt voort uit het feit dat elke conditie afzonderlijk aan de tand gevoeld wordt. In tegenstelling tot Decision Coverage houdt Condition Coverage niet automatisch in, dat alle statements worden doorlopen. Dat wordt duidelijk aan het volgende voorbeeld: if (a && b) { SA; } else { SB; } waarbij we twee testcases ontwikkelen die a en b als volgt TRUE/FALSE maken:
44
testcase nr: 1 2
a: TRUE FALSE
b: FALSE TRUE
statement uitgevoerd: SB SB
Statement SA wordt dus niet uitgevoerd en een aperte fout daarin wordt door deze testcases niet ontdekt! Opdracht: Leid testcases af voor het programma GCD en ga zelf na welke van de fouten A t/m E door deze testcases wel/niet ontdekt zullen worden.
5.2.4
Decision/Condition Coverage
Aangezien het toch wel wenselijk is dat ook alle statements worden doorlopen, ligt het voor de hand om de beide voorgaande technieken te combineren. In Decision/Condition Coverage gaat het er dus om dat zowel alle condities afzonderlijk als de beslissingen als geheel minstens één keer TRUE en FALSE worden gemaakt. Door het TRUE en FALSE maken van alle beslissingen wordt Statement Coverage weer geïmpliceerd. Laten we bij het GCD voorbeeld uitgaan van de reeds via Decision Coverage afgeleide testcases:
(1) (2)
Invoer: iA = 2, iB = 3 iA = 4, iB = 2
Verwachte uitvoer: 1 2
Deze testcases zullen alle beslissingen zowel TRUE als FALSE maken. Maar hoe zit het met de afzonderlijke condities? Deze krijgen door beide testcases de volgende waarden te geven (ga dit na): testcase: iB0 iB%iResult>0 (iA%iResult>0)||(iB%iResult>0) (1) FALSE FALSE TRUE TRUE (2) TRUE FALSE FALSE FALSE Er blijkt nog niet aan de Condition Coverage voorwaarde voldaan te zijn, immers de conditie iA % iResult > 0 krijgt nergens de waarde TRUE. Daarom voegen we nog een testcase toe die hierin voorziet. Op een vergelijkbare manier als hiervoor leiden we bijvoorbeeld de volgende testcase af, die genoemde conditie TRUE zal maken:
(3)
Invoer: iA = 3, iB = 2
Verwachte uitvoer: 1
Welke van de fouten A t/m E zullen nu wel/niet worden ontdekt? Het is te simpel om te zeggen dat, aangezien het dus (toevallig) dezelfde verzameling testcases oplevert als bij de voorgaande twee technieken tesamen, ook dezelfde fouten A t/m E wel of niet ontdekt zullen worden. Immers, als de condities anders luiden, leiden ze misschien wel tot andere (in dit geval aanvullende) testcases. Daarom zullen we ook hier de fouten toch moeten analyseren:
45
A.
Regel 3 is abusievelijk: if (iB > iResult) Geen aanvullende testcases. De fout zal niet worden ontdekt.
/* optimalisatie */
B.
Regel 5 is abusievelijk: while (iA % iResult > 0 || iB % iResult < 0) De conditie iB % iResult < 0 is altijd FALSE, dus niet TRUE te maken! Aan Condition Coverage kan dus niet helemaal worden voldaan. De testcase ontwikkelaar zal dit tijdens testcase generatie opmerken en de programmeur dus in een vroeg stadium al kunnen wijzen op een fout.
C.
Regel 5 is abusievelijk te kort: while (iA % iResult > 0) Geen aanvullende testcases. De fout zal worden ontdekt.
D.
Regel 5 is abusievelijk: while ((iA % iResult > 0) != (iB % iResult > 0)) Geen aanvullende testcases. De fout zal niet worden ontdekt.
E.
Regel 5 is abusievelijk: while (iA % iResult > 0 && iB % iResult >= 0) De conditie iB % iResult >= 0 is altijd TRUE, dus niet FALSE te maken! Aan Condition Coverage kan dus niet helemaal worden voldaan. Ook hier zal de testcase ontwikkelaar de programmeur al in een vroeg stadium kunnen wijzen op een fout.
Opdracht:
5.2.5
Ga dit alles na.
Multiple Condition Coverage
De fouten A en D worden nog altijd niet ontdekt. Hoe komt dat? Voor wat betreft fout A is daar niets aan te doen! Dat komt omdat het if-statement slechts een optimalisatie is van het algoritme. Ongeacht de uitkomst van de if-beslissing is de uitkomst altijd correct (aannemend dat de rest van het programma goed werkt). Als de ifconditie dus niet klopt, is het programma alleen wat trager. Zo'n fout is in het algemeen slechts door code-inspectie op te sporen (of in dit geval door executietijdmetingen, maar dat wordt slechts voor tijdkritische systemen gedaan). Het niet ontdekken van fout D heeft een andere oorzaak. Hier gaat het wel degelijk om een tekortkoming in de Decision/Condition Coverage techniek: deze checkt alleen de afzonderlijke condities en de beslissingen als geheel, maar kijkt onvoldoende naar de operatoren, die de condities tot een beslissing aaneen smeden! En juist in die operatoren wordt ook nog wel eens een fout gemaakt, bv. het verwisselen van een || door een && of omgekeerd. Hoe kunnen we er achter komen of de tussenoperatoren correct zijn? Daar is maar één afdoende oplossing voor: de gehele waarheidstabel voor alle condities genere-
46
ren. Met andere woorden, we zorgen er niet alleen voor dat alle condities een keer TRUE en FALSE worden, maar ook in elke mogelijke combinatie. Elke verkeerde operator zal dan door de mand vallen. Samengevat komt de Multiple Condition Coverage op het volgende neer: we genereren testcases zodanig dat van elke beslissing (decision) alle condities in alle mogelijke combinaties TRUE en FALSE worden gemaakt. Dit geldt natuurlijk alleen voor zover dit mogelijk is. In het volgende geval zijn bijvoorbeeld niet alle combinaties mogelijk: if (iA > 0 && iA <= 10) { .... } Hier zouden volgens het Multiple Condition Coverage criterium de volgende combinaties getest moeten worden: iA > 0 FALSE FALSE TRUE TRUE
iA <= 10 FALSE TRUE FALSE TRUE
testcase generatie: onmogelijk! mogelijk mogelijk mogelijk
Het is onmogelijk een waarde voor iA te verzinnen zodanig dat beide condities FALSE zijn. We testen in dit geval alle combinaties die wel mogelijk zijn. Wat betekent Multiple Condition Coverage voor ons voorbeeldprogramma Gcd? We zullen de volgende combinaties moeten testen: if-statement regel 3: iB < iResult (M1) FALSE (M2) TRUE while-statement regel 5: iA % iResult > 0 (M3) FALSE (M4) FALSE (M5) TRUE (M6) TRUE
iB % iResult > 0 FALSE TRUE FALSE TRUE
We zullen weer met het while-statement beginnen, omdat we dan vanzelf ook ifsituaties kunnen meetesten. Achtereenvolgens analyseren we de combinaties M3 t/m M6. M3:
Aan het begin van regel 5 geldt: (iA % iResult <= 0) && (iB % iResult <= 0) ofwel (omdat een modulo berekening geen negatief getal kan opleveren): (iA % iResult == 0) && (iB % iResult == 0)
47
M2:
We kiezen er nu bv. voor om wel aan de if-decision (regel 3) te voldoen, want dan hebben we hier direct aan de eis M2 voldaan. Aan het begin van regel 4 geldt dan (iResult vervangen door iB): (iA % iB == 0) && (iB % iB == 0)) ofwel (omdat iB % iB == 0) TRUE is: (iA % iB == 0) Aan het begin van regel 3 geldt: (iA % iB == 0) && (iB < iResult) Aan het begin van regel 2 geldt (iResult vervangen door iA): (iA % iB == 0) && (iB < iA) hetgeen bijvoorbeeld leidt tot de volgende testcase: Invoer: iA = 4, iB = 2
M4:
M1:
Aan het begin van regel 5 geldt: (iA % iResult == 0) && (iB % iResult > 0) We kiezen er nu voor om niet aan de if-decision (regel 3) te voldoen, zodat nu direct aan de eis M1 is voldaan. Regel 4 wordt nu niet doorlopen. Aan het begin van regel 3 geldt: (iA%iResult == 0)&&(iB%iResult > 0)&&(iB>=iResult) Aan het begin van regel 2 geldt (iResult vervangen door iA): (iA % iA == 0) && (iB % iA > 0) && (iB >= iA) ofwel (omdat iA % iA == 0) TRUE is: (iB % iA > 0)&&(iB >= iA) hetgeen bijvoorbeeld leidt tot de volgende testcase: Invoer: iA = 2, iB = 3
M5:
Verwachte uitvoer: 1
Aan het begin van regel 5 geldt: (iA % iResult > 0) && (iB % iResult == 0) We kiezen er nu bv. voor om aan de if-decision (regel 3) te voldoen. Aan het begin van regel 4 geldt dan (iResult vervangen door iB): (iA % iB > 0) && (iB % iB == 0)) ofwel (omdat iB % iB == 0) TRUE is: (iA % iB > 0) Aan het begin van regel 3 geldt: (iA % iB > 0) && (iB < iResult) Aan het begin van regel 2 geldt (iResult vervangen door iA): (iA % iB > 0) && (iB < iA) hetgeen bijvoorbeeld leidt tot de volgende testcase: Invoer: iA = 3, iB = 2
M6:
Verwachte uitvoer: 2
Verwachte uitvoer: 1
Aan het begin van regel 5 geldt: (iA % iResult > 0) && (iB % iResult > 0) We kiezen er nu bv. voor om niet aan de if-decision (regel 3) te voldoen. Aan het begin van regel 3 geldt dan:
48
(iA%iResult > 0)&&(iB%iResult > 0)&&(iB >= iResult) Aan het begin van regel 2 geldt (iResult vervangen door iA): (iA % iA > 0) && (iB % iA > 0) && (iB >= iA) Dit kan niet omdat (iA % iA > 0) FALSE is; daarom gaan we nu wel door het ifstatement: M6:
Aan het begin van regel 5 geldt: (iA % iResult > 0) && (iB % iResult > 0) We kiezen er nu dus voor om wel aan de if-decision (regel 3) te voldoen. Aan het begin van regel 4 geldt dan (iResult vervangen door iB): (iA % iB > 0) && (iB % iB > 0) Ook dit kan niet omdat (iB % iB > 0) FALSE is; daarom zullen we een slag door de loop moeten maken.
M6:
Aan het begin van regel 5 geldt: (iA % iResult > 0) && (iB % iResult > 0) Teruggaand door de loop geldt dat aan het begin van regel 6 geldt (iResult vervangen door (iResult - 1)): (iA % (iResult - 1) > 0) && (iB % (iResult - 1) > 0) Omdat we de loop zijn ingegaan, moet aan de while-conditie voldaan geweest zijn, dus nu geldt aan het begin van regel 5: ((iA % iResult > 0) || (iB % iResult > 0)) && (iA % (iResult-1) > 0) && (iB % (iResult-1) > 0) We kiezen er nu voor om niet aan de if-decision (regel 3) te voldoen. Aan het begin van regel 3 geldt dan: ((iA % iResult > 0) || (iB % iResult > 0)) &&(iA % (iResult-1) > 0) && (iB % (iResult-1) > 0) && (iB >= iResult) Aan het begin van regel 2 geldt (iResult vervangen door iA): ((iA % iA > 0) || (iB % iA > 0)) && (iA % (iA-1) > 0) && (iB % (iA-1) > 0) && (iB >= iA) ofwel omdat (iA % iA > 0) FALSE is: (iB % iA > 0) && (iA % (iA-1) > 0) && (iB % (iA-1) > 0) && (iB >= iA) Aan (iA % (iA-1) > 0) is voldaan als iA > 2. Verder moet iB >= iA en mag iB niet deelbaar zijn door iA noch door iA-1, hetgeen bijvoorbeeld leidt tot de volgende testcase: Invoer: iA = 3, iB = 5
Verwachte uitvoer: 1
Aan de eisen M1 en M2 is inmiddels voldaan, zodat de totale oogst aan testcases er als volgt uitziet: Invoer: iA = 4, iB = 2 iA = 2, iB = 3 iA = 3, iB = 2 iA = 3, iB = 5
Verwachte uitvoer: 2 1 1 1
49
Opmerking: Bovenstaande methode lijkt nogal bewerkelijk. Er valt echter niet aan te ontkomen om de condities die bij een bepaald conditioneel statement moeten gelden, terug te rekenen naar ingangscondities die bij aanroep van de module moeten gelden. Maar soms zijn ook intuïtief ingangswaarden te kiezen, waarbij we dan voorwaarts kunnen rekenen om te bepalen of inderdaad op de goede plek aan de gewenste condities is voldaan. Deze aanpak kan in sommige gevallen aantrekkelijker zijn. Multiple Condition Coverage impliceert alle voorgaande White Box technieken. Als in een module alle beslissingen slechts uit één conditie bestaan, geldt dat Multiple Condition Coverage overeenkomt met zowel Decision Coverage als Condition Coverage (en dus ook met Decision/Condition Coverage). Opdracht:
Ga na dat de fouten B t/m E in dit geval allemaal zullen worden ontdekt.
Opdracht:
Fout A wordt nog altijd niet ontdekt; weet je nog waarom? Hoe is die fout op te sporen?
5.2.6
Multiple condition coverage toegepast op "Raad het getal"
Een mogelijke oplossing voor het programma "Raad het getal" (met dank aan Gerard van den Bosch): int main (void) { unsigned int uiRaadgetal; unsigned int uiTeRadenGetal; unsigned int uiBeurt; char acNaam[100]; char acRaadgetal[10]; char acAntwoord[10]; char *cLaatsteKarakter; bool bNaamGoed; bool bEindeSpelronde; bool bStop; srand ((unsigned int)time(NULL));
1
do {
2 bNaamGoed = true; cout << "Geef je naam" << endl; cin >> acNaam; if ((strlen (acNaam) >= 1) && (strlen (acNaam) <= 30)) { for (int i = 0; i < strlen (acNaam); i++) { if (isdigit (acNaam[i])) { bNaamGoed = false; 50
3 4 5 6 7 8 9
} } } else { bNaamGoed = false; } if (!bNaamGoed) { cout << "Ongeldige naam" << endl; } } while (!bNaamGoed);
10 11 12 13 14
do {
15 uiTeRadenGetal = 10 + rand () % 291; uiBeurt = 1; /* cout << "Debug: Te raden getal is " << uiTeRadenGetal << endl;
16 17 */
do {
18 /* cout << "Debug: Dit is beurt " << uiBeurt << endl; bEindeSpelronde = false; cout << "Geef raadgetal" << endl; cin >> acRaadgetal; uiRaadgetal = strtoul (acRaadgetal, &cLaatsteKarakter, 10); if (uiRaadgetal < 10 || uiRaadgetal > 300 || *cLaatsteKarakter != '\0') { cout << "Ongeldig getal" << endl; } else { if (uiRaadgetal == uiTeRadenGetal) { cout << "Geraden" << endl; bEindeSpelronde = true; } else { if (uiBeurt == 6) { cout << "Niet Geraden" << endl; bEindeSpelronde = true; } else { uiBeurt++;
51
*/ 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
if (uiRaadgetal < uiTeRadenGetal) { cout << "Te Klein" << endl; } else { cout << "Te Groot" << endl; }
35 36 37 38
} } } } while (!bEindeSpelronde);
39
cout << "Nog een spel? (j/n)" << endl; bStop = false; do { cin >> acAntwoord; if (strcmp (acAntwoord, "n") { bStop = true; } } while (!(strcmp (acAntwoord, "n") && !(strcmp (acAntwoord, "j")); } while (!bStop);
40 41 42
cin.get (); return 0;
48 49
} We moeten nu de volgende combinatie van condities testen: if-statement regel 6: strlen (acNaam) >= 1 false m1 false m2 true m3 true
strlen (acNaam) <= 30 false true false true
for-statement regel 7: i < strlen (acNaam) m4 false m5 true if-statement regel 8: isdigit (acNaam[i]) m6 false m7 true
52
onmogelijk!
43 44 45 46 47
if-statement regel 12: !bNaamGoed m8 false m9 true while-statement regel 14: !bNaamGoed m10 false m11 true if-statement regel 23: uiRaadgetal < 10 m12 false m13 false m14 false m15 false m16 true m17 true true true
uiRaadgetal > 300 *cLaatsteKarakter != '\0' false false false true true false true true false false false true true false true true
if-statement regel 26: uiRaadgetal == uiTeRadenGetal m18 false m19 true if-statement regel 30: uiBeurt == 6 m20 false m21 true if-statement regel 35: uiRaadgetal < uiTeRadenGetal m22 false m23 true while-statement regel 39: !bEindeSpelronde m24 false m25 true if-statement regel 44: strcmp (acAntwoord, "n") m26 false m27 true
53
onmogelijk onmogelijk
while-statement regel 46: !strcmp (acAntwoord, "n") !strcmp (acAntwoord, "j") false false m28 false true m29 true false m30 true true
onmogelijk!
while-statement regel 47: !bStop m31 false m32 true In dit programma zijn drie afzonderlijke onderdelen te onderscheiden, die elkaar niet beïnvloeden: - het invoeren van de naam (test m1 t/m m11) - het invoeren van raadgetallen n(test m12 t/m m25) - het geven van het antwoord op de vraag om nog eens te spelen (test m26 t/m m32) Derhalve ligt het voor de hand om telkens aan het einde van het overeenkomstige programmadeel te starten voor het testen van de overeenkomstige condities. We hoeven evenmin in alle gevallen helemaal naar het begin van het programma te rekenen, maar alleen zover de betreffende variabelen veranderen. In het onderstaande geven we steeds aan wat er moet gelden aan het begin van de genoemde regel om te voldoen aan de betreffende testcase. Zodra ook aan een andere testcase voldaan is, tonen we die op aan het begin van de betreffende regel. m10 14 bNaamGoed m8 12 bNaamGoed Dit betekent dat de statements 9 en 11 niet moeten worden uitgevoerd, dus m6 8 bNaamGoed && !isdigit (acNaam[i]) We kiezen dat de for-lus minstens één keer wordt uitgevoerd. De eerste keer dat de for-instructie wordt uitgevoerd, is i=0 en er moet gelden: m5 7 bNaamGoed && !isdigit (acNaam[0]) && (0 < strlen (acNaam)) De if-instructie van regel 6 moet true opleveren, immers bNaamGoed moet true blijven, dus m3 6 bNaamGoed && !isdigit (acNaam[0]) && (0 < strlen (acNaam)) && (strlen (acNaam) >= 1) && (strlen (acNaam) <= 30) In regel 3 wordt bNaamGoed true gemaakt, zodat nog moet gelden: 3 !isdigit (acNaam[0]) && (0 < strlen (acNaam)) && (strlen (acNaam) >= 1) && (strlen (acNaam) <= 30) Hieraan is voldaan als we b.v. kiezen acNaam = P Verwachte uitvoer: "Geef raadgetal" Bij deze keuze is strlen ("P") = 1. De for-instructie wordt nu twee keer uitgevoerd. De tweede keer is i = 1 en geldt dus (i < strlen (acNaam)) is false, immers (1 < 1 is false), zodat ook aan testcase m4 7 is voldaan.
54
m11 14 !bNaamGoed m9 12 !bNaamGoed Dit betekent dat statement 9 of 11 moet worden uitgevoerd. We kiezen hier voor statement 11, zodat de voorwaarde van regel 6 false moet zijn. We kiezen voor m1 6 (strlen (acNaam) < 1) && (strlen (acNaam) <= 30) Hieraan is voldaan als acNaam = (geen invoer, alleen enter). Nu geldt: strlen ("") = 0. Verwachte uitvoer: "Ongeldige naam" en "Geef je naam" m7
8
7
6
m2
7
isdigit (acNaam[i]) De for-lus moet dus minstens één keer worden uitgevoerd. De eerste keer dat de for-instructie wordt uitgevoerd, is i=0 en moet gelden: isdigit (acNaam[0]) && (0 < strlen (acNaam)) We bereiken allen de for-lus als de voorwaarde van de if-instructie van regel 6 true is. Er moet dus gelden: isdigit (acNaam[0]) && (0 < strlen (acNaam)) && (strlen (acNaam) >= 1) && (strlen (acNaam) <= 30) Hieraan is voldaan als b.v. acNaam = 3 Verwachte uitvoer: "Ongeldige naam" en "Geef je naam" (strlen (acNaam) >= 1) && (strlen (acNaam) > 30) Kiezen we een naam van meer dan 30 karakters, dan is hieraan voldaan, bv. acNaam = DitIsEenNaamMetMeerDanDertigKarakters Verwachte uitvoer: "Ongeldige naam" en "Geef je naam"
We gaan nu verder met het invoeren van een raadgetal en de bijbehorende testcases. Laten we in dit geval de methode van 'intuïtief' kiezen van de invoergegevens demonstreren. Hierbij doorlopen we het programma van boven naar beneden en gaan daarbij na welke testcases worden uitgevoerd. Aan het begin van de do-instructie van regel 18 geldt telkens uiBeurt = 1 en aan het begin van de if-instructie van regel 23 geldt telkens !bEindeSpelronde m12 23 uiRaadgetal >= 10 && uiRaadgetal <= 300 && *cLaatsteKarakter == '\0' Om hieraan te voldoen kiezen we uiRaadgetal = uiTeRadenGetal. Nu wordt else-instructie 25 uitgevoerd en is tevens voldaan aan de conditie van instructie 26. m19 26 uiRaadgetal == uiTeRadenGetal 28 bEindeSpelronde = true, zodat geldt m24 39 bEindeSpelronde Verwachte uitvoer: "Geraden" en "Nog een spel? (j/n)" (uiBeurt = 1) (m12) 23 uiRaadgetal >= 10 && uiRaadgetal <= 300 && *cLaatsteKarakter == '\0' We kiezen nu uiRaadgetal = 10 met de bedoeling dat uiRaadgetal < uiTeRadenGetal. Dan geldt
55
m18 m20 m23 m25
26 30 35 39
26 30 m22 35 39
uiRaadgetal != uiTeRadenGetal uiBeurt != 6 immers uiBeurt = 1. uiRaadgetal < uiTeRadenGetal !bEindeSpelronde Verwachte uitvoer: "Te klein" en "Geef raadgetal" (uiBeurt = 2) We kiezen vervolgens uiRaadgetal = 300 met de bedoeling dat uiRaadgetal > uiTeRadenGetal. Dan geldt uiRaadgetal != uiTeRadenGetal uiBeurt != 6 immers uiBeurt = 2. uiRaadgetal > uiTeRadenGetal !bEindeSpelronde Verwachte uitvoer: "Te groot" en "Geef raadgetal" (uiBeurt = 3)
We kiezen vervolgens 4 keer uiRaadgetal = 300 zodat geldt Verwachte uitvoer 3x: "Te groot" en "Geef raadgetal" m21 30 uiBeurt == 6 Nu wordt instructie 32 uitgevoerd, zodat 39 bEindeSpelronde Verwachte uitvoer: "Niet geraden" en "Nog een spel? (j/n)" (uiBeurt = 6) m13 23 uiRaadgetal >= 10 && uiRaadgetal <= 300 && *cLaatsteKarakter != '\0' Hieraan voldoet bv. uiRaadgetal = 55k Verwachte uitvoer: "Ongeldig getal" en "Geef raadgetal" (uiBeurt = 1) m14 23 uiRaadgetal >= 10 && uiRaadgetal > 300 && *cLaatsteKarakter == '\0' Hieraan voldoet bv. uiRaadgetal = 320 Verwachte uitvoer: "Ongeldig getal" en "Geef raadgetal" (uiBeurt = 1) m15 23 uiRaadgetal >= 10 && uiRaadgetal > 300 && *cLaatsteKarakter != '\0' Hieraan voldoet bv. uiRaadgetal = 320k Verwachte uitvoer: "Ongeldig getal" en "Geef raadgetal" (uiBeurt = 1) m16 23 uiRaadgetal < 10 && uiRaadgetal <= 300 && *cLaatsteKarakter == '\0' Hieraan voldoet bv. uiRaadgetal = 5 Verwachte uitvoer: "Ongeldig getal" en "Geef raadgetal" (uiBeurt = 1) m17 23 uiRaadgetal < 10 && uiRaadgetal <= 300 && *cLaatsteKarakter != '\0' Hieraan voldoet bv. uiRaadgetal = 5k Verwachte uitvoer: "Ongeldig getal" en "Geef raadgetal" (uiBeurt = 1)
56
Tot slot moeten de testcases worden opgespoord voor het testen van het antwoord op de vraag of er nog een spel gespeeld moet worden (instructie 40). Instructie 41 zorgt ervoor dat geldt: !bStop We voeren weer intuïtief waarden in voor acAntwoord en gaan vervolgens na welke testcases hiermee worden afgedekt. Kies acAntwoord = J, dan geldt: m26 44 !strcmp (acAntwoord, "n") m30 46 !strcmp (acAntwoord, "n") && !strcmp (acAntwoord, "j") m32 47 !bStop Verwachte uitvoer: (geen reactie) Kies acAntwoord = j, dan geldt: 44 !strcmp (acAntwoord, "n") m29 46 !strcmp (acAntwoord, "n") && strcmp (acAntwoord, "j") 47 !bStop Verwachte uitvoer: "Geef je naam" Kies acAntwoord = n, dan geldt: m27 44 strcmp (acAntwoord, "n") Nu geldt: 45 bStop m28 46 strcmp (acAntwoord, "n") && !strcmp (acAntwoord, "j") m31 47 bStop Verwachte uitvoer: (het programma stopt) Hiermee hebben we alle testcases afgedekt. Overigens is het ondoenlijk om na te gaan of de gegenereerde te raden getallen inderdaad random zijn en minimaal 10 en maximaal 300 bedragen. Dit is het beste op te lossen met code inspection. De functie random() genereert softwarematig random getallen van 0 t/m tenminste 32767. In instructie 16 wordt het resultaat van rand() modulo 291 genomen, zodat de uitkomst hiervan 0 t/m 290 kan zijn. Dit wordt vermeerderd met 10 zodat het te raden getal uiteindelijk 10 t/m 300 kan bedragen. Nu genereert de functie random() echter altijd dezelfde reeks getallen als het programma opnieuw wordt opgestart. We willen steeds een andere reeks. Om dit te bereiken moet de functie rand() telkens worden voorzien van een andere startwaarde (seed). Dit gebeurt door aan het begin van het programma de functie srand() aan te roepen (instructie 1) en die hierbij telkens een andere invoerparameter mee te geven. Hiervoor wordt meestal de uitvoerwaarde van de functie time() gebruikt. Dit is immers de tijd in seconden vanaf middernacht op 1 januari 1970 tot nu. Hiermee hebben we afdoende gecontroleerd dat de reeks getallen inderdaad willekeurig is en telkens anders als het programma opnieuw wordt opgestart. We zetten de gevonden testcases zodanig in volgorde dat we deze testen efficiënt achtereenvolgens kunnen uitvoeren (fig. 23).
57
testcase m1,m9,m11 m7 m2 m3,m4,m5,m6, m8,m10 m12,m19,m24 m26,m30,m32 m29
invoer naam = (geen invoer, alleen enter) naam = 3 naam = DitIsEenNaamMetMeerDanDertigKarakters naam = P
m13
raadgetal = te raden getal antwoord = J antwoord = j naam = P raadgetal = 55k
m14
raadgetal = 320
m15
raadgetal = 320k
m16
raadgetal = 5
m17
raadgetal = 5k
verwachte uitvoer "Ongeldige naam" en "Geef je naam" "Ongeldige naam" en "Geef je naam" "Ongeldige naam" en "Geef je naam" "Geef raadgetal" * "Geraden" en "Nog een spel? (j/n)" (geen reactie) "Geef je naam" "Geef raadgetal" "Ongeldig raadgetal " en "Geef raadgetal" (raadbeurt = 1) "Ongeldig raadgetal " en "Geef raadgetal" (raadbeurt = 1) "Ongeldig raadgetal " en "Geef raadgetal" (raadbeurt = 1) "Ongeldig raadgetal " en "Geef raadgetal" (raadbeurt = 1) "Ongeldig raadgetal " en "Geef raadgetal" (raadbeurt = 1) * "Te klein", "Geef raadgetal" (raadbeurt = 2)
m18,m20,m23, raadgetal = 10 (< te raden getal) m25 m22 raadgetal = 300 (> te raden getal) * "Te groot" en "Geef raadgetal" (raadbeurt = 3) m21 4 keer invoeren: raadgetal = 300 (> te raden getal) * 3 keer: "Te groot" en "Geef raadgetal", daarna: "Niet geraden" en "Nog een spel? (j/n)" (raadbeurt = 6) m27,m28,m31 antwoord = n (programma stopt) * Om deze test uit te voeren is het handig als (tijdens de testfase) het te raden getal en raadbeurt worden afgedrukt. Fig. 23 Testcases volgens multiple condition coverage bij 'Raad het getal'
58
5.2.7
Minimale set testcases bij "Raad het getal"
In deze paragraaf worden de 4 sets testcases samengevoegd tot een minimale set. Deze set dekt alle testcases af die zijn gevonden met de methoden van equivalence partitioning (e), boundary value analysis (b), cause-effect graphing (c) en multiple condition coverage (m). Fig. 24 laat deze minimale set testcases zien. testcase invoer c1 m1,m9,m11,e4, naam = b1 m7,e3,c2 naam = 3 m2,e5,b4 m3,m4,m5,m6, m8,m10,e1,e2, b2,c3 m12,m19,m24, e10,b10,c7 m26,m30,m32, e22,c12 e20,b12 e21,b14 m29,e19,c11
(start het programma) (geen invoer, alleen enter)
"Ongeldige naam" en "Geef je naam"; niet "Geef raadgetal" "Ongeldige naam" en "Geef je naam" "Geef raadgetal" (raadbeurt = 1); geen andere melding
naam = Docent_Joseph_Gerardus_Rouland+ naam = P
raadgetal = te raden getal
*
antwoord = J antwoord = antwoord = ja antwoord = j
m13,e13,c4
naam = P raadgetal = 55k
m14,e15,b8 m15 m16,e14,b5 m17 e12
raadgetal = 301 raadgetal = 320k raadgetal = 9 raadgetal = 5k naam = Jos en raadgetal = 12.5
verwachte uitvoer "Geef je naam" "Ongeldige naam" en "Geef je naam"
"Geraden" en "Nog een spel? (j/n)"; niet "Geef raadgetal" (geen reactie) (geen reactie) (geen reactie) "Geef je naam" (raadbeurt = 1); programma stopt niet "Geef raadgetal" "Ongeldig raadgetal " en "Geef raadgetal" (raadbeurt = 1); geen andere melding "Ongeldig raadgetal " en "Geef raadgetal" (raadbeurt = 1) "Ongeldig raadgetal " en "Geef raadgetal" (raadbeurt = 1) "Ongeldig raadgetal " en "Geef raadgetal" (raadbeurt = 1) "Ongeldig raadgetal " en "Geef raadgetal" (raadbeurt = 1) "Ongeldig raadgetal " en "Geef raadgetal" (raadbeurt = 1)
59
testcase m18,m20,m23, m25,e6,e7,e8, b6,c5 m22,e9,b7,c6
invoer raadgetal = 10 (< te raden getal)
*
raadgetal = 300 (> te raden getal)
*
b9 b11
raadgetal = te raden getal – 1 raadgetal = te raden getal + 1 raadgetal = 300 (> te raden getal), daarna raadgetal = 300 (> te raden getal)
* * *
m21,e23,e24, b16,c9 m27,m28,m31, e17,e18,b13, c13 b3 b15,c10
c8
antwoord = n
(start het programma) naam = Docent_Joseph_Gerardus_Rouland geef 5 keer raadgetal ≠ te raden getal en dan raadgetal = te raden getal * antwoord = j geef 6 keer raadgetal = 10 (< te raden getal)
*
verwachte uitvoer "Te klein" en "Geef raadgetal" (raadbeurt = 2); niet "Ongeldig getal" "Te groot" en "Geef raadgetal" (raadbeurt = 3); niet "Ongeldig getal" "Te klein" en "Geef raadgetal" (raadbeurt = 4) "Te groot" en "Geef raadgetal" (raadbeurt = 5) "Te groot" en "Geef raadgetal", daarna: "Niet geraden" en "Nog een spel? (j/n)" (raadbeurt = 6); niet "Te groot" en niet "Geef raadgetal" (programma stopt)
"Geef je naam" "Geef raadgetal" (raadbeurt = 1); Tot slot: "Geraden" en "Nog een spel? (j/n)"; niet "Niet geraden" "Niet geraden", "Nog een spel? (j/n)" (raadbeurt = 6); niet "Te klein" en niet "Geef raadgetal" twee verschillende reeksen te raden getallen
antwoord = n start het programma twee keer en voer per keer een aantal raadspellen uit * Om deze test uit te voeren is het handig als (tijdens de testfase) het te raden getal en raadbeurt worden afgedrukt. e11,e16
Fig. 24 Minimale set testcases bij "Raad het getal"
60
5.3
Modulair testen
Het voorgaande had vooral betrekking op het ontwerpen van testcases voor een enkele module. De meeste systemen bestaan echter uit een groot aantal modules, al dan niet genest. Modulair testen omvat het systematisch testen van de afzonderlijke systeemmodules (zoals deelprogramma's of afzonderlijke subroutines/functies) alvorens het systeem als geheel wordt getest. Er is een drietal redenen voor deze aanpak. Ten eerste kan op deze wijze de complexiteit van het systeem en dus van het testen daarvan beter worden beheerst. Ten tweede is debugging veel eenvoudiger omdat van veel fouten kan worden vastgesteld in welke module ze zich moeten bevinden. En ten slotte maakt moduletesten het mogelijk om het testproces simultaan uit te voeren. Om modulair testen mogelijk te maken is het noodzakelijk, dat van elke module een goede en complete specificatie bestaat. Voor een functie of subroutine betekent dit - een beschrijving van de aanroep syntax (ook genaamd het "prototype"), - een beschrijving van de werking waarin ook de rol van de parameters omschreven is. - de ingangsvoorwaarden waaraan voldaan moet zijn voordat de functie wordt aangeroepen (preconditie) en - de uitgangsvoorwaarden die gelden direct na uitvoering van de functie (postconditie). De moduletest probeert aan te tonen dat de module niet aan deze modulespecificatie voldoet. Daarbij worden de Black Box en White Box technieken, die hiervoor besproken zijn, gebruikt om testcases te genereren. Ook code-inspectie geschiedt vaak per module. Bij het testen van laag-niveau (elementaire) modules is vooral de White Box methode van belang, naarmate het niveau van de module hoger is wordt White Box testen complexer en gaan de Black Box technieken prevaleren.
5.3.1
Incrementeel en niet-incrementeel testen
Bij het modulair testen zijn twee dingen van belang. Ten eerste het ontwerp van een geschikte verzameling testcases per module. Ten tweede de wijze waarop we de modules worden samengevoegd om het uiteindelijke systeem te vormen. Dat laatste kan op verschillende manieren. De gekozen wijze heeft gevolgen voor de wijze waarop de tests worden uitgevoerd, de benodigde testhulpmiddelen, foutlokalisatie, enzovoorts. Met andere woorden, de manier van samenvoegen van modules is van groot belang. Allereerst zullen we twee manieren van samenvoegen bekijken: incrementeel en niet-incrementeel. Vervolgens zal in de daaropvolgende paragraaf een tweetal incrementele methoden, top-down en bottom-up, nader worden bekeken. Wat is de beste methode van samenstellen? Moeten we eerste de afzonderlijke modules testen en vervolgens de modules samenstellen tot het volledige systeem en dan dat systeem testen? Of is het beter om de volgende te testen module aan de verzameling reeds geteste modules toe te voegen om die module dan in dat verband te testen? De eerste methode noemen we niet-incrementeel testen, de laatste methode incrementeel testen (of ook wel niet-incrementele, resp. incrementele integratie).
61
A
B
C
D
E
F
G
H
Fig. 25 Module invocatiestructuur Laten we als voorbeeld de invocatiestructuur van een programma nemen zoals gegeven in fig. 25. De rechthoeken geven de modules (functies, procedures, subroutines, deelprogramma's) van het programma weer. De pijlen geven aan hoe de modules van elkaar gebruik maken. Module A roept drie andere modules aan: B, C en D. Module B gebruikt module E en module E op zijn beurt module G, enz. Module G wordt aangeroepen/gebruikt door drie modules, te weten C, D en E. A is de "top"module, G en H zijn de "bottom"modules. Niet-incrementeel testen Niet-incrementeel testen betekent dat elk van de modules A t/m H eerst afzonderlijk worden getest. Afhankelijk van de omstandigheden kunnen deze tests achtereenvolgens of simultaan worden uitgevoerd. Ten slotte worden de modules samengevoegd om het aldus gevormde totale programma als geheel te testen. Voor het testen van een afzonderlijke module is allereerst een test driver nodig, een afzonderlijk programma dat de te testen module aanroept met de gewenste testwaarden als invoerparameters en dat vervolgens de uitvoer van de module toont of registreert. Voor de topmodule is zo'n test driver programma niet altijd nodig, namelijk als de topmodule het hoofdprogramma is; een test driver is wel nodig als de topmodule bv. een bibliotheekfunctie of een class-method is. Voorts zijn er meestal een of meer test stubs nodig. Dat zijn apart vervaardigde modules die de plaats innemen van de modules, die door de te testen module worden aangeroepen. Deze stubmodules moeten ervoor zorgen dat ze, als ze worden aangeroepen met bepaalde waarden voor de invoerparameters, de juiste waarden voor de uitvoerparameters retourneren. Voorts moeten ze ook zonodig de systeemtoestand op de juiste wijze veranderen als daar sprake van is. Ze moeten bijvoorbeeld de waarden van globale variabelen of de inhoud van een file aanpassen, maar alleen in zoverre de juiste werking van de te testen module daarvan afhankelijk is. Met andere woorden, test stubs simuleren de werking van de modules die ze vervangen. Voor bottommodules hoeven geen test stubs vervaardigd te worden. Het moge duidelijk zijn, dat vooral het vervaardigen van goede test stubs heel moeilijk kan zijn. Het is slechts zelden mogelijk om de test stub te laten bestaan uit een "lege" procedure die een vast resultaat teruggeeft aan het aanroepende programma. 62
Ze moeten meestal een goede waarde retourneren anders crasht het aanroepende programma. Voorts dienen ze de systeemtoestand (globale variabelen, files, etc.) op de juiste wijze te veranderen, anders kan ook langs deze weg het aanroepende programma crashen vanwege een inconsistente systeemtoestand. In ons voorbeeld moeten er voor module A (het hoofdprogramma) alleen drie test stubs vervaardigd worden die de plaats innemen van de modules B, C en D. Voor bv. module D moet er een test driver gemaakt worden om D aan te roepen en er moeten twee test stubs gemaakt worden die de plaats innemen van de modules F en G. Ook voor de modules C en E moeten er (naast test drivers) test stubs gemaakt worden om de plaats van module G in te nemen. Al deze G-stubs zijn in principe verschillend; ofwel de code is verschillend of het is dezelfde code met verschillende input- en outputwaarden. Voor de onderste modules G en H hoeven er alleen test drivers gemaakt te worden. In totaal moeten er in dit voorbeeld 7 test drivers en 9 test stubs gemaakt worden: test-drivers: B C D E F G H
test-stubs: stub B t.b.v. module A stub C t.b.v. module A stub D t.b.v. module A stub E t.b.v. module B stub F t.b.v. module D stub G t.b.v. module C stub G t.b.v. module D stub G t.b.v. module E stub H t.b.v. module F
Nadat alle modules getest zijn, worden ze samengevoegd, waarna het aldus ontstane systeem in zijn geheel wordt getest. Incrementeel testen Een andere aanpak is incrementeel testen. Hierbij worden niet eerst alle modules geïsoleerd getest, maar elke te testen module wordt toegevoegd aan de verzameling reeds geteste modules. De reeds geteste modules nemen dus voor een deel de plaats in van test drivers en/of test stubs van de niet-incrementele aanpak. Er is een aantal manieren om incrementeel te testen. We kunnen met de bovenste of een van de onderste modules beginnen. En ook zijn er veel mogelijkheden ten aanzien van welke modules we vervolgens toevoegen en in welke volgorde. Stel dat we onderaan beginnen. We kunnen dan kiezen uit de modules G of H. Deze kunnen tegelijkertijd (door verschillende testers) of achter elkaar getest worden. Ook kunnen we alleen G testen en H pas later toevoegen als D en F getest zijn. Aangezien het vaak belangrijk is om de testperiode te minimaliseren, kan het verstandig zijn om G en H tegelijk te testen. Daartoe moeten er test drivers voor G en H ontwikkeld worden. Test stubs zijn niet nodig. Zodra G getest is kunnen we bijvoorbeeld de modules E, C en desgewenst ook D testen. Voor deze modules hoeven we geen test stubs te schrijven om module G te vervangen, want we gebruiken module G zelf. Wel zal er voor F een test-stub geschreven moeten worden ten behoeve van module D. Als D getest is, kan F getest worden. Daarvoor is dan geen test driver nodig (want die rol vervult module D) en ook geen test stub (die rol vervult module H). Nadat ver63
volgens ook module B en A (in enige volgorde) achtereenvolgens zijn toegevoegd, is het gehele systeem samengesteld. Nadat de laatst toegevoegde module is getest, kan ten slotte de systeemtest worden uitgevoerd. Vergelijking tussen wel/niet incrementeel testen Welke beide bovengenoemde methoden verdient nu de voorkeur? Beide methoden hebben hun voor- en nadelen. We zetten deze op een rijtje. Voordelen van incrementeel testen: I1 Niet-incrementeel testen vergt veel meer werk omdat er veel meer test drivers en test stubs vervaardigd moeten worden. Bij incrementeel testen is dat aantal veel minder omdat de reeds geteste modules de rol van test driver en test stub overnemen. I2 Programmeerfouten of foute aannames met betrekking tot de interface tussen modules worden eerder ontdekt bij incrementeel testen, namelijk zodra de betreffende modules worden samengevoegd. Ook de lokalisatie van dergelijke fouten is veel eenvoudiger omdat onmiddellijk duidelijk is welke interface het betreft. Dit in tegenstelling tot niet-incrementeel testen waarbij alle modules in één keer worden samengevoegd. I3 Hoewel bij incrementeel testen de nadruk ligt op het testen van de laatst toegevoegde module (en zijn interfaces) worden vanzelf ook de reeds aanwezige modules weer aan de tand gevoeld. Daarmee worden ze dus grondiger getest dan bij de niet-incrementeel aanpak. Voordelen van niet-incrementeel testen: N1 Bij niet-incrementeel testen is een kortere doorlooptijd mogelijk. De afzonderlijke modules kunnen in principe allemaal tegelijkertijd getest worden, zij het dat daar dan wel veel testers of testgroepen voor nodig zijn. Bij deze afweging moet echter ook worden opgemerkt dat ook het vervaardigen van test drivers en vooral ook test stubs een niet te verwaarlozen inspanning vergt en daarvan zijn er bij niet-incrementeel testen veel meer nodig dan in het incrementele geval. Overigens is ook bij incrementeel testen een zekere mate van parallel testen mogelijk. N2 Niet-incrementeel testen kan goedkoper zijn in termen van de gebruikte computertijd. Test drivers en stubs zijn geheel toegesneden op de onderhavige test. Het gebruik van andere modules als test driver en stub, zoals bij incrementeel testen wordt gedaan, kan aanzienlijk meer computertijd en resources vragen als deze modules zeer rekenintensief zijn of bv. veel schijfaccessen doen. Bij een afweging moet echter ook worden meegenomen dat ook het vervaardigen van test drivers en test stubs computerresources kost. Deze voordelen tegen elkaar afwegend kunnen we stellen, dat incrementeel testen in het algemeen de voorkeur verdient. Met name de punten I1 en I2 zijn zwaarwegend.
5.3.2
Top-Down en Bottom-Up testen
Uitgaande van deze voorkeur voor incrementeel testen is de volgende vraag, welke vorm van incrementeel testen het meest geschikt is. De volgende twee incrementele methoden zijn het meest prominent: top-down testen en bottom-up testen. Merk op 64
dat het hier gaat om teststrategieën die in principe los staan van de wijze waarop het systeem is ontwikkeld. Het systeem kan op een top-down of een bottom-up manier ontworpen of geprogrammeerd zijn, maar kan vervolgens zowel op een top-down of een bottom-up manier getest worden. Top-down testen De top-down strategie begint bij de topmodule (in ons voorbeeld dus module A). Vervolgens dient een volgende module gekozen te worden. Dat kan module B, C of D zijn. Welke module gekozen wordt is in principe niet van belang. Het enige criterium voor de keuze van een module X is dat alle modules die X aanroepen, getest zijn. Er is zelfs parallellisme mogelijk: na A kunnen zowel de modules B, C en D tegelijkertijd getest worden. Ook module E kan reeds getest worden terwijl de test van C en/of D nog bezig is. Module G kan pas getest worden als de modules A t/m E getest zijn. De volgende twee richtlijnen kunnen helpen bij het bepalen welke module vervolgens getest zal moeten worden: 1 Test een kritieke module zo vroeg mogelijk. Onder een kritieke module verstaan we een complexe module, een module met een nieuw algoritme of een module waarvan verwacht wordt dat die mogelijkerwijs veel fouten bevat. 2 Test modules die I/O doen zo vroeg mogelijk. Dit vereenvoudigt het ontwerp van test stubs (anders moeten test stubs gemaakt worden die I/O uitvoeren). Zodra de I/O modules zijn toegevoegd is ook de inspectie van testresultaten eenvoudiger. Nog een mogelijk voordeel is, dat het programma inclusief zijn I/O functionaliteit reeds getoond kan worden (prototype), terwijl nog niet alle functionaliteit aanwezig is. Merk op dat er bij de top-down strategie geen test drivers vervaardigd hoeven te worden. Immers de reeds geteste modules dienen als test driver voor de nieuw te testen module. Het grootste probleem van top-down testen is het volgende: stel dat we een module ter test toevoegen, bijvoorbeeld module F in ons voorbeeld. Voor deze modules is er een aantal testcases ontworpen; deze geven aan welke invoerwaarden we aan module F moeten aanbieden. Het is nu de test driver, die deze waarden moet aanbieden aan F. Wat is echter onze test driver? Het geheel van geteste modules tot nu toe! Dus we moeten aan dat geheel zodanig invoer aanbieden dat aan module F de gewenste invoer gegeven wordt. Dat betekent een (vaak lastige) vertaling van de invoer van F naar de invoer van het programma-in-wording. En ook voor wat betreft de uitvoer zijn er problemen. De uitvoer van F is niet direct zichtbaar, maar geeft aanleiding tot gedrag en (mogelijk maar niet noodzakelijkerwijs) uitvoer van het programma-in-wording. Omdat dus zowel de invoer als de uitvoer van module F vertaald moeten worden naar invoer resp. uitvoer/gedrag van de rest van het programma is top-down testen een verre van eenvoudige zaak. Een voordeel van top-down testen lijkt te zijn dat het mogelijkerwijs hand in hand kan gaan met top-down ontwerp. Bij top-down ontwerp zijn de hoogste modules in de hiërarchie het eerst klaar en daarna worden de lagere modules ontworpen. De hoogste modules kunnen dus reeds gecodeerd en getest worden terwijl de lagere modules nog ontworpen worden. Dit kan een gunstig effect hebben op de doorlooptijd. Toch wijst de praktijk uit, dat dit voordeel vaak schijn is. Immers, het ontwerpproces 65
is intrinsiek een iteratief proces. Vaak wordt op een lager niveau duidelijk, dat op een hoger niveau een ontwerpbeslissing niet goed genomen is en moet worden bijgesteld. Als dat hogere niveau inmiddels gecodeerd en getest is, dan is veel van deze inspanning tevergeefs geweest. Het is daarom verstandiger om de testfase echt na de ontwerp- en codeerfase te plaatsen. Bottom-up testen Bottom-up testen is het omgekeerde van top-down testen. We beginnen bij de laagste modules in de hiërarchie en voegen daar steeds hogere modules aan toe. Aangezien er op het laagste niveau vaak meerdere modules zijn, is parallel testen meestal mogelijk. Ook bij het toevoegen van hoger-niveau modules blijft parallel testen vaak nog geruime tijd mogelijk. In ons voorbeeld kunnen we beginnen met de modules G en H, die parallel getest kunnen worden. Vervolgens worden één of meer modules toegevoegd, bijvoorbeeld E en F. De combinaties E+G en F+H kunnen ook weer parallel getest worden. Er is weer geen vaste regel te geven welke module(s) vervolgens toegevoegd moeten worden. Een richtlijn kan zijn een zodanige volgorde te kiezen dat er zo lang mogelijk parallel getest kan worden. De enige echte eis die gesteld wordt is dat een module slechts toegevoegd kan worden als alle door die module aangeroepen modules reeds getest zijn. Merk op dat er bij de bottom-up strategie geen test stubs vervaardigd hoeven te worden. Immers de reeds geteste modules dienen als test stubs voor de nieuw te testen module. Voor elke toe te voegen module dient er een test driver geschreven worden. Dit is meestal veel eenvoudiger dan het schrijven van test stubs. Immers de test driver hoeft niets anders te doen, dan de te testen module steeds weer opnieuw aan te roepen met steeds andere invoerwaarden, waarna de uitvoerwaarden worden vergeleken met de verwachte uitvoerwaarden. In tegenstelling tot top-down testen hoeft er geen vertaling plaats te vinden van de programmatestinvoer/uitvoer naar de testinvoer/uitvoer van de te testen module. Vergelijking tussen top-down en bottom-up testen Diverse voor- en nadelen van top-down en bottom-up testen zijn reeds genoemd. We geven hier een completer overzicht. Voordelen van top-down testen: 1. Er hoeven geen test drivers geschreven te worden (alleen test stubs). 2. Tijdwinst in samenhang met top-down ontwerp omdat hoog-niveau modules reeds getest kunnen worden terwijl de lager-niveau modules nog in het ontwerpstadium zijn; zoals reeds is uitgelegd, is dit echter een twijfelachtig voordeel. 3. Raamwerkprogramma zijn beschikbaar zodra I/O-modules zijn toegevoegd (bv. te gebruiken als prototype). 4. Geschikt als de meeste fouten in de hoogste-niveau modules voorkomen. Voordelen van bottom-up testen: 1. Er hoeven geen test stubs geschreven te worden (alleen test drivers; dat is aanmerkelijk eenvoudiger dan het schrijven van test stubs).
66
2. Testinvoer is gemakkelijk aan te testen module aan te bieden. Testuitvoer is gemakkelijk te beoordelen. 3. Geschikt als de meeste fouten in de laagste-niveau modules voorkomen. Het zal duidelijk zijn, dat de bottom-up teststrategie in het algemeen de voorkeur verdient. Merk overigens op dat er naast top-down en bottom-up testen ook nog andere vormen van incrementeel testen mogelijk zijn.
5.4
Het uitvoeren van testcases
Hier volgen nog enkele overwegingen die van belang zijn bij het uitvoeren van de test. 1. Bij het uitvoeren van een testcase wordt de actuele uitvoer (en nieuwe toestand) van de te testen module vergeleken met de verwachte uitvoer (en verwachte nieuwe toestand). Als de actuele uitvoer niet in overeenstemming is met de verwachte uitvoer, dan kunnen daar twee redenen voor zijn. Ofwel de module bevat een fout, die met deze testcase wordt ontdekt, ofwel de verwachte uitvoer klopt niet (in dat geval is de testcase dus fout). De oorzaak van een foute testcase kan zijn dat de testontwikkelaar de specificaties niet goed gelezen of begrepen heeft. Ook komt het nogal eens voor dat de specificaties voor meerdere uitleg vatbaar zijn. Om dit soort situaties te vermijden is het verstandig om ook de verzameling testcases te "reviewen" alvorens de test wordt uitgevoerd, met andere woorden, de testcases dienen zelf ook "getest" te worden! 2. Overweeg of het mogelijk is om gebruik te maken van speciale testgereedschappen, die het leven van een tester een stuk kunnen veraangenamen omdat ze veel routinematige werkzaamheden automatiseren, zowel bij testcasegeneratie als bij het uitvoeren ervan. 3. Vergeet niet om, naast het inspecteren van de verwachte uitvoer, ook te kijken of de te testen module dingen doet die hij niet zou moeten doen, bijvoorbeeld het veranderen van de algemene systeemtoestand (globale variabelen, environment variabelen, files, etc.). Met andere woorden, als de module deze variabelen en/of files onveranderd behoort te laten, inspecteer dan of dat ook inderdaad zo is. Voorbeeld: Test bij aanroep van de functie SearchTable (bijlage 1) ook of de inhoud van de tabel aiTable onveranderd is. 4. Zoals in het "vierde gebod" genoemd, dient een programmeur niet de tester van zijn eigen modules te zijn. Het komt echter vaak voor dat de afzonderlijke modules van een programma door verschillende programmeurs worden gemaakt. In dat geval kunnen programmeurs optreden als testers van de modules van collega's. Met name is het een goede zaak dat de programmeur van module A optreedt als tester van module B als module A module B aanroept. 5. De tester dient zich in principe tot de test te beperken. Het debuggen geschiedt door de programmeur van de betreffende module. 6. Als een module een onverwacht hoog aantal fouten blijkt te bevatten, dan dient die module aan een nader onderzoek onderworpen te worden. Dit kan bijvoorbeeld een nadere Code Review of Walkthrough sessie zijn. Zelfs opnieuw ontwikkelen van de desbetreffende module moet serieus overwogen worden.
67
5.5
Andere vormen van functioneel testen
Hoe goed onze testcases ook mogen zijn, toch beperken zij zich in het algemeen tot de gewone input/output functionaliteit zoals weergegeven in de specificaties ("functionele eisen"). Daarnaast zijn er vaak nog bijzondere eisen al dan niet expliciet in de specificaties verwoord. Meestal niet vermeld zijn eigenschappen, die men "redelijkerwijs" van een goed systeem mag verwachten. Net zo min als men bij een auto nog zal specificeren, dat er een achteruitversnelling moet zijn of een gaspedaal, zo mag men van een softwaresysteem redelijkerwijs verwachten dat het niet zal vastlopen (oneindige loop), dat de gebruiker gegevens via het toetsenbord kan intypen, etc. Deze eisen staan dus meestal niet in de specificaties omdat ze gangbaar zijn. Alleen als er wordt afgeweken van het gangbare dan zal dit in de specificaties worden vermeld. In het voorbeeld van de auto is dit het geval als er sprake is van een afwijkende versnelling of manier om gas te geven. In het geval van een softwaresysteem als een oneindige loop juist de bedoeling is (bv. een continu systeem of een fail-safe systeem) of als de gebruiker van een afwijkend invoermedium gebruik maakt. Nu volgen enkele vormen van testen die betrekking hebben op systeemaspecten, die al dan niet in de specificaties staan verwoord. Alleen als ze in de specificaties beschreven staan, zijn deze testen erop gericht om aan te tonen dat het systeem niet aan deze eisen voldoet. Als ze niet gespecificeerd zijn, dan zal er sprake moeten zijn van "impliciete specificaties": een beschrijving van wat in het algemeen onder goed gedrag wordt verstaan. Dan komt testen neer op het aantonen dat het systeem niet aan deze impliciete specificaties voldoet. Het is daarom raadzaam dat een testafdeling over een lijst met algemene specificaties beschikt. Systeemtest De systeemtest omvat de totale test op systeemniveau. Dit omvat functionele tests, zoals eerder uiteengezet, maar ook het testen van andere functionele en nietfunctionele aspecten. Andere vormen van functionele tests staan hieronder beschreven. De systeemtest (ook vaak Alfatest genoemd) is de afrondende test alvorens het product wordt vrijgegeven. Acceptatietest De systeemtest wordt meestal uitgevoerd door de instantie die het systeem heeft vervaardigd. Als het systeem voor één specifieke klant is gebouwd, dan wil die klant meestal niet blindelings vertrouwen op die systeemtest. Een van de redenen is "gebod 4". Daarom zal de klant ook vaak zelf een test op systeemniveau uitvoeren of laten uitvoeren door een onafhankelijke instantie. De acceptatie van het systeem hangt af van de vraag of het de afnemer lukt om aan te tonen dat het systeem niet aan de specificaties voldoet. Een acceptatietest is niet haalbaar bij producten die voor een grote markt bestemd zijn. In dat geval wordt vaak een Bètatest toegepast (zie hieronder). Bètatest Soms wordt een systeem voorlopig vrijgegeven. Het systeem kan dan door de toekomstige gebruikers aan de tand worden gevoeld. Deze gebruikers verkrijgen de bètaversie van het product gratis of tegen een sterk gereduceerde prijs. Zij stellen daar tegenover dat ze gevonden fouten en tekortkomingen terugmelden aan de fabrikant. 68
Deze gebruikt de terugmeldingen om het product te vervolmaken voor de definitieve vrijgave. Volumetest Dit deel van de systeemtest onderwerpt het systeem aan grote hoeveelheden gegevens. Zo kan bij het testen van een compiler een absurd groot programma worden aangeboden. Een link module kan een zeer groot aantal modules moeten verbinden. Een simulator voor elektronische schakelingen kan een schakeling met vele duizenden componenten worden aangeboden. Een tekstverwerker kan een extreem lange tekst moeten verwerken. Als een systeem gebruikt maakt van opslag op verwisselbare media (floppy disks, tapes), laat dan zoveel gegevens opslaan dat één cassette niet voldoende is. Enzovoorts. Volumetesting is er met andere woorden op gericht om aan te tonen dat het systeem grote hoeveelheden gegevens niet aankan. Let er echter wel op dat, als er in de eisen een limiet gesteld is aan de maximale hoeveelheid, deze hoeveelheid niet overschreden wordt. Volumetesting is vaak een tijdrovende en dus dure aangelegenheid. Daarom moet dit soort testen weloverwogen worden uitgevoerd. Vermijd overbodige volumetests, maar zorg er wel voor dat elk systeem in ieder geval aan enkele volumetests wordt blootgesteld. Stress test Ook bij de stress test gaat het om grote hoeveelheden, maar dan per tijdseenheid. Bijvoorbeeld een zeer groot aantal toetsaanslagen per seconde, een zeer groot aantal transacties per uur. Het verschil tussen volumetest en stress test kan goed duidelijk worden gemaakt door het voorbeeld van een typiste. Volumetest komt dan overeen met het typen van een zeer groot document en stress test met het aantal woorden per minuut wat zij kan typen. Omdat stress test een tijdselement bevat, is deze test alleen van belang voor systemen waar verwerkingssnelheid een grote rol speelt, zoals interactieve systemen, real-time systemen, procesbesturingssystemen, enz. Een luchtgeleidingssysteem kan bijvoorbeeld tot zo'n 100 vliegtuigen per sector aan. Dan kan het zinvol zijn om te testen hoe het systeem reageert als een 101e vliegtuig de sector binnenkomt. Ook zinvol is de test waarbij 100 vliegtuigen tegelijk de sector binnenkomen. Bij een multiuser systeem, dat 50 gebruikers moet aankunnen, is het niet alleen verstandig om te testen hoe het systeem zich gedraagt bij 50 gebruikers, maar ook wat er gebeurt als er 50 gebruikers tegelijkertijd inloggen. Veel systemen kunnen dit niet of slecht aan. Toch is het geen onrealistische situatie. Als 's morgens het kantoor opengaat of als het systeem gecrashed is en weer in de lucht komt, dan kan een dergelijke situatie wel degelijk ontstaan. Een vliegtuigbesturingsprogramma dient getest te worden op het tegelijkertijd bedienen van allerlei bedieningsorganen, aangezien dat in een noodgeval best kan optreden. Een procesbesturingsprogramma dient getest te worden op het tegelijkertijd binnenkomen van allerlei signalen. Veel stress tests komen overeen met situaties, die in de werkelijkheid kunnen optreden. Er zijn echter ook stress tests, die situaties nabootsen die waarschijnlijk nooit in het echt zullen gebeuren. Toch zijn ook deze testen van groot belang omdat zij fouten aan het licht kunnen brengen, die in werkelijkheid toch kunnen optreden, ook in minder stressvolle omstandigheden. Overigens zegt de beruchte Wet van Murphy dat alles wat fout kan gaan ook ooit fout zal gaan.
69
Performance test In veel specificaties staan eisen vermeld betreffende de verwerkingssnelheid (throughput) en de responstijd van het systeem. Meestal wordt dit uitgedrukt in statistische termen. Bijvoorbeeld: de responstijd dient in 98% van de gevallen minder dan 2,0 seconden te bedragen. Veelal zijn verwerkingssnelheid en responstijd ook aan elkaar gerelateerd. Naarmate het drukker is (hogere throughput) zullen de responstijden toenemen doordat er steeds langere wachtrijen ontstaan. Denk maar eens aan een supermarkt. Bij een lage throughput (weinig klanten) wordt je snel geholpen. Als het heel druk is, zijn de wachtrijen lang, maar het aantal klanten dat per uur geholpen wordt ligt veel hoger dan wanneer het stil is. Daarom worden performance eisen vaak uitgedrukt in termen van: "de responstijd dient in 98% van de gevallen lager te zijn dan 2,0 seconden bij een verwerkingssnelheid van 20 transacties per minuut". De performance test tracht nu aan te tonen dat deze eisen niet gehaald worden. Soms zijn daarvoor uitgebreide testopstellingen nodig, bijvoorbeeld om een heleboel gebruikers te simuleren die transacties aan het systeem aanbieden. Resource usage test Er kunnen eisen zijn met betrekking tot het beslag dat een systeem legt op de beschikbare algemene middelen. Zo kan een Management Information Systeem regelmatig data verzamelen uit allerlei andere systemen in het bedrijf. Dan kan de eis gesteld worden, dat de belasting van die andere systemen gemiddeld niet meer mag bedragen dan 1 procent. Of een systeem dat af en toe van een ISDN-verbinding gebruik maakt om te communiceren met andere systemen, mag deze verbinding tot maximaal 10% van de tijd bezetten. De resource usage test tracht aan te tonen dat de geformuleerde eisen ten aanzien van het resource gebruik niet gehaald worden. Vaak is deze test goed te combineren met volume of stress tests. Installatietest Sommige softwaresystemen hebben ingewikkelde installatieprocedures. Het spreekt vanzelf dat ook deze getest moeten worden en wel op de verschillende platformen waarvoor het systeem bedoeld is. User interface test Er kunnen expliciet eisen zijn gesteld aan de User Interface. Ook kan in de specificaties verwezen zijn naar een norm waaraan voldaan moet zijn, bv. IBM's SAA-CUA. Het is dan zaak om aan te tonen dat aan deze eisen niet is voldaan. Op het gebied van de User Interface zijn er veel eisen, die niet expliciet zijn verwoord. Dit kunnen aspecten zijn als goed kleurgebruik, een consistente plaatsing van knoppen en velden, robuustheid tegen verkeerde invoer, enzovoorts. Het is verstandig om een checklist te hanteren met User Interface eisen. Deze kan door de systeemontwerper gehanteerd worden, waarna de tester probeert te achterhalen aan welke eisen niet is voldaan. Regressietest Een regressietest vindt alleen plaats als een reeds getest systeem is veranderd, bijvoorbeeld doordat er fouten zijn verbeterd of functies zijn toegevoegd. Er moet dan gekeken worden of de oude functionaliteit daardoor niet in kwaliteit achteruit is gegaan. Met name als de programmastructuur twijfelachtig is, komt het nogal eens voor dat het verbeteren van een fout zelf weer andere fouten introduceert. De regressie70
test bestaat uit het opnieuw uitvoeren van reeds eerder uitgevoerde tests. Dit onderstreept het belang van het bewaren van oude testcases; zie ook "gebod 7".
5.6
Test stopcriteria
Wanneer kunnen we stoppen met testen? Dat is een niet zo eenvoudig te beantwoorden vraag. Immers is de laatst gevonden fout ook de laatste fout? In het algemeen moeten we niet de illusie hebben dat het ons zal lukken om grotere programma's geheel foutvrij te maken. We moeten dus accepteren dat ieder groter programma fouten bevat. Het is alleen wel belangrijk om ervoor te zorgen dat de fouten zich niet te vaak manifesteren. Het is daarom van belang om veel aandacht te besteden aan de meest gebruikte functies van het systeem. Het is niet erg dat fouten, die zelden of nooit optreden, niet ontdekt worden. Alleen als een fout zeer dramatische gevolgen kan hebben (verlies van levens of miljarden euro's) dan is het lonend om veel grondiger dan gemiddeld te testen. Een alternatief voor al te grondig testen is het systeem zodanig te ontwerpen, dat het fail-safe is. Dit houdt in dat het systeem is uitgerust met een apart bewakingssysteem, dat een gevaarlijke systeemtoestand kan detecteren en het systeem vervolgens naar een veilige toestand dwingt. Voor een verkeerslicht betekent dat: alle lampen op rood. Voor een spoorwegovergang: bomen dicht. Voor een röntgenapparaat: röntgenbuis afschakelen. Zo'n bewakingssysteem is zelf weer een (deel)systeem dat fouten kan bevatten en dus getest dient te worden. Maar er is sprake van winst als de onveilige systeemtoestanden betrekkelijk gemakkelijk te identificeren zijn. Het bewakingsdeel is dan ook relatief eenvoudig en kan daardoor gemakkelijker foutvrij gemaakt worden. In de praktijk worden nogal eens de volgende criteria gehanteerd voor het beëindigen van de testfase: 1. Stop wanneer de geplande tijd voor de testfase verstreken is. Dit criterium is nutteloos, want het dwingt in generlei mate tot een flinke testinspanning. Je zou in het extreme geval zelfs de periode kunnen laten verstrijken zonder iets te doen. 2. Stop wanneer geen van de testcases een fout hebben aangetoond. Dit criterium is evenmin bruikbaar omdat het niets zegt over de kwaliteit van de testcases. Het zou zelfs contraproductief kunnen werken als er een grote druk is om het product op een bepaalde datum vrij te geven. Men zou dan wel eens geneigd kunnen zijn om de testcases niet al te moeilijk te maken. Wat dat betreft zijn mensen erg resultaatgericht. Als iemand wordt verteld dat zijn taak volbracht is als zijn testcases geen fouten aantonen, dan kan hij onbewust testcases schrijven die dit doel het snelst realiseren en het toepassen van bewerkelijke testcasegeneratietechnieken vermijden. 3. Ontwikkel testcases met behulp van bepaalde testcasegeneratietechnieken teneinde de kwaliteit van de testcases te garanderen. Stop met testen wanneer alle aldus gegenereerde testcases geen fout meer hebben aangetoond. Dit is een bruikbaar criterium, maar er moeten wel enkele kanttekeningen bij geplaatst worden. Ten eerste zijn niet alle testcasegeneratietechnieken in alle omstandigheden bruikbaar. Ten tweede worden alleen kwalitatief hoogwaardige
71
testcases gegenereerd als degene die de technieken toepast, daarin voldoende bekwaam is. 4. Stop met testen zodra meer dan 90% (of 95%, of 99%) van de fouten gevonden is of zodra de geplande testtijd verstreken is, welke van de twee het laatste is. Hier is het stopcriterium zodanig geformuleerd dat het de primaire taak van het testen ondersteunt, namelijk het vinden van fouten. Dit legt de nadruk op het actief opsporen van fouten. De grote vraag is alleen: hoe weten we wanneer 90% van de fouten ontdekt is? Dan moeten we dus weten hoeveel fouten het programma in totaal bevat. Gelukkig zijn er wel mogelijkheden om een redelijk betrouwbare schatting te maken van het aantal fouten in een programma: Ervaring Als de softwarefabrikant zelf veel ervaring heeft met het schrijven van software van dezelfde soort als het te testen programma, dan weet men ook hoeveel fouten in dat soort software gebruikelijk is (op grond van eigen tests en de fouten die teruggemeld zijn door gebruikers). Als er geen eigen ervaring is, dan is er vaak toch wel een industriegemiddelde bekend voor dit type software. Wiskundige modellen Er bestaan modellen die op grond van het aantal reeds gevonden fouten en de verstreken tijd tussen die fouten een schatting kunnen maken van het totaal aantal fouten. Fish-in-pond De naam is ontleend aan een bekende manier om het aantal vissen in een meer te schatten. Er worden bv. 200 vissen gevangen, gemerkt en teruggegooid in het meer. Als de vissen na verloop van tijd de kans gehad hebben om zich weer goed over het meer te verspreiden, worden er bijvoorbeeld weer 200 vissen gevangen. Als daarvan 15 gemerkt zijn, dan kunnen we daaruit afleiden dat er zich in het meer zo'n 2600 à 2700 vissen bevinden. Op een soortgelijke manier kunnen we bewust een aantal fouten in het programma injecteren, met name typische fouten zoals die veel worden gemaakt. Vervolgens wordt er gekeken hoeveel van de in de testfase ontdekte fouten overeenkomen met de geïnjecteerde fouten. Daaruit is dan een schatting te maken van het totaal aantal fouten in het programma. Deze en nog andere schattingsmethoden blijven echter ruwe benaderingen voor het werkelijke aantal fouten in een programma. Voorts is het belangrijk om in te zien dat fouten voor minder dan de helft zijn toe te schrijven aan fouten in de codering en het logisch ontwerp. De overige fouten worden al eerder in het ontwikkeltraject gemaakt. De grote onzekerheidsmarge in het schatten van fouten heeft grote gevolgen. Stel dat we de testfase beëindigen wanneer 95% van het geschatte aantal fouten aan het licht gebracht zijn. Het betreft een groot programma, waarvan we het aantal fouten schatten op 150 met een onzekerheidsmarge van 20. We mogen de test dus beëindigen als er 143 fouten gevonden zijn. Maar de onzekerheidsmarge maakt de kans groot dat er niet eens zoveel fouten zijn. Dus halen we ons beëindigingscriterium nooit. 5. Per dag of week wordt het aantal gevonden fouten bijgehouden. Het criterium voor beëindigen van de testfase is: het aantal fouten is gedaald tot onder een van tevoren vastgestelde minimum waarde en blijft gedurende een van tevoren vastgestelde periode onder dat minimum (zie fig. 26). Deze techniek wordt in de praktijk veel en met succes gehanteerd. 72
aantal fouten
nieuwe releases
vrijgave
min. periode
drempelwaarde
weeknummer Fig. 26 Verloop van gevonden aantal fouten per week In de praktijk zal er in het begin een groot aantal fouten gevonden worden en het aantal gevonden fouten per tijdseenheid zal een dalende tendens vertonen. Af en toe worden er echter nieuwe "releases" uitgebracht waarin de gevonden fouten verbeterd zijn. Mogelijkerwijs zijn er echter weer wat andere fouten geïntroduceerd, zodat na zo'n release het aantal fouten per tijdseenheid weer stijgt, maar daarna weer verder daalt. Na enkele iteraties tussen testafdeling en programmeerafdeling zal het aantal fouten uiteindelijk zo laag worden dat vrijgave van het programma verantwoord is. Het is wel van belang de drempelwaarde niet te hoog en de duur niet te kort te kiezen.
73
6
Niet-functioneel testen
In paragraaf 5.5 is reeds een aantal testen gegeven die een aanvulling vormen op de algemene functionele testen. Daarnaast zijn er in het algemeen nog andere systeemeisen, die betrekking hebben op andere systeemaspecten dan de functionaliteit. Alle eisen dienen onderworpen te worden aan een test. Zo'n test probeert aan te tonen dat er aan de betreffende eis niet is voldaan. Om dit te kunnen is het nodig dat eisen gekwantificeerd zijn. Met andere woorden, het moet onomstotelijk kunnen worden vastgesteld of er al dan niet aan de eis is voldaan. Elke eis dient dus zodanig geformuleerd te zijn dat deze testbaar is. De eis dat "het systeem gebruiksvriendelijk moet zijn" is niet testbaar, want wat is het criterium voor gebruiksvriendelijkheid? De eis "het programma moet menugestuurd zijn" is daarentegen wel te testen. Ook te testen is: "Vanaf drie maanden na introductie moet meer dan 80% van de gebruikers het systeem kunnen gebruiken zonder de HELP-functie meer dan vijf maal per dag te raadplegen". De eis "het systeem moet veilig zijn tegen inbraak" is weer niet testbaar. De eis "het systeem moet niet te kraken zijn" is wel testbaar (er is maar één succesvolle poging nodig), maar niet realistisch. Immers elk systeem is in principe te kraken. Een wel realistische, testbare eis is bijvoorbeeld: "het mag de hackersclub HackIt niet lukken om binnen een maand het systeem te kraken". Dit voorbeeld geeft aan, hoe creatief men soms moet zijn om eisen testbaar te formuleren! Er zijn verschillende niet-functionele systeemaspecten, die getest kunnen worden. Een niet uitputtende lijst staat hieronder in alfabetische volgorde vermeld. Compatibility testing Veel systemen zijn een nieuwere versie van een ouder systeem. Meestal geldt dan de eis dat het nieuwe systeem "backward compatible" is met het oude systeem, m.a.w. alle modules, files en andere gegevens die onveranderd blijven, moeten ook met het nieuwe systeem bruikbaar blijven. Compatibility testing beoogt aan te tonen dat aan de compatibiliteitseisen niet is voldaan. Configuration testing Veel software wordt ontwikkeld om op meerdere platformen te draaien. Daarbij worden vaak eisen aan het platform gesteld, zoals het processortype, de kloksnelheid, de hoeveelheid werkgeheugen, de hoeveelheid harde-schijfruimte, de aanwezigheid van een geluidskaart, etc. Meestal is het aantal verschillende platformen te groot om ze allemaal te kunnen testen, maar wel is het van belang om het systeem in elk geval met elk type hardware device te testen en ook de minimale en maximale configuratie te testen. Als het programma zelf uit verschillende componenten bestaat, die in verschillende samenstellingen geconfigureerd kunnen worden, dan dient iedere mogelijke combinatie getest te worden. Documentation testing Een goede gebruikersdocumentatie is van het grootste belang. Meestal is er sprake van een installatiehandleiding, een leerboek (tutorial) en een naslagwerk (reference guide). Ook kunnen beknopte overzichtskaartjes of toetsmallen tot de documentatie behoren. 74
De belangrijkste eisen, die we aan de gebruikersdocumentatie stellen zijn 1. dat ze klopt met hoe het systeem zich in werkelijkheid gedraagt, 2. dat ze volledig is en 3. dat ze geschikt is voor de gebruikersdoelgroep. De eerste eis vertaalt zich naar testcases, doordat ieder voorbeeld uit de handleiding wordt uitgeprobeerd. Voor het bepalen van de volledigheid en geschiktheid is een code review-achtige sessie geschikt. Reliability testing Sommige specificaties vermelden eisen met betrekking tot de betrouwbaarheid van het systeem. Betrouwbaarheid (reliability) is zelf gedefinieerd als de kans dat het systeem faalt binnen een bepaalde tijd. Meestal worden eisen echter geformuleerd in termen van beschikbaarheid (availability) of MTTF (Mean Time To Failure). De beschikbaarheid is de tijd dat het systeem beschikbaar is zijn als fractie van de tijd dat het systeem beschikbaar moet zijn. De MTTF geeft de gemiddelde tijd tussen storingen weer. Betrouwbaarheidseisen betreffen vaak een totaalsysteem, waarbij zowel de hardware als de software een rol spelen. Het bepalen van de betrouwbaarheid van hardware is minder moeilijk dan die van software. Er bestaan modellen voor het schatten van softwarebetrouwbaarheid, maar daar gaan we hier niet verder op in. Security testing Soms hebben systemen specifieke security eisen, bijvoorbeeld aangaande de toegankelijkheid van het systeem of van bepaalde gegevens of functies. Security testing richt zich op deze eisen. Serviceability testing Er kunnen eisen zijn betreffende de onderhoudbaarheid van een systeem. Soms zijn systemen uitgerust met speciale gereedschappen voor onderhoud, bv. consistentiecheckers, log-files, trace-gereedschappen (wat gebeurt er allemaal in het systeem), monitor-gereedschappen (hoe is het gesteld met snelheden, bezettingsgraden, throughput), gereedschappen voor het optimaal instellen van systeemparameters, enz. In deze gevallen richt serviceability testing zich op de aanwezigheid en goede werking van de genoemde hulpgereedschappen. Vaak zijn serviceability eisen ook gesteld in termen van de snelheid waarmee bepaalde handelingen verricht moeten kunnen zijn. Service testing is er dan op gericht om aan te tonen dat deze eisen niet gehaald worden.
75
Bijlage 1: SearchTable Specificatie: int SearchTable
/* ** function: ** ** ** ** ** ** ** ** ** pre: ** ** ** post: ** ** ** ** */
(int int int int
aiTable[], iNrValues, iValue, *piIndex);
This function searches the value iValue in table aiTable, which contains iNrValues elements. The index value is returned in *piIndex. The function returns the following value: 0 = success -1 = Value not found in table; in this case is *piIndex undefined. aiTable is sorted in ascending order and iNrValues >= 0 if returnvalue is 0 then 0 <= *piIndex < iNrValues and aiTable[*piIndex] == iValue else there is no i (0 <= i < iNrValues) such that aiTable[i] == iValue
76
Implementatie: int SearchTable
(int int int int
aiTable[], iNrValues, iValue, *piIndex)
{ int iHead = 0; int iTail = iNrValues; while (iHead < iTail) { *piIndex = (iTail + iHead) / 2; if (aiTable[*piIndex] == iValue) { return 0; } if (aiTable[*piIndex] < iValue) { iHead = *piIndex + 1; } else { iTail = *piIndex; } } return -1; }
77
Bijlage 2: MeanOfTable Specificatie: float MeanOfTable /* ** function: ** ** ** ** pre: ** ** post: ** */
(int aiTable[], int iNrValues);
This function returns the mean value of the values in table aiTable, which contains iNrValues elements. iNrValues > 0 iNrValues * MeanOfTable (aiTable, iNrValues) == the sum of all values in table aiTable.
78
Implementatie: float MeanOfTable
(int aiTable[], int iNrValues)
{ long lSum = 0; int iIndex = 0; while (iIndex < iNrValues) { lSum += aiTable[iIndex]; iIndex++; } return (float) lSum / (float) iNrValues; }
79
Bijlage 3: Triangle Specificatie: Het programma Triangle leest een regel met drie integer getallen A, B en C, onderling gescheiden door een of meer spaties, van de Standaard Invoer (stdin). Deze drie getallen stellen de lengten van de zijden van een driehoek voor. Het programma geeft de volgende uitvoer naar Standaard Uitvoer (stdout): GELIJKZIJDIG als de driehoek met zijden A, B en C een gelijkzijdige driehoek is, GELIJKBENIG als de driehoek met zijden A, B en C een gelijkbenige driehoek is, ONGELIJKBENIG als de driehoek met zijden A, B en C geen gelijkbenige driehoek is. ONGELDIG als de invoer niet overeenkomt met een geldige driehoek.
Implementatie: Niet van belang voor dit dictaat.
80
Bijlage 4: Mean Specificatie: Het programma MEAN leest een file met fixed-point real getallen in. Syntax van aanroep: MEAN [<precision>] De file bevat maximaal 1000 niet-lege regels met op elke regel precies één getal, dat moet voldoen aan de volgende syntax:
::= ::= ::= ::= ::= ::=
. ['+' | '-']. [] | . {}. '0' | '1' | '2' | ... | '9'. '.'.
Lege fileregels zijn toegestaan en worden genegeerd. De waarde van elk getal is minimaal -1020 en maximaal +1020. Het programma berekent het gemiddelde van de getallen in de file en drukt het resultaat af naar standaard uitvoer, afgerond naar het aantal cijfers achter de komma zoals aangegeven door de parameter <precision>. De parameter <precision> is een integer waarde (zonder teken) met een minimale waarde van 0 en maximale waarde van 10. Als deze parameter niet wordt meegegeven is de default waarde 0. Bij een waarde 0 wordt het gemiddelde zonder decimale punt afgedrukt. In de volgende gevallen wordt een melding naar standaard uitvoer afgedrukt en het programma beëindigd (gegeven wordt de conditie gevolgd door de foutmelding): * MEAN wordt aangeroepen met een onjuist aantal parameters: CORRECT SYNTAX: MEAN [<precision>] * De file kan niet geopend worden: FILE COULD NOT BE OPENED * De waarde van <precision> is buiten bereik of geen geheel getal: PRECISION MUST BE INTEGER VALUE IN RANGE 0...10 * De waarde van een getal in de file voldoet niet aan de syntax: INVALID NUMBER: * Een regel bevat meer dan één getal: dit wordt gelijkgesteld aan een syntaxfout (zie vorige item). * De waarde van een getal in de file valt buiten het gespecificeerde bereik: NUMBER OUT OF RANGE: . * De file bevat geen geldige getallen: FILE IS EMPTY. * De file bevat meer dan 1000 getallen: TOO MANY NUMBERS.
Implementatie: Niet van belang voor dit dictaat.
81
Bijlage 5: Gcd Specificatie: int Gcd (int iA, int iB); /* ** function: This function returns the Greatest Common ** Divisor of the values iA and iB. ** ** pre: iA > 0 and iB > 0 ** ** post: Let z = Gcd (x, y) then z is the greatest ** integer value such that x mod z = y mod z = 0. */
82
Implementatie: int Gcd (int iA, int iB) { int iResult = iA; if (iB < iResult) /* optimalisatie */ { iResult = iB; } while (iA % iResult > 0 || iB % iResult > 0) { iResult--; } return iResult; }
83
In deze onderwijspublicatie is géén auteursrechtelijk beschermd werk opgenomen.
84