Follow Us on Twitter

Test first, implement later

Maart 2009 - Bij het ontwikkelen van applicaties wordt er vaak pas in de laatste stadia van het ontwikkeltraject gekeken of software daadwerkelijk doet wat het zou moeten doen.Testers kijken pas in de integratie fase of de applicatie voldoet aan de specificatie. Eindgebruikers en opdrachtgevers kijken pas in de acceptatie fase of aan de verwachtingen voldaan is. Naarmate het ontwikkelproces van een applicatie vordert, worden fouten steeds duurder om op te lossen. Daarom is het wenselijk fouten zo vroeg mogelijk te vinden. Om fouten te ontdekken worden daarom in de ontwikkelfase unit tests gebruikt. Unit tests worden door de ontwikkelaar geschreven om de functionele werking van de code te controleren.

Unit tests worden over het algemeen pas na de code zelf geschreven. De test word daardoor vaak volgens de logica van de bestaande code geschreven. Hierdoor worden fouten in de logica regelmatig niet gevonden. Test Driven Development is een andere aanpak van unit tests welke zorgt dat tijdens de ontwikkel fase meer fouten gevonden worden.

In dit Whitebook beschrijven we Test Driven Development en geven we een aantal handvatten voor het effectief gebruik ervan in het ontwikkelproces. Daarnaast kijken we naar tooling en worden mogelijke pitfalls toegelicht.

Algemene beschrijving

Traditioneel wordt er eerst code geschreven, en als er nog tijd is, unit test cases gemaakt. Bij Test Driven Development wordt er eerst nagedacht over wat er eigenlijk geïmplementeerd moet worden, en hoe dit getest kan worden. Daarna worden de test cases geschreven, en pas als laatste de daadwerkelijke implementatie. Deze methodiek zorgt voor een implementatie waarbij er in eenzelfde tijdsbestek veel meer aandacht is geweest voor de werking en testbaarheid van de code. Ook kan er tijdens de ontwikkelfase continu gecontroleerd worden welke delen van de applicatie al werkend zijn, en wat er nog te doen staat.

Ontwikkelen met Test Driven Development

Aan het ontwikkelproces met Test Driven Development zitten twee belangrijke zaken vast die vaak voor problemen zorgen. Ten eerste moet de projectleiding er van overtuigd worden dat de Test Driven Development methodiek daadwerkelijk zorgt voor een sneller ontwikkeltraject. En de ontwikkelaars zullen met een heel andere strategie het ontwikkeltraject in moeten gaan. De meeste ontwikkelaars werken bottom up en beginnen dus met een heel algemeen idee van hoe (een deel van) het project moet gaan werken. Test Driven Development eist echter dat aan het begin van het ontwikkeltraject al in hoge mate bekend is hoe elk deel van het project moet gaan werken. Zie figuur 1 voor een schematisch overzicht van het Test Driven Development ontwikkelproces.

Test Driven Development process
Figuur 1: Test Driven Development process

Als de projectleiding is overtuigd dat Test Driven Development gebruikt moet gaan worden, moeten de ontwikkelaars aan de hand van de specificaties gaan kijken naar de te maken applicatie. Aan de hand van de functionele en technische specificaties wordt het project in delen opgedeeld, en voor elk deel wordt er bepaald hoe het precies moet gaan werken. Dit betekent dus input, output mogelijkheden, grensgevallen, etcetera.

Aan de hand van deze testcases worden dan unit tests geschreven. Om snel te kunnen testen is het verstandig om veel met interfaces te werken en frameworks als junit, dbUnit en httpUnit te gebruiken. Door zoveel mogelijk aan het begin van het ontwikkelproces te denken aan hoe iets getest moet gaan worden, wordt het implementeren van deze tests een stuk makkelijker dan achteraf. Vaak blijken achteraf blokken code die eigenlijk onafhankelijk getest moeten worden, sterk aan elkaar vast te zitten. Daardoor gaat er veel tijd zitten in het losweken van deze blokken om ze testbaar te maken.

Uiteindelijk worden dan de code blokken geïmplementeerd waarbij er steeds gekeken wordt of de testcases correct draaien. En zo niet dan kan al tijdens het ontwikkelen van dat stuk code direct het probleem gevonden worden en opgelost. Normaal worden deze problemen vaak pas veel later gevonden. Ofwel tijdens het schrijven van unit tests achteraf, ofwel tijdens integratie- of acceptatie testen. In die gevallen moet er eerst weer gekeken worden naar de code, waarbij je maar mag hopen dat dezelfde ontwikkelaar beschikbaar is. Daarnaast moet de ontwikkelaar zich eerst weer bekend maken met dat stuk code wat relatief veel tijd kan kosten. Deze vertraging is een deel van de oorzaak van de hoge kosten van het oplossen van fouten in latere fases. Andere kosten zijn bijvoorbeeld testers die moeten wachten tot de fout opgelost is.

Voorbeeld

Als voorbeeld zullen we een deel van de case van de jaarlijkse Whitehorses Developer Derby gebruiken. Het gaat hier om een applicatie om het voor leaserijders makkelijk te maken om de rittenadministratie bij te houden. Een beheerder moet auto's aan contracten kunnen verbinden. Echter zijn hier een aantal regels aan verbonden. In dit voorbeeld gaan we uit van deze (versimpelde) set regels:

  • Elk contract heeft een auto.
  • Contracten hebben een begin- en einddatum.
  • Een auto kan meerdere contracten hebben.
  • Een auto mag niet meerdere contracten hebben waarvan de periodes overlappen.

We willen nu zorgen dat het stuk van de applicatie dat moet zorgen voor de afhandeling van contracten correct werkt. Dus moeten er testcases opgezet worden. De eerste drie regels worden al afgevangen in het datamodel dat te zien is in Figuur 2.

Datamodel voor de tests
Figuur 2: Datamodel voor de tests

Een contract bestaat uit vier attributen: een ID, een auto id, een startdatum en een einddatum. De auto tabel heeft een id, een kenteken, een type en een merk.

Er blijft dus één bedrijfsregel over die goed moet worden getest: “Een auto mag niet meerdere contracten hebben waarvan de periodes overlappen”. Voor het bepalen voor overlap gebruiken we de volgende interface.

boolean checkVoorOverlap(Contract nieuwContract, Collection contracten)

Er is gekozen om een IllegalArgumentException te gooien als het contract overlapt met een ander contract voor dezelfde auto. Voor onze tests moeten we nu controleren hoe deze methode omgaat met de testsituaties. Een aantal testsituaties (met verwacht resultaat):

  • Er is nog geen contract voor de Auto (accepteren).
  • Er is een contract voor de auto die afloopt voor de opgegeven begindatum (accepteren).
  • Er is een contract voor de auto die begint na de opgegeven einddatum (accepteren).
  • Er is een contract voor de auto die afloopt na de opgegeven begindatum en voor de opgegeven einddatum (afwijzen).
  • Er is een contract voor de auto die begint na de opgegeven begindatum en voor de opgegeven einddatum (afwijzen).
  • Er is een contract voor de auto die begint voor de opgegeven begindatum, en eindigt na de opgegeven einddatum (afwijzen).

In bijna alle gevallen hebben we hier bestaande contracten nodig. Dit is echter goed op te lossen met dbUnit en een in-memory database als HSQLDB. Met XML-configuratie maken we een aantal auto's en contracten aan die bij het opstarten van de test worden geladen. Zo kan er onafhankelijk van infrastructuur (databases etc.) getest worden en is de status van de database aan het begin van de tests altijd hetzelfde.







Code 1: XML configuratie om twee auto’s en een contract in de database te zetten met dbUnit

Nu kunnen we dus testcases schrijven waarbij we vooraf weten wat er in de database staat. In dit geval zijn twee auto’s en een contract voor één van de auto’s voldoende.

In de testcase hoeven we dan alleen nog maar te proberen contracten aan te maken met verschillende start- en einddata:

public void setUp() throws Exception {
// code om dbUnit data in hsqldb in te lezen
}

public void testCheckForOverlap() {
Auto autoZonderContract = // code om auto uit de database te halen
Collection bestaandeContracten =
// code om alle (relevante) contracten uit de database te halen

// eerst controleren of een auto zonder contract correct afgehandeld wordt
Contract nieuwContract = new Contract(Calendar.getInstance()
, Calendar.getInstance()
, autoZonderContract);
boolean result = ContractUtil.checkForOverlap(nieuwContract
, bestaandeContracten);
assertFalse("Er zou geen overlap moeten zijn", result);
....
}

Code 2: Deel van de testcase

Nu de testcode geschreven is, kan er worden begonnen met de daadwerkerlijke implementatie van de functie.

Handige technieken

Tegenwoordig is er voor bijna elk aspect van een applicatie wel een unit test framework te krijgen, httpUnit voor web interactie, dbUnit voor database, mocking voor het testen van klasses die sterk verweven zijn. Voor het testen van database logica is utPLSQL erg geschikt.

Welke technieken er geschikt zijn, is afhankelijk van het type applicatie. Over het algemeen zijn dbUnit en httpUnit erg handige frameworks. dbUnit zorgt voor een datamodel dat elke keer weer met exact dezelfde data gevuld is en zorgt er daardoor voor dat ook unit tests op data access niveau consistent en herhaalbaar zijn.. httpUnit test op een vrij eenvoudige manier het gebruik van een webapplicatie door het simuleren van interacties.

Pitfalls

Uiteraard is Test Driven Development geen vervanging van de integratie en acceptatie testen. Het vermindert wel het aantal gevonden defecten in deze testfases. Ook moet goed gekeken worden naar wat er getest gaat worden. Er zit een enorm grijs gebied tussen geen testen en alles testen. Niet testen is geen alternatief en alles testen is te tijdrovend en levert vaak te weinig op. Daarom moet er een goede balans gevonden worden. Een stelregel is dat alle business logica getest moet worden, en dat alle triviale functionaliteit (getters, setters etc.) overgeslagen kan worden.

Ook is Test Driven Development voornamelijk geschikt voor nieuwe projecten. Voor het aanpassen van bestaande code is de business case voor Test Driven Development een stuk zwakker doordat het vrij waarschijnlijk is dat er moeilijk te testen code wordt gevonden.

Conclusie

Test Driven Development biedt de mogelijkheid om een aantal veel voorkomende problemen bij het ontwikkelen van applicaties te voorkomen. Het wordt mogelijk om fouten in een eerder stadium te vinden. Bovendien zorgt de nadruk op testen vanaf het begin er voor dat ontwikkelaars beter testbare code gaan schrijven. Test Driven Development is wel een methodiek die het beste werkt als er vanaf het begin van een project mee gewerkt wordt. Halverwege overschakelen zal over het algemeen veel tijd kosten en de winst is in die gevallen onzeker.

Referenties

Waardering:
 

Reacties

Nieuwe reactie inzenden

De inhoud van dit veld is privé en zal niet openbaar worden gemaakt.

Meer informatie over formaatmogelijkheden

CAPTCHA
Deze vraag is om te testen of u een persoon bent en om spam te voorkomen
Image CAPTCHA
Enter the characters shown in the image.