Naar de inhoud
maarten.
Alle posts

Unit-, integratie- en end-to-endtests vergelijken

Maarten Soetens 12 min lezen

Unit-, integratie- en end-to-endtests onderzoeken software op verschillende niveaus, en leggen daardoor ook verschillende soorten fouten bloot. Lees hoe je per niveau een passende test kiest en welke rol testdekking, onderhoudbaarheid en testomgevingen spelen.

Testniveaus kiezen op basis van de fout die je wilt vinden

Een testniveau beschrijft welk deel van een systeem je tegelijk controleert. Bij een unit test staat één afgebakend onderdeel centraal, zoals een functie of klasse. Een integratietest onderzoekt of meerdere onderdelen correct samenwerken. Een end-to-endtest doorloopt een complete gebruikersroute via de interfaces en infrastructuur die daarbij horen. Het onderscheid gaat dus niet alleen over de omvang van de test, maar ook over de plek waar een fout zichtbaar wordt.

Een rekenregel die een verkeerd bedrag oplevert, hoort meestal eerst thuis in een unit test. Een fout in de omzetting van dat bedrag naar een databaseveld vraagt om een integratietest. Als de gebruiker uiteindelijk een onjuist totaal op de pagina ziet, kan een end-to-endtest aantonen dat de fout door de hele keten heen komt. Eén defect kan op meerdere niveaus zichtbaar zijn, maar de test die het dichtst bij de oorzaak zit, geeft vaak de beste diagnose.

Kies daarom niet vooraf één niveau als oplossing voor alle risico’s. Kijk naar de foutlocatie, de gevolgen van falen en de afhankelijkheden die nodig zijn om het gedrag waar te nemen. Die afweging bepaalt waar een test het meeste inzicht geeft en voorkomt dat een omvangrijke systeemtest wordt ingezet voor een eenvoudige regel.

Unit tests vinden fouten in logica en randgevallen

Een unit test controleert een klein, herkenbaar onderdeel van de code onder gecontroleerde omstandigheden. Dat kan een functie zijn die invoer valideert, een klasse die een statusovergang bepaalt of een module die een berekening uitvoert. Omdat de test weinig afhankelijkheden nodig heeft, is de oorzaak van een mislukking meestal snel te lokaliseren. Unit tests zijn vooral geschikt voor voorwaarden, grenswaarden, foutafhandeling en combinaties van invoer die in normaal gebruik niet vaak voorkomen.

Praktisch betekent dit dat je niet alleen het gebruikelijke pad test. Controleer bijvoorbeeld lege invoer, een waarde precies op de grens, ongeldige combinaties en situaties waarin een afhankelijkheid een fout teruggeeft. Gebruik een mock of stub wanneer een externe dienst, klok of database het gedrag anders onvoorspelbaar maakt. Zo test je de eigen logica zonder dat een netwerkstoring wordt aangezien voor een defect in de functie.

Te veel mocks kunnen de test echter losmaken van de echte werking van het systeem. Een test kan dan slagen terwijl de implementatie niet meer overeenkomt met het gedrag van de dependency. Test daarom concrete logica direct, en gebruik simulaties doelgericht voor grenzen die je niet in een unit test wilt betrekken. Een wijziging in interne structuur hoeft geen testwijziging te vereisen zolang het waarneembare gedrag gelijk blijft.

Integratietests controleren contracten tussen onderdelen

Integratietests onderzoeken of componenten op de afgesproken manier met elkaar communiceren. Denk aan applicatiecode die gegevens opslaat in een database, een API die een bericht naar een queue publiceert of een client die een externe dienst aanroept. De test verifieert niet alleen losse logica, maar ook praktische details zoals veldnamen, serialisatie, transacties, foutcodes en de interpretatie van lege of ontbrekende waarden.

Een veelvoorkomende fout ontstaat doordat twee onderdelen afzonderlijk correct lijken, maar verschillende aannames hanteren. Een applicatie verwacht bijvoorbeeld een datum in één formaat, terwijl de databaseadapter een ander formaat opslaat. Ook migraties, configuratie van verbindingen en wijzigingen in API-contracten kunnen pas zichtbaar worden wanneer de betrokken onderdelen samen draaien. Een integratietest met een echte database of een representatieve service kan zulke afwijkingen aantonen die mocks niet zien.

De keuze tussen echte dependencies, containers en nagebootste diensten hangt af van wat je wilt controleren. Gebruik een echte database wanneer querygedrag, constraints of migraties relevant zijn; simuleer een externe provider wanneer de test anders afhankelijk wordt van diens beschikbaarheid. Houd de testgrens bewust klein: hoe meer systemen tegelijk worden betrokken, hoe lastiger het wordt om een fout aan één contract toe te wijzen. Test vooral de verbindingen waar incompatibiliteit operationele gevolgen heeft.

End-to-endtests toetsen complete gebruikersroutes

Een end-to-endtest voert een scenario uit door een werkende applicatie, vanaf een gebruikersactie tot en met het zichtbare resultaat. Een test kan bijvoorbeeld een formulier invullen, een opdracht indienen en controleren of de resulterende status in de interface verschijnt. Daarmee worden onderdelen gecombineerd die in unit- en integratietests afzonderlijk zijn getest: browsergedrag, applicatielogica, API’s, opslag en configuratie.

Dit niveau kan fouten vinden die alleen in de volledige keten ontstaan. Denk aan een knop die door een frontendwijziging geen verzoek meer verstuurt, een verkeerd gekoppelde route, een authenticatiecookie die ontbreekt of een achtergrondtaak die niet wordt uitgevoerd. Een end-to-endtest kan ook aantonen dat de applicatie technisch bereikbaar is, maar voor de gebruiker toch geen bruikbare uitkomst oplevert. De keerzijde is dat een mislukking meerdere mogelijke oorzaken heeft en daardoor meer onderzoek vraagt.

Beperk het aantal scenario’s tot belangrijke bedrijfsprocessen en controleer daarin resultaten die voor gebruikers betekenisvol zijn. Selectors die steunen op veranderlijke CSS-klassen, vaste wachttijden en gedeelde gebruikersaccounts maken tests kwetsbaar. Gebruik stabiele herkenningspunten, wacht op concrete toestanden en isoleer testdata waar dat kan. End-to-endtests vervangen geen tests van uitzonderingen of rekenregels: die zijn sneller en nauwkeuriger te controleren op lagere niveaus.

Testdekking zegt niet hoeveel fouten je daadwerkelijk vindt

Testdekking geeft aan welk deel van de code tijdens tests wordt uitgevoerd. Dat is nuttige informatie, maar een hoog percentage bewijst niet dat de belangrijkste uitkomsten zijn gecontroleerd. Een test kan een regel uitvoeren zonder te verifiëren of die regel het juiste resultaat oplevert. Omgekeerd kan een relatief kleine set zorgvuldig gekozen tests veel relevante risico’s afdekken, vooral wanneer die op kritieke beslislogica en foutpaden is gericht.

Maak onderscheid tussen regeldekking, takdekking en gedrag. Regeldekking laat zien welke regels zijn aangeraakt; takdekking kijkt ook naar alternatieve uitkomsten van voorwaarden. Geen van beide vertelt zelfstandig of assertions betekenisvol zijn. Een test die alleen controleert dat een functie niet crasht, kan bijvoorbeeld slagen terwijl de functie een onjuist bedrag teruggeeft. Combineer dekking daarom met review van testgevallen, analyse van gewijzigde code en aandacht voor risicovolle domeinregels.

Een dekkingstarget kan teams helpen om ongeteste code zichtbaar te maken, maar wordt contraproductief wanneer het percentage het doel op zichzelf wordt. Dan ontstaan tests die vooral regels uitvoeren of bestaande implementatiedetails vastleggen. Gebruik rapportages om blinde vlekken te onderzoeken en volg veranderingen in dekking per onderdeel. Een daling rond nieuwe betalings-, autorisatie- of validatielogica verdient meer aandacht dan hetzelfde percentageverlies in code zonder grote gevolgen bij falen.

Onderhoudbare tests controleren gedrag zonder de implementatie vast te zetten

Tests zijn zelf software en vragen dus onderhoud. Een test is bruikbaar wanneer een ontwikkelaar begrijpt welk gedrag wordt gecontroleerd, waarom een mislukking relevant is en waar de oorzaak waarschijnlijk ligt. Duidelijke namen, kleine scenario’s en assertions die één betekenisvolle uitkomst controleren, maken fouten makkelijker te diagnosticeren. Hergebruik van testhulpmiddelen kan duplicatie beperken, maar een algemene testlaag met veel impliciete instellingen kan juist verhullen wat een scenario werkelijk doet.

De meest onderhoudbare tests richten zich op contracten en waarneembaar gedrag, niet op toevallige details van de implementatie. Als een test vastlegt welke interne methode eerst wordt aangeroepen, kan een veilige refactor veel tests breken zonder dat gebruikersgedrag is veranderd. Soms is interactiegedrag wel relevant, bijvoorbeeld wanneer een bericht precies één keer moet worden verstuurd. Leg die verwachting dan expliciet vast als onderdeel van het contract, in plaats van iedere interne stap te verifiëren.

Flaky tests zijn tests die zonder relevante codewijziging soms slagen en soms falen. Ze ontstaan vaak door gedeelde toestand, parallelle uitvoering, afhankelijkheid van tijd of willekeurige testdata. Een tijdelijke retry kan het symptoom verbergen, maar maakt de suite minder betrouwbaar als de oorzaak blijft bestaan. Registreer bij falen voldoende context, isoleer data en vervang vaste slaapintervallen door wachten op een concrete gebeurtenis. Verwijder of herstel tests die geen betrouwbaar signaal meer geven.

Testomgevingen bepalen hoe betrouwbaar resultaten zijn

Een test is alleen representatief als de omgeving de eigenschappen bevat die voor het gedrag van belang zijn. Een lokale unit test heeft doorgaans weinig infrastructuur nodig. Integratietests vragen vaak om een database, message broker of testserver met passende configuratie. End-to-endtests kunnen daarnaast een browser, authenticatie, achtergrondprocessen en een samenhangende set testdata vereisen. Verschillen tussen ontwikkel-, test- en productieconfiguratie kunnen fouten opleveren die in een geïsoleerde lokale omgeving niet zichtbaar worden.

Omgevingen moeten reproduceerbaar worden ingericht. Leg versies van databases en diensten vast, voer schemawijzigingen op dezelfde manier uit als in de applicatie en maak configuratie expliciet. Als tests afhankelijk zijn van handmatig aangemaakte accounts of achtergebleven records, wordt de uitkomst gevoelig voor de volgorde waarin tests draaien. Automatiseer het aanmaken en opruimen van data waar dat haalbaar is, en gebruik afzonderlijke gegevens per scenario wanneer tests parallel worden uitgevoerd.

Een testomgeving hoeft niet altijd een volledige kopie van productie te zijn. Voor integratietests is een geïsoleerde database vaak nuttiger dan een gedeeld testexemplaar, omdat falen dan beter reproduceerbaar is. Voor end-to-endtests kunnen gesimuleerde externe diensten de controle vergroten, maar ze tonen niet aan dat de echte provider hetzelfde contract volgt. Maak daarom per dependency een bewuste keuze tussen isolatie en representativiteit, en documenteer welke risico’s daarmee buiten beeld blijven.

Verdeel testscenario’s op basis van snelheid, risico en diagnose

Een bruikbare testsuite combineert niveaus in plaats van één niveau overal voor in te zetten. Unit tests zijn doorgaans geschikt voor veel combinaties van logica, omdat ze snel draaien en gericht falen. Integratietests zijn waardevol op grenzen waar data, protocollen en configuratie samenkomen. End-to-endtests controleren een kleiner aantal routes met grote gebruikers- of bedrijfsimpact. Deze verdeling is geen vaste verhouding: een systeem met complexe rekenregels vraagt mogelijk veel unit tests, terwijl een applicatie met kritieke koppelingen extra integratiecontrole nodig heeft.

Beoordeel een scenario op drie vragen: hoe groot is de schade als dit gedrag faalt, op welk niveau is de fout het best te isoleren en welke omgeving is nodig om de werking geloofwaardig te controleren? Een ingewikkelde validatieregel is meestal goedkoop en precies te testen als unit. Een databaseconstraint hoort thuis in een integratietest die de echte database-engine gebruikt. Een essentiële aanmeldroute kan daarnaast een end-to-endtest rechtvaardigen, ook als de onderliggende regels al lager zijn afgedekt.

Bij een defect is het nuttig te kijken welk niveau het eerder had kunnen signaleren. Ontdekt een productie-incident een fout in een eenvoudige berekening, voeg dan een gerichte unit test toe. Gaat het om een incompatibel API-contract, versterk dan de integratietests rond die grens. Een end-to-endtest toevoegen voor iedere gevonden fout maakt de suite traag en diagnostisch zwak. De gekozen test hoort niet alleen het incident te reproduceren, maar ook de foutklasse scherp af te bakenen.

Veelgestelde vragen

In welke volgorde voer je unit-, integratie- en end-to-endtests uit in een CI-pipeline?

Voer in een CI-pipeline eerst de snelle unit tests uit, daarna de integratietests en als laatste de end-to-endtests. Zo krijgt een ontwikkelaar snel feedback over eenvoudige fouten, terwijl uitgebreidere controles alsnog vóór een release kunnen plaatsvinden.

  • Laat unit tests bij voorkeur draaien bij elke commit of pull request.
  • Voer integratietests uit zodra de relevante applicatieonderdelen en services beschikbaar zijn.
  • Plan end-to-endtests na succesvolle lagere testlagen, eventueel parallel voor verschillende gebruikersroutes.

Voor kritieke routes kan een beperkte end-to-endset op elke pull request nodig zijn. Minder urgente, langere scenario’s kunnen in een nachtelijke run. Zorg dat een mislukte test duidelijk aangeeft welke wijziging of testlaag aandacht vraagt.

Wat is mutation testing en wanneer is het nuttig?

Mutation testing controleert of je tests fouten kunnen ontdekken door kleine, opzettelijke wijzigingen in de code aan te brengen. Een mutation kan bijvoorbeeld een vergelijking omdraaien of een berekening aanpassen. Als alle tests daarna nog slagen, kan dat betekenen dat een belangrijke uitkomst niet echt wordt gecontroleerd.

  • Gebruik mutation testing vooral bij risicovolle logica, zoals prijsberekeningen of autorisatie.
  • Bekijk welke mutaties overleven en voeg alleen tests toe die betekenisvol gedrag controleren.
  • Behandel een mutatiescore niet als doel op zichzelf: sommige mutaties zijn niet relevant of kunnen niet door tests worden waargenomen.

Omdat deze techniek extra werk en rekentijd vraagt, is ze meestal geen vervanging voor de normale testset. Ze is vooral een aanvullende manier om twijfel over de kwaliteit van assertions te onderzoeken.

Hoe voeg je tests toe aan een legacy-applicatie zonder alles eerst te refactoren?

Voeg bij een legacy-applicatie eerst tests toe rond bestaand gedrag dat je wilt behouden, zonder vooraf een grote refactor uit te voeren. Zo leg je vast wat de code nu doet en verklein je het risico dat een latere wijziging ongemerkt belangrijk gedrag verandert.

  • Kies een kleine, afgebakende wijziging of een fout die je wilt herstellen.
  • Maak waar mogelijk een test op het niveau waarop het bestaande gedrag betrouwbaar waarneembaar is.
  • Als code moeilijk te bereiken is, introduceer dan een kleine naad, zoals een vervangbare dependency, in plaats van de hele module te herschrijven.

Breid de tests geleidelijk uit wanneer je onderdelen aanpast. Een tijdelijke karakteriseringstest hoeft niet elke interne eigenschap vast te leggen; richt je op uitkomsten die gebruikers of andere onderdelen nodig hebben.

Hoe test je een integratie met een externe API zonder die API bij elke test aan te roepen?

Test een integratie met een externe API meestal met een nagebootste reactie voor de meeste tests, en controleer daarnaast periodiek of de echte API nog overeenkomt met het verwachte contract. Zo blijven tests snel en onafhankelijk van storingen of limieten bij de provider.

  • Controleer met nagebootste reacties hoe je applicatie succes, fouten en onverwachte antwoorden verwerkt.
  • Gebruik waar beschikbaar een sandbox om authenticatie, verzoeken en echte API-contracten te valideren.
  • Bewaar geen gevoelige live-antwoorden als testmateriaal; verwijder of vervang persoonsgegevens en geheimen.

Een mock toont niet aan dat de provider zich werkelijk aan het contract houdt. Plan daarom een aparte controle met de sandbox of een beperkte echte aanroep, en zorg dat een storing daar niet alle lokale tests blokkeert.

Mag je productiegegevens gebruiken in een testomgeving?

Gebruik productiegegevens niet rechtstreeks in een testomgeving, tenzij daar een aantoonbare noodzaak voor is en passende privacy- en beveiligingsmaatregelen gelden. Productiedata kan persoonsgegevens, bedrijfsgeheimen of informatie bevatten die buiten de oorspronkelijke context niet veilig is.

  • Kies waar mogelijk synthetische gegevens die de relevante gevallen nabootsen.
  • Als een representatieve dataset noodzakelijk is, anonimiseer of maskeer gegevens vóór gebruik en controleer of heridentificatie niet eenvoudig mogelijk is.
  • Beperk toegang, bewaartermijnen en kopieën, en verwijder data zodra die niet meer nodig is.

Let ook op gegevens in logs, screenshots en foutmeldingen: die kunnen ongemerkt dezelfde gevoelige informatie bevatten. Stem het gebruik van echte gegevens af op het privacybeleid en de toepasselijke wetgeving, en leg vast wie verantwoordelijk is voor de dataset.

Portret van Maarten

Maarten

Freelance developer in Nijmegen

Even kennismaken?

Vertel kort wat er speelt. Dan hoor je wat er kan, wat ik anders zou doen en waar AI bij jou wél en niet iets toevoegt. Vrijblijvend.

[email protected]
Het kantoor in Nijmegen
© 2026 maarten.online Sitemap Privacy Algemene voorwaarden
Het kantoor in Nijmegen

Maarten.

Freelance developer in Nijmegen. Liever direct contact? Dat kan ook.

Kennismaken

Laat je gegevens achter, dan kijken we of het klikt. Vrijblijvend en zonder verkooppraat.

Maarten

Stuur een bericht via WhatsApp

Hoi! Waar kan ik je mee helpen?

nu