Een kostbaar en vaak onderschat onderdeel van software ontwikkeling is het (geautomatiseerd) testen daarvan. Goed geautomatiseerd testen is een investering met een hoog rendement omdat regressiefouten bij wijzigingen voorkomen kunnen worden.Hiermee heeft de ontwikkelaar uiteindelijk meer tijd voor het opleveren van business value in plaats van het oplossen van bugs.
Er zijn verschillende lagen van automatische tests, elk met hun eigen eigenschappen en complexiteit. Daarnaast is een verscheidenheid aan test strategieën, tools en lagen. Correct begrip en inzicht in deze zaken is essentieel voor een gezond ontwikkelproces.
Soorten tests in het software ontwikkelproces
Zoals gezegd, zijn er diverse soorten en smaken tests. Veelal geautomatiseerd maar ook handmatige tests hebben een belangrijke plaats in het ontwikkelproces.
Unit-tests
Een unit-test is precies wat het woord ‘unit’ suggereert. Test één class/method of unit-of-behaviour waarbij alle dependencies gemocked (gesimuleerd) worden. In een unit-test wordt de programma logica gevalideerd tegen een voorspelbare set van (grens)waarden. Een perfecte set unit-tests raakt dus alle code binnen de methode. Als het niet mogelijk is om alle dependencies te mocken, moet overwogen worden of het class design wel correct is. Probeert de class of method te veel te doen en moet bepaalde functionaliteit misschien als dependency geïnjecteerd worden? Unit testen is dus niet alleen essentieel voor het valideren van correcte werking maar ook een hulpmiddel voor het valideren van het ontwerp van een applicatieonderdeel.
Het maken van kwalitatieve unit-tests wordt vaak onderschat. Beperkt tests tot de essentie en probeer testcases eenvoudig te houden. Hou er rekening mee dat grotere refactorings ook grote wijzigingen in unit-tests met zicht mee kunnen brengen.
Component tests
Bij component tests wordt de samenhang/samenwerking van onderdelen van een systeemcomponent getest. Dit soort test zijn met name bedoeld om technische samenhang zoals dependency injection, configuratie en middleware van een bepaalde service te valideren. Bij component tests kan gekozen worden om bijvoorbeeld infrastructure dependencies te mocken om bepaalde testscenario’s op te zetten of te valideren. Als dit om wat voor reden dan ook niet haalbaar of mogelijk is, kan de samenhang getest worden met integratietests.
Integratie tests
Zoals de naam al doet vermoeden, is een integratietest bedoeld om een keten van componenten te testen. Denk hierbij aan live dependencies zoals authenticatie, hosting, database, configuratie en geconsumeerde Api’s. Een pijnpunt bij integratietest is de herhaalbaarheid van het testen van mutaties. Een mutatie kan ervoor zorgen dat een tweede run van dezelfde test niet meer het gewenste resultaat geeft. In dit soort scenario’s moeten databases dus voor of na de test teruggedraaid worden wat complicaties kan geven, bijvoorbeeld in het geval van 3rd party Api’s. Databases in Docker containers met een specifieke dataset kunnen hiervoor een oplossing bieden. Het onderhoud van dergelijke tests kan zeer kostbaar blijken.
End-to-end tests
Als specifieke soort integratietest wordt bij end-to-end testen de gehele keten van applicatiecomponenten vanuit het oogpunt van de gebruiker getest. Deze tests worden doorgaans aan het einde van het ontwikkelproces uitgevoerd en dekken niet alleen de hele stack (van UI tot persistence) maar mogelijk ook meerdere functionele processen. Dergelijke tests zijn zeer kostbaar om te maken en te onderhouden, maar zijn nodig om vast te stellen of het product voldoet aan de verwachting van de consument.
UI/UX tests
User interface testing is een specialistische tak van testen waarbij een geautomatiseerd proces door de applicatie ‘klikt’ en elementen van de html output valideert om test-scenario’s te doorlopen. Het opzetten en onderhouden hiervan kan zeer tijdrovend zijn maar een goede set UI tests kan interessant zijn om bijvoorbeeld de werking van standaard componenten te valideren maar zeker ook om regressie-fouten in de UI te voorkomen. Hou er rekening mee dat elke ondersteunde browser vanwege verschillen in rendering zijn eigen (sub)set aan UI tests nodig zou kunnen hebben.
Functionele tests
Bij functionele testen gaan we na of het systeem dat gemaakt is voldoet aan wat het moet doen. Ofwel, zijn de business rules goed gemodelleerd en kan de gebruiker doen wat verwacht is.
Bij dit soort functionele tests kan het interessant zijn om tooling zoals SpecFlow te gebruiken. Hiermee kan een niet-developer zoals aan analist of zelfs een business specialist, in een niet technische taal testcases beschrijven die ‘onder water’ door een ontwikkelaar uitgewerkt worden. Dergelijke tools bevorderen de samenwerking tussen testers, ontwikkelaars en analisten doordat er een gemeenschappelijke taal wordt gesproken die iedereen begrijpt. Dit levert uiteindelijk weer een beter eindproduct op waarbij de specificaties onderdeel zijn van de code.
In een ideale wereld stelt een analist met de product owner acceptatiecriteria op in een formele meta-taal zoals Gherkin waarmee de ontwikkelaar niet alleen de functionaliteit kan bouwen en testen maar ook de documentatie op orde is voor toekomstige referentie.
Load testing en performance testing
Met load testing en performance testing stel je vast hoe onderdelen van, of het systeem als geheel onder druk blijven werken. Valideer niet-functionele eisen (responsetijd, aantal gelijktijdige verbindingen, etc.) of ontdek performance bottlenecks veroorzaakt door zwakke schakels in het systeem. Een systeem kan prima presteren met één gebruiker maar concurrency-issues kunnen leiden tot verzadiging van resources bij meerdere gelijktijdige gebruikers en zo inzicht geven in zwakke plekken in de code of benodigde schaling van resources.
Statische code analyse
Code analyse is meer bedoeld als kwaliteitscontrole voor de code op zich dan validatie van functionele specificaties. Deze tools kunnen mogelijke bugs, anti-patterns, conventie overtredingen, over-complexiteit, etc. herkennen en suggesties geven voor verbeteringen van de code. Dit kan dwingend zijn als onderdeel van de CI/CD pipeline zoals bijvoorbeeld SonarQube of als extensie in de lokale ontwikkelomgeving zoals SonarLint of ReSharper.
Security tests
Statische (SAST) en dynamische (DAST) security tests zijn specifiek bedoeld om mogelijke veiligheidslekken tijdens het ontwikkelproces op te sporen. Denk hierbij aan het herkennen van bekende exploits zoals SQL injection en buffer overflow, issues met (nuget) packages en zelfs bewuste exploits door interne medewerkers.
Het verschil tussen SAST en DAST is te vinden in de manier van testen. SAST analyseert de code statisch en DAST test de run-time als black-box.
Handmatig of verkennend testen
Hoewel het zinvol is om zoveel mogelijk tests te automatiseren is er altijd een plaats voor handmatig testen. Denk aan situaties waarbij een nieuwe feature geïntroduceerd wordt, een nieuw UI element of waar geautomatiseerd testen te kostbaar of te complex is.
Verkennend testen wordt bij voorkeur uitgevoerd door specialisten die er een sport van maken om die bijzondere edge-cases te vinden waar de ontwikkelaar net niet aan gedacht heeft.
AI ondersteuning bij testen
Ook in software testen heeft kunstmatige intelligentie stappen gemaakt. Belangrijkste taken voor AI in het ondersteunen van geautomatiseerd testen zijn bijvoorbeeld het omzetten van natuurlijke taal naar testcases, herkennen van trends in gebruikersinteractie en onregelmatigheden in visuele componenten van een applicatie en herkennen en repareren van kapotte tests. Het mag duidelijk zijn dat AI ook op dit gebied de komende jaren meer invloed zal hebben.
Test Pyramid vs Trophy
Mike Cohn introduceerde in 2010 reeds de test-piramide en deze werd nog eens aangehaald door Martin Fowler. De filosofie van deze piramide is dat de breedte staat voor het aantal tests en de hoogte voor de complexiteit, run-time of kosten. Volgens dit model zijn er dus relatief veel unit-tests en relatief weinig integratie-tests/end-to-end tests aangezien de kosten hiervan veel hoger zijn.
source: andersenlab.de
De ‘Test Trophy’ werd geïntroduceerd door Kent C. Dodds en bekijkt testen meer vanuit het UI/UX perspectief. Zijn kritiek is dat een beperkte set integratietests niet bevorderlijk is voor het vertrouwen van de eindgebruiker en dat unit-tests lang niet altijd nuttig of correct zijn. De Test Trophy heeft dus meer nadruk op integratietesten en minder op unit-testen. Daarnaast vinden we in dit model ook de statische tests terug wat tegenwoordig niet meer weg te denken is uit het DevOps proces.
Hoewel dergelijke modellen natuurlijk illustratief zijn, hangt de mix van test-lagen af van wat voor systeem er ontwikkeld wordt. Is er veel complexe business logica, dan is het misschien goed om meer te investeren in unit-test en component tests. Is het systeem afhankelijk van andere Api’s buiten de invloed van de ontwikkelaars, zijn integratietests weer erg belangrijk, etc…
Test strategie
Een test strategie omvat het geheel van tools, methoden, analyses en processen rondom de kwaliteit van het systeem gedurende de hele levenscyclus. De strategie motiveert afwegingen tussen kosten en baten van gemaakte keuzes, risicoanalyses en is afgestemd met ontwikkelaars maar zeker ook met de business stakeholders.
Hou er bij het opstellen van een strategie rekening mee dat testen een kostbare investering is en je dus beperkt tot wat nodig is. Testen om te testen leidt tot onnodige kosten en onnodige doorlooptijd bij wijzigingen.
Tot slot
Software testen is een complexe en specialistische bezigheid. Er zijn veel lagen in het testproces met elk hun eigen aandachtsgebied, (functioneel, performance, security, etc.) met elk eigen tools en processen. Denk goed na bij het ontwikkelen van een nieuw systeem over wat getest moet worden en hoe intensief. Stel een test strategie op voor de gehele levenscyclus van een systeem en hou deze actueel op basis van nieuwe inzichten, tools en technieken.
Over de auteur:
Edo de Jongh springt in zijn vrije tijd regelmatig uit vliegtuigen. In zijn werk gaat het er meestal wat rustiger aan toe. In dit artikel deelt Edo zijn inzichten op het gebied van geautomatiseerd testen.
Edo de Jongh
Lead developer Bergler Software Solutions