Unit Testing - Hoofdstuk 4

Ingediend door Dirk Hornstra op 27-sep-2021 22:11

We gaan aan de slag met The 4 pillars of a good unit test. Vorige keer hoofdstuk 1 behandeld: link, daarna hoofdstuk 2: link, hoofdstuk 3: link, nu is het tijd voor hoofdstuk 4.

De Timo-samenvatting

Zorg dat je testen je beschermen tegen regressie (code wordt aangepast, er ontstaan bugs, worden deze door je testen gevonden), weerstand hebben tegen refactoring (als code aangepast wordt en nog steeds goed werkt, maar de test zegt "NIET OK", dan is de test-code niet robuust genoeg), testen moeten snel zijn en goed onderhoudbaar zijn. Het maximaliseren van die eigenschappen kan alleen bij de onderhoudbaarheid en de weerstand tegen refactoring. Bescherming tegen regressie en zorgen dat je testen snel zijn, die 2 schijnen elkaar uit te sluiten, dus daar zul je een goede middenweg in moeten vinden. Bouw je test alsof je voor een black box een test maakt (dus alleen specs en eisen zijn bekend, niet de interne werking), maar beoordeel je test wel alsof het een white box test is.

 

Een goede test-suite bestaat uit het feit dat het een integraal onderdeel van de development cycle is. Niet alleen ontwikkelen, maar ook testen. Tevens focussen de testen zich alleen op de belangrijkste delen van de code. En het levert je maximaal voordeel op met minimale inspanningen.

Om dat laatste punt in praktijk te kunnen brengen moet je dus kunnen zien wat voor test waardevol is (en welke testen weinig opleveren en dus overgeslagen kunnen worden).

Het kunnen beoordelen en schrijven van een waardevolle test zijn 2 verschillende dingen. In dit hoofdstuk wordt eerst naar het beoordelen gekeken.

De 4 pilaren van een goede test-suite:

  • bescherming tegen regressie
  • weerstand tegen refactoring
  • snelle feedback
  • onderhoudbaarheid

 

Regressie is een software bug, bijvoorbeeld na aanpassen van code voor een probleem of nieuwe functionaliteit werkt een bepaalde feature niet meer. Hoe meer code, hoe meer mogelijke bugs. Om te meten hoe goed tests scoren om dit te voorkomen zijn er deze aspecten:

  • de hoeveelheid code die uitgevoerd wordt tijdens de test
  • de complexiteit van de code
  • hoe belangrijk de code is die getest wordt (bijvoorbeeld business-rules, algoritmes)

Hierbij moeten ook externe bibliotheken die gebruikt worden mee getest worden.

Weerstand tegen refactoring betekent hoe lang je testcode groene seinen blijft geven terwijl de stuctuur van de code aangepast wordt (functionaliteit zou hetzelfde moeten blijven). Het kan dat deze een rood licht geeft, terwijl de code nog steeds goed werkt, een false positive dus. Deze testen zorgen dat de developer in een vroeg stadium ziet dat er iets fout gaat en dit kan oplossen en het geeft de dveloper de zekerheid dat er geen regressie in de code zal plaatsvinden.

False positives ondermijnen deze zaken, geen tijd in het uitzoeken willen steken, foutmeldingen negeren en alle voordeel is weg. En uiteindelijk is het vertrouwen in de test-suite weg.

Wat veroorzaak "false positives"? Vaak zit de testcode teveel gekoppeld aan de implementatiecode. Dit moet zoveel mogelijk ontkoppeld zijn, de test moet wat met het eindresultaat doen, meer niet.

Het boek geeft een voorbeeld van een HTML-renderer die uit sub-renderers bestaat. Een test valideert die sub-renderers. Maar dan duik je dus al in de implementatie van de class die je test. Je moet de output valideren: de HTML die opgebouwd wordt.

Het maximaliseren van de correctheid van een test

Het boek toont een tabel met testuitkomsten. Zo heb je dat de test OK zegt, functionaliteit correct: helemaal goed (een true negative). En je hebt de test die faalt, maar waarbij de functionaliteit ook kapot uit: helemaal goed (een true positieve). Maar er zijn nog 2 andere situaties, de test zegt OK, maar functionaliteit is kapot (een false negative - bescherming tegen regressie) en de test zegt NIET OK, maar de functionaliteit is correct (een false positive - weerstand tegen refactoring).
Dus hoe goed kan juist test de aanwezigheid van bugs aantonen en hoe goed kan de test aantonen dat er juist geen bugs zijn?

Initieel zijn false positives niet zo erg, je duikt de code in en lost het op. Maar hoe verder het project zich ontwikkelt, hoe meer effect de false positives krijgen.

Derde en vierde pilaar, snelle feedback en onderhoudbaarheid.

Hoe sneller tests uitgevoerd kunnen worden, hoe meer er gemaakt worden en hoe vaker ze uitgevoerd worden.
Het onderhoud wordt bepaald door hoe moeilijk het is om te snappen wat een test doet en hoe moeilijk het is om een test uit te voeren.

De waarde van een test bepaal je door de score per onderdeel maal de score van de andere onderdelen uit te voeren. Is een test niet onderhoudbaar en krijg een score 0, dan is het totaal een 0, want bij een wijziging (die altijd zal komen) zal de test waarschijnlijk vervallen of met veel moeite werkend gemaakt worden.

Het boek benoemt een aantal methodes;

End-to-end test
Deze test valideert meestal het systeem zoals de eindgebruiker het systeem ook ziet. Dus met gebruik van database, UI, externe applicaties. Hoewel de test goed beschermt tegen regressie-fouten en false positives is het vaak een trage test.

Triviale test
Deze test valideert "triviale zaken", wat meestal code is waar niet vaak bugs in zal ontstaan. Hoewel ze zeer snelle feedback geven is hier meestal de vraag: wat is het nut van deze test?

Brosse test
Deze test valideert snel en valide code, maar geeft vaak false positives terug.

Het lijkt er dus op dat de ideale test niet bestaat. De 3 componenten, bescherming tegen regressie, weerstand tegen refactoring en snelle feedback zijn zaken die elkaar uitsluiten. Voldoen aan twee van de drie lukt wel, maar de derde is dan vaak de beperkende factor.
Hoewel het verleidelijk is om te zeggen: we laten de eisen van alle 3 een beetje zakken, dan voldoet het wel, dat is dus niet de manier. Volgens het boek is de weerstand tegen refactoring niet valide, deze moet je behouden, dus je zult moeten schipperen tussen snelheid en de bescherming tegen regressie.

De Test pyramide

Boven: End-to-end, midden: Integration tests, onder: Unit tests, waarbij je dus de meeste unit tests hebt, iets minder integratie tests en uiteindelijk een aantal end-to-end tests.
Tests in het hoge deel geven de voorkeur aan bescherming tegen regressie, het onderste deel aan snelheid.

Black box testing

Er wordt getest wat de output is, hoe dit opgebouwd wordt, dat kan de test niet zien of beoordelen. Dus op basis van specificaties en eisen.

Slecht voor bescherming tegen regressie, maar goed bij de weerstand tegen refactoring.

White box testing

Hier weet je exact wat er intern gebeurt, de tests worden opgebouwd op basis van de broncode, niet vanuit specificaties en eisen. Deze manier is vaak wat diepgravender. Maar de tests zijn vaak "bros" omdat ze juist teveel gekoppeld zijn aan de geteste code.

Goed voor bescherming tegen regressie, maar juist slecht bij de weerstand tegen refactoring.

Tijdens het schrijven van tests kun je het beste de black box methode gebruiken. Bij het analyseren van de tests kun je het beste de white box methode gebruiken.