Geen paniek, falen mag bij testgedreven software-ontwikkeling

Steven de Vries
Steven de Vries

4 juni 2020

De waarde van testen door een Enrise testspecialist

Test je software pas als het klaar is? Dan ben je te laat. Bij een testgedreven ontwikkelmethode test je daarentegen al voordat er überhaupt een regel code geschreven is. Gegarandeerd dat de eerste test faalt. Van hieruit ga je ontwikkelen wat wel werkt. Het helpt om al in een vroeg stadium problemen te tackelen, fouten te voorkomen en software stabieler en veiliger te maken.

Bij een testgedreven ontwikkelproces test je niet de software als het geschreven is, maar ontwikkel je eerst een test en op basis daarvan ontwikkel je alleen wat nodig is om de test te laten slagen. De term Test Driven Development komt altijd voor in onze uitleg over hoe we werken bij Enrise. In dit artikel nemen we je mee door diverse testmethoden die we inzetten tijdens de ontwikkeling van sites, shops, portals en apps voor bedrijven als Simpel.nl en Dr. Oetker.

Goede testvoorbereiding

Als het gaat over testen, dan worden al snel afkortingen gebruikt. Binnen ons vak korten we graag af tot enkele letters. De drie afkortingen die we binnen software-ontwikkeling vaak gebruiken zijn:

  • DDD, van Domain Driven Design
  • BDD, dat staat voor Behaviour Driven Development
  • TDD, het daadwerkelijke Test Driven Development

Domain Driven Design

Goed testen kun je pas als je weet hoe de business, het domein, werkt. Daarom beginnen we met het uitschrijven van een verklarende woordenlijst, een glossary, met domeintermen. Dat is vakjargon, de taal van het bedrijf, het domein. Het opstellen van de woordenlijst is nog geen werk voor een tester of een developer, al kan dat uiteraard wel. Meestal is het de Product Owner binnen een project die samen met de klant zo’n lijst opstelt.

Door vakjargon in de code te gebruiken kunnen we al tests opstellen voordat we code geschreven hebben. De business flows binnen een applicatie die de Product Owner heeft omschreven, dienen als input voor de tests en basis voor de uiteindelijke code.

Met testen in dit vroege stadium toets je ook functionele wensen. Als klant ga je meestal uit van de happy flows, ga je uit van de juiste en logische stappen die een gebruiker zet, zoals je ze hebt bedoeld. Nu kun je ook afwijkende flows opmerken en daar keuzes in maken voordat er code wordt geschreven.

Behaviour Driven Development

Vanaf dit moment kan een tester worden ingeschakeld. Wat hij of zij als eerste zal doen is het wenselijke systeemgedrag beschrijven. Behaviour Driven Development (BDD), noem je dat. BDD is erop gericht dat je vanuit (business)gedrag een test opstelt en niet vanuit techniek. Met exact de termen volgens de woordenlijst, wordt uitgeschreven wat het systeemgedrag in een bepaalde flow doet of wat het behoort te doen bij uitzonderingen. Dit is nuttig, omdat je ermee kunt ontdekken of je processen en benamingen mist. Maar ook maakt het duidelijk wat je moet ontwikkelen en wat niet.

Voor Behaviour Driven Development werken wij met Behat.

Test Driven Development

Nu alle randvoorwaarden in de glossary zijn opgenomen en gedragsbeschrijvingen zijn gedefinieerd, kun je de eerste tests uitvoeren. Belangrijk om te weten is dat je bij een testgedreven ontwikkelproces niet de software test als het geschreven is, maar eerst een test ontwikkelt en op basis daarvan alleen ontwikkelt wat nodig is om de test te laten slagen. 

Tegelijk werken we continu aan de onderhoudbaarheid van code. Een test die slaagt met slordige code wil niet zeggen dat het resultaat dan prima is. Ook zullen tijdens de ontwikkeling edgecases voordoen. We voegen daarom steeds tests toe.

Elke test zal in eerste instantie falen. Logisch, want er moet nog software geschreven worden. Door stap voor stap te testen en te ontwikkelen bouw je aan een solide basis die niet stuk gaat als je softwareproject omvangrijker wordt. Omdat je ieder nieuw stukje code ontwikkelt op basis van een testscenario is de kans op misstappen en onnodige zijsprongen zeer klein.

Unit test

Volgende stap is het unit testen van de geschreven code. Waar het net nog ging om het slagen of falen van code-uitvoer, gaat het bij unit tests om de test van de kleinst mogelijke eenheid. Bijvoorbeeld een bepaalde functie, zoals op een knop drukken waardoor iets in beeld verschijnt. Het is een op zichzelfstaande functie, die geen externe bronnen raadpleegt. In een unit test worden alle externe afhankelijkheden gemockt, zodat we precies weten welke data er in een functie gebruikt wordt en controleren we de uitkomst. 

Als ik A erin stop, moet B eruit komen. En als ik B erin stop, moet C eruit komen. Zo klein en concreet is een unit test.

Edge cases

Wat logisch is, is de happy flow: muntje erin, kauwgombal eruit. Of meer digitaal: 1 product in de winkelwagen = een lege winkelwagen +1. Bij unit tests worden de zogeheten edge cases aan de tand gevoeld. Net als bij BDD (zie boven), maar nu technisch. Dus de situaties die niet vaak voorkomen. Of die niet logisch zijn. Je houdt rekening met ander gedrag. Zoals een product dat niet beschikbaar is. Of een postcode zoeken die niet bestaat. We noemen dit ook wel Defensive programming: Klopt het wat ik in elke situatie terugkrijg?

Wij gebruiken onder andere PHPUnit voor backend-code.

Integratietest

Tijdens unit testen zijn de kleine onderdelen van een groter geheel getest en doorontwikkeld. Onafhankelijk van elkaar werkt het nu prima, maar je moet ook de keten waarin die onderdelen samenwerken controleren. Terug naar het voorbeeld van de winkelwagen: Je zoekt een product, vindt het en plaatst het in je winkelmand. Je past misschien aantallen aan of je verwijdert een ander product en rekent af via een gekozen betaalmethode. Nu is het tijd om die keten als geheel te testen: de integratietest. De flow wordt geautomatiseerd of met mensenhanden door een tester doorlopen om na te gaan of de keten goed verloopt, er geen fouten in zitten en waar nog onderdelen bijgewerkt moeten worden. 

Prullenbak werkt zoals bedoeld. Handdroger werkt zoals bedoeld.
Wat als ze samenwerken? De integratietest geeft daar antwoord op.

Zodra je wijzigingen doet in de units, is de volgende fase eigenlijk geen integratietest meer, maar noemen we het een regressietest: ook al is de wijziging nog zo klein, je zult de hele keten opnieuw moeten doorlopen om te ontdekken of je aanpassingen geen gevolgen hebben gehad voor de andere onderdelen in de keten. 

Nog een mooi voorbeelden van falende integratie:
Sensoren werken. Deuren werken. Hekjes werken. Maar of dit het probleem oplost…?

Je maakt met testwerk wel keuzes. Je kunt immers niet elke mogelijk toekomstige – niet-logische – benadering nabootsen. Een 100% dekking van fouten bestaat dan ook niet. Richt je bij elke testfase op de onderdelen die de meeste waarde leveren voor de betreffende business. Dus bij een flow voor een offerte-aanvraag is het belangrijk dat een gebruiker die kan voltooien, zodat daadwerkelijk een offerte-aanvraag in gang gezet kan worden. Dat is de directe waarde voor je business.

Styling test

We zijn al lekker op weg! Functioneel werkt alles nu top. In feite voldoende voor een keurige livegang, maar er is meer wat je kunt doen om optimale software op de markt te brengen. Want dat het werkt is fijn, maar is de code ook netjes geschreven? Ook dat kun je geautomatiseerd testen! 

Je kunt code checken op bijvoorbeeld het gebruik van spaties, komma’s, lege regels en andere oneffenheden. De test brengt ze aan het licht, zodat je je werk kunt verbeteren en finetunen. Prettig voor jezelf en voor de mensen na jou met je code aan de slag gaan. 

De juiste toolset voor het testen van je codestyling is afhankelijk van je programmeertaal en frameworks. Frontend, backend, CSS-checks of PHP-controles: voor elke codestandaard is een aparte tool. Bij Enrise gebruiken we voor backendcode, bijvoorbeeld voor PHP, ECS (Easy Coding Standards), PHP Mess Detector of Linting tools. Dat gaat wel verder dan alleen spaties en komma’s, want je analyseert en traceert er ook bugs mee, suboptimale code (bijna goed, maar net niet) en ongebruikte parameters.

Als code bestaat uit tientallen geneste items, dan veroorzaakt dat meer onderhoud, logisch.  Voor frontendontwikkeling maken we onder andere gebruik CodeSmell, dat de complexiteit van je code wil reduceren. De nadruk bij CodeSmell ligt op onderhoudbaarheid. Want schone code is beter te onderhouden. De tool stimuleert opschoning en zorgt ervoor dat iedereen de code op dezelfde manier schrijft en leest.

Als je 30 div’s genest hebt, dan maak je je werk onnodig complex. Misschien niet direct, maar je krijgt het ooit terug bij het onderhouden van je code. 

Security

Je code is netjes, de flows werken voortreffelijk, het moment om van buitenaf je software aan de tand te voelen. Tijd voor de security test om te ontdekken of de app, api of site veilig is en vrij van kwetsbaarheden van buitenaf. 

Bij Enrise werken we hoofdzakelijk met open source oplossingen voor onze projecten. Van open source software worden kwetsbaarheden actief bijgehouden. Middels onze Continuous Integration pipeline kunnen we de actuele kwetsbaarheden naast onze code leggen om realtime te controleren of die kwetsbaarheden ook van toepassing zijn op onze code.

We gebruiken voor zowel backend (composer) als voor frontend (NPM) modules een security checker om mogelijke problemen qua secority vroegtijdig te signaleren.

Accessibility test

Of je nu je online vindbaarheid wilt verbeteren, een zo breed mogelijk publiek wilt bereiken of gewoon voorop wilt lopen als het gaat om wet- en regelgeving, dan neem je Web Accessibility serieus. We hebben in samenwerking met SIDN een tool ontwikkeld die checkt of de applicatie voldoet aan toegankelijkheid voor blinden en slechtzienden. 

Bijna een jaar geleden mochten we ons scharen onder de eerste lichting SIDN-Pioniers van 2019. Dankzij de steun van het SIDN fonds konden we van start met de ontwikkeling van onze Continuous Accessibility Checker. Een tool waarmee programmeurs hun ontwikkelde software al tijdens de testfase kunnen checken op toegankelijkheid voor blinden en slechtzienden. Collega Henk-Jan van Voorthuijsen, developer en initiatiefnemer van de tool sprak met het fonds over de tool en de steun.

Afbeelding: tijdens de test zijn twee issues gesignaleerd.

Handmatig testen

Behalve alle automatische tests zijn er ook tests die we handmatig doen. Dat begint bijvoorbeeld met code reviews. We checken elkaars code, stellen de code op de proef en vragen door over de gekozen oplossingsrichting. Twee zien meer dan een, dus samenwerken aan dezelfde oplossing maakt de code beter. 

Daarnaast maken onze testers captures, opnames van de routes die je als gebruiker aflegt. Inclusief navigatie, muiskliks en invoervelden. Doordat we die test als opname kunnen bewaren, kunnen we ‘m later, bij wijzigingen en verbeteringen alsnog geautomatiseerd laten testen volgens diezelfde stappen. 

We gaan het liefst nog een stap verder. We zijn namelijk developers pur sang, dus willen we zoveel mogelijk automatiseren. Door het opgeslagen handmatige testproces te exporteren, kunnen we het onderbrengen in onze continuous integration pijplijn. Zodra nieuwe software voltooid is, kan het van kleinste unit-test tot volledige testflow worden uitgevoerd: Pagina’s worden bezocht, formulieren ingevuld en verstuurd en er worden logs en rapportages bijgehouden van wat slaagt en wat faalt.

Continu blijven testen

Het testen van je code en applicaties houdt niet op alles eenmaal live en in gebruik is. Ook dan is blijven testen belangrijk om de kwaliteit, netheid, onderhoudbaarheid, veiligheid en toegankelijkheid te blijven challengen. 

Performance Monitoring

Met permanente monitoring van je applicaties houdt je onder andere grip op veiligheid, snelheid en stabiliteit. Zelfs als alle tests uitwijzen dat je applicatie top draait, kun je als gebruiker nog steeds de ervaring hebben dat alles traag en stroef gaat. Dankzij permanente monitoring kun je ontdekken waar de bottleneck zit. Bijvoorbeeld in welk applicatiedeel de meeste rekenkracht zit. Of waardoor het toch komt dat bezoekers afhaken in een bestelproces terwijl de winkelmand al gevuld is.

Voor application performance monitoring maken we onder andere gebruik van New Relic.

Ook monitoring van externe bronnen is essentieel om controle te houden op wat er met je applicatie en dus ook met je bezoekers gebeurt. Valt een externe database weg? Heeft een API van een leverancier een storing? Dankzij monitoring weet je dit al voordat de eerste gebruiker er last van heeft en kun je erop anticiperen.

Monitoring kun je nog breder trekken. Wij monitoren bijvoorbeeld ook clusters en servers, de uptime van externe bronnen of heel specifiek op bepaalde jobqueues. Dankzij dit brede inzicht in de gezondheid en prestaties van hardware en software kunnen we vrijwel realtime ingrijpen bij veranderingen.

Testen en je time to market

Testen, testen, testen. Ik hoor je denken: hoelang gaat het duren voordat een applicatie dan eindelijk in gebruik genomen kan worden? En wat kost dat allemaal? 

Door veelvuldig te testen en erop te leren vertrouwen dat die uitkomsten tot goede resultaten leiden, kun je dankzij testen onderhoudskosten en bugfixes besparen. Ook kun je dankzij een testgedreven ontwikkeling je time to market verkorten door je testfase samen te voegen met je acceptatiefase. Alle tests geslaagd? Dan kan de applicatie direct naar de productie-omgeving.

Opleveren doen we bij Enrise bij voorkeur op feature-niveau: continu kleine functionaliteiten toevoegen aan de live-omgeving in plaats van wachten op een groot livegang-moment. Daarmee maken we het mogelijk dat je zelf een feature kan reviewen, eigenhandig testen, accorderen en geautomatiseerd live kan zetten. Dat werkt alleen als je de diverse testfasen automatiseert, van domein tot gedrag en techniek.

Het voordeel: alleen ontwikkelen wat nodig is om waarde te leveren aan eindgebruikers. Dus niet bouwen wat de developer vet vindt, maar ontwikkelen wat een eindgebruiker nodig heeft om effectief een doel te bereiken. En als kers op de taart daar als klant maximale controle en flexibiliteit in hebben.