Unit Testing - Hoofdstuk 5

Ingediend door Dirk Hornstra op 25-oct-2021 21:19

We gaan aan de slag met Mocks and Test fragility. Vorige keer hoofdstuk 1 behandeld: link, daarna hoofdstuk 2: link, hoofdstuk 3: link, hoofdstuk 4: link, nu is het tijd voor hoofdstuk 5.

De Timo-samenvatting

Mocks zijn output gericht, stubs zijn gericht op input. Zorg dat je interne functionaliteit afgeschermd is met private/internal, zodat alleen de zaken die er echt toe doen / uitgevoerd/aangeroepen worden in een test-suite getest worden. Zorg met encapsulation dat je geen "domme data-objecten" hebt, maar data-objecten die hun eigen functionaliteit hebben en daardoor centraal te beheren/refactoren zijn.

 

Met "mocken" van code, sommige developers zijn lovend, anderen zijn radicaal tegen, want ze zeggen dat het je code een stuk "fragieler" maakt. De waarheid ligt (zoals vaak) in het midden.

Een test-double kunnen we onderverdelen naar "mocks" en "stubs".  Gerard Meszaros geeft aan dat er 5 verschillende types zijn, maar die zijn te groeperen naar de mock en stub. Een mock bevat mock en spy, de stub bevat de stub, dummy en fake.

  • Mocks zorgen voor uitgaande interaties. Het boek geeft het voorbeeld van een SMTP-server, om een mail te versturen (uitgaand), wat een effect in de SMTP-server genereert, zou een mock object kunnen doen dat de mail verzonden wordt.
  • Stubs zorgen voor inkomende interacties. Het boek geeft het voorbeeld van data opvragen uit een database.

 

Het boek geeft de uitleg wat de verschillen zijn:

  • Een spy is een mock die handmatig gemaakt is (dus waarbij geen framework gebruikt wordt). Ook wel "handwritten mock" genoemd.
  • Mocks worden met een framework opgebouwd.
  • Dummy: een hard-coded waarde die teruggegeven wordt.
  • Stub: een dependency die op basis van verschillende scenario's verschillende waardes kan retourneren.
  • Fake: is bijna gelijk aan een stub, maar is meestal een stuk code voor iets wat in het echt nog gebouwd moet worden.


Er volgt nog een korte uitleg dat het framework Moq een class Mock heeft, maar dat is de tool die gebruikt wordt, de "echte" mock is het object wat aangemaakt wordt.

Hierna wordt uitgelegd dat je de uitgaande acties moet valideren, niet de inkomende. Je valideert de mock en niet de stub. De stub levert je bijvoorbeeld 5 records aan, dit moet je niet in je code gaan valideren. De acties op die records en het resultaat daarvan, dat moet gecontroleerd worden.

Dit komt overeen met het CQS (comand query seperation) principe, een query moet een actie uitvoeren of data opvragen, niet beide uitvoeren.

Het boek geeft aan dat productiecode in 2 dimensies ingedeeld kan worden:

  • een publieke of private API (application programming interface)
  • observable gedrag tegenover details van implementatie


Het eerste deel is makkelijk te doen, door je class of methodes internal of private te maken.
Als code "observable gedrag" moet hebben, maak je een operatie (functie) beschikbaar die ervoor zorgt dat de client (aanroepende code) zijn doel kan bereiken. Deze functie voert een berekening uit, heeft een bepaald effect of heeft beide.

Of er moet een "state" beschikbaar gesteld worden waarmee de client zijn doel kan bereiken. State is de huidige staat van het systeem.

Een "nette" API zorgt dat de implementatie-details private/afgeschermd zijn en dat alleen de noodzakelijke delen publiek beschikbaar zijn. Maar vaak "sijpelt" er wat interne functionaliteit door naar de buitenwereld. Het boek geeft een voorbeeld van een User-class die een Name property heeft en een NormalizeName-functie, beide zijn extern aan te roepen. Maar NormalizeName moet alleen intern in het "set"-event van de Name-property aangeroepen worden. Als het aantal acties wat je moet uitvoeren op een class meer dan 1 acties is om iets te bereiken, dan is dat een "code-smell" dat er informatie lekt.

Met "encapsulation" zorg je dat code en acties centraal bij elkaar staan. Het boek noemt ook het "tell-don't-ask" principe van Martin Fowler: link. Maak niet een object aan die alleen als "container voor data" dient en ga in andere classes je eigen functionaliteit bouwen, maar zorg dat die functies in jouw data-object zitten.

  • Verbergen van implementatie details: lager risico op bewuste of onbewuste foutieve data-invoer
  • Bundelen van data en functies: zorg dat aangemaakte objecten niet afwijken van de ingestelde grenzen


We zien een plaatje van een hexagon, een 6-hoekig raster. Dat zijn de "application services". Daarbinnen zit een rondje, het "domain" met de business-logica. Je kunt het volgens mij ook iets anders weergeven, maar het steunt op de hexagonal architecture van Alistair Cockburn. Om het zo te tonen, dat heeft hij gedaan met de volgende beweegredenen:

  • een duidelijke scheiding tussen het domein (waar alle regels zitten, de "intelligentie") en de application services dienen alleen om de input en output te verbinden en niet om daar zelf logica op uit te voeren.
  • classes binnen de domein-laag moeten alleen met elkaar data uitwisselen en niet afhankelijk zijn van andere classes buiten het domein (omdat er dan via de application services communicatie moet plaatsvinden).
  • externe diensten kunnen nooit rechtstreeks verbinding maken met je domeinlaag, dit moet altijd via de application services.


Er zijn 2 verschillende soorten communicatie, intra-system en inter-system. Intra is binnen de classes in de applicatie, Inter is communicatie met externe diensten (SMTP service, externe API). Mocks kunnen prima gebruikt worden voor de inter-communicatie (je kent de interne implementatie van die externe dienst niet), maar mocks voor intra-communicatie is wel gevoelig voor refactoring e.d.

We komen nog even terug bij de London School en de Classical School. De auteur gaf al aan een voorkeur te hebben voor de classical school. Doordat bij de London School mocks voor bijna alles gebruikt, worden vaak implementatie-details meegenomen in de testen. Een out-of-process item kan gemockt worden (een database-connectie, een SMTP server), zodat ook het testen binnen niet al te lange tijd uit te voeren zijn (als elke keer deze objecten aangemaakt moeten worden, kan dit je testen (extreeem) traag maken.