De zin en onzin van testautomatisering

2020-01-15

We leven in een wereld waarin nieuwe applicaties binnen de kortste keren online staan. Wanneer je iets nieuws bedacht hebt, heeft een ander dit al uitgewerkt. Je wil dan ook je ontwikkelomgeving zo inrichten dat je zo snel mogelijk live kunt. Je buildstraat is ingericht op continuous delivery en daar hoort een korte feedback loop op de aangepaste software bij. Testautomatisering zorgt ervoor dat je binnen enkele minuten weet of de software nog werkt, waar je met handmatige tests dagen of zelfs weken zoet bent.

Maar welke tests zijn de moeite waard om te automatiseren? Hoe hoog moet de dekking zijn? En hoeveel tijd mogen deze tests in beslag te nemen om continuous delivery na te kunnen streven?

Als uitgangspunt hiervoor heb ik de ‘traditionele’ testpiramide genomen.

Testpiramide


Unit tests

Onderaan in de testpiramide staan de unit tests. Deze unit tests runnen enkele seconden en geven snel feedback terug of gewijzigde code impact heeft op het gewenste resultaat. Unit tests zijn geschikt om de verschillende (corner)cases van een component te testen.

Adresveld

Neem bijvoorbeeld bovenstaand invoerveld voor een huisnummer. Verwacht is dat dit veld waardes bevat tussen de 1 en ongeveer 10.000. Een testcase bevat dus een valide waarde die hier binnen valt. Aangezien het een invoerveld is, wil je ook met niet-valide data testen, zoals 0, -1, 2.147.483.648, ‘null’ en ‘a’, om te controleren of de applicatie deze input goed afhandelt.

Unit tests staan het dichtst bij de code en dienen dan ook voldoende code af te dekken met tests, om aan te tonen dat wijzigingen in de code geen functionaliteit breekt. Het grootste aantal geautomatiseerde tests zit dan ook op dit niveau, omdat ze snel feedback geven.
SonarQube kan helpen met het controleren van de dekkingsgraad (in veel projecten is de standaard een dekking van ongeveer 80%) en een melding geven wanneer de code niet voldoende wordt afgedekt door tests.

Integration tests

In de middelste laag zitten de integration tests. Integration tests richten zich op het punt waar meerdere onderdelen van de applicatie samen komen en zijn vaak service gericht (REST/SOAP/etc.). Ze testen (deels) tegen live instanties, maar niet tegen de user interface (GUI). Integration tests duren langer dan unit tests, maar een stuk minder lang dan end-to-end tests, doordat er geen visuele tests worden uitgevoerd.

REST call

In bovenstaand voorbeeld wordt er een GET request gedaan naar de server en wordt er een response verwacht in de vorm van een XML bericht.

Integration tests kunnen worden geschreven met behulp van Cucumber. Acceptatiecriteria worden duidelijk geformuleerd in feature files (scenario’s, zie onderstaand voorbeeld). Hierdoor ontstaat er ‘living’ documentation, die voor alle betrokkenen (bijv. Scrum team en stakeholders) goed te begrijpen is (human-readable met functionele scenario’s).

Cucumber scenario


End-to-end tests

De bovenste laag bevat de end-to-end (E2E) tests. E2E tests en GUI tests zijn gericht op de flow door het systeem. Deze tests checken o.a. de user interface en de front-end van de applicatie (HTML/CSS/etc). Deze tests zijn in vergelijking tot de andere tests het traagst. Daarnaast zijn ze kwetsbaar (wijziging in een onderdeel van de applicatie of GUI zorgt voor falende tests) en vergen daardoor ook veel onderhoud. Deze tests kunnen echter wel heel waardevol zijn, aangezien de interactie van de test met de applicatie vergelijkbaar is met die van een (eind)gebruiker.

E2E en GUI tests kunnen worden geautomatiseerd met Cucumber en Selenium.
Selenium is een automation tool om web-based applicaties te testen. Selenium geeft support voor verschillende programmeertalen, waaronder Java en ondersteunt meerdere browsers (o.a. IE, Chrome, Firefox, Safari en Opera). Het maakt gebruik van locators (classname, id, enz.) uit de HTML broncode om tests te automatiseren. Voor tests met Selenium wordt vaak gebruik gemaakt van XPath (XML path). D.m.v. een XPath expressie is het mogelijk om snel elementen op een webpagina te vinden (bijv. "//input[@id='search']" voor het inputveld in een zoekformulier). Er zijn echter een aantal nadelen wanneer er gebruik wordt gemaakt van XPath:

Bij gebruik van absolute paden of lange relatieve paden (bijv. "//div/div/div/h1/b[contains(text(),'Dikgedrukte header')]") falen tests snel wanneer er een kleine HTML wijziging plaatsvindt.
Je dient in de code duidelijk aan te geven of je 1 of meerdere resultaten verwacht. Wanneer je 1 resultaat verwacht en het XPath heeft meerdere hits, dan zal je test falen en aangeven het resultaat niet gevonden te hebben.
Paden lijken soms veel op elkaar maar worden niet generiek opgezet, met als gevolg dat er veel code duplicatie ontstaat.

Om duplicatie te voorkomen in de uitwerking van de testscenario's is het aan te raden om gebruik te maken van het Page Object Model design pattern (bijv. wanneer je op meerdere webpagina's een filteroptie hebt, kun je de code hergebruiken).

Zoals hierboven al beschreven wordt kunnen deze tests erg traag en kwetsbaar worden. Beperk deze tests dan ook zoveel mogelijk tot waar het écht waarde geeft.


Pixel-perfect

Een andere mogelijkheid om de GUI te testen, die mijn voorkeur geeft boven Cucumber-Selenium tests, is met BackstopJS (of een soortgelijke visuele regressietesttool).  Deze tool maakt het mogelijk om snel en eenvoudig feedback te krijgen op wijzigingen op webpagina's en losse HTML componenten op een pagina.
Tijdens de eerste run wordt er een referentiebeeld aangemaakt en opgeslagen. Een volgende run slaat actuele beelden op en vergelijkt deze met de referentiebeelden, tot op de pixel nauwkeurig. De testrapportage geeft visueel de afwijkingen aan, waardoor wijzigingen snel zichtbaar worden. Mochten de wijzigingen gewenst zijn (bijv. een aanpassing in de GUI), dan kan het laatste beeld opgeslagen worden als nieuw referentiebeeld. Een voorbeeld uit BackstopJS:

BackstopJS voorbeeld


De omgekeerde testpiramide

In veel organisaties zien de geautomatiseerde tests eruit als een 'omgekeerde' piramide. Veel trage E2E en GUI tests en minder unit tests. End-to-end tests geven waardevol inzicht in de flow, maar zorgen er wel voor dat de livegang van functionaliteit geblokkeerd kan worden door de lange doorlooptijd.
Daarnaast zijn unit tests van belang om snel iedere variatie van een component te testen, dus wanneer je deze tests mist loop je kans dat er kwetsbaarheden in de applicatie ontstaan.
Wanneer je tests niet volgens een gestructureerd plan zijn opgesteld, is het gevaar dus dat de applicatie onvolledig of juist overcompleet wordt afgetest.


Het testwiel

Tegenwoordig zijn er verschillende variaties op de traditionele testpiramide. Veelal zijn er lagen toegevoegd, omdat niet alle soorten tests werden meegenomen. Daarnaast suggereert de piramide dat somige type tests belangrijker zijn dan andere, terwijl ze allen van belang zijn voor een goed draaiende applicatie.
Met deze gedachte is het testwiel bedacht.

Testwiel

Per blokje functionaliteit is het goed om te kijken welk type test er het best voor geschikt is. Tijd, efficiëntie en waarde van de tests zijn hierbij van belang.


Security tests

Wat uit het testwiel een belangrijk punt is om mee te nemen bij de automatisering zijn de security tests. Aangezien automatische tests meedraaien in je build pipeline (unit tests in de build-stap, integration en end-to-end tests in de test-stap) kunnen security tests op een relatief eenvoudige manier meedraaien in de pipeline als kwaliteit-stap. In deze stap kan een SonarQube scan draaien (om code kwaliteit te checken), maar ook tools als een OWASP dependency checker of een security scan.


Tot slot

Door je tests goed doordacht te automatiseren, krijg je snelle feedback over gewijzigde functionaliteit, worden mogelijke security issues sneller zichtbaar en krijgen betrokkenen inzicht in de gebouwde functionaliteit en acceptatiecriteria.
Sneller inzicht krijgen en een kortere time-to-market, dan is testautomatisering écht zinvol.


Bekijk alle posts van Thanja