Mocken van dependencies
Unit testen wordt vaak lastiger wanneer je code interageert met "de buitenwereld": filesystemen, databanken, invoer van de gebruiker, uitvoer naar de terminal, externe servers,...
Om deze reden wordt vaak gebruik gemaakt van "mocks": waarden die de plaats innemen van onderdelen die het moeilijk maken om unit testen te schrijven. Deze leveren vooraf vastgelegde data af eerder dan de echte handelingen uit te voeren. Achteraf kunnen we ook controleren dat deze gebruikt zijn zoals verwacht. Dit past binnen het black box principe dat gehanteerd wordt voor unit testen. Jest bevat ingebouwde functionaliteit voor het maken van mocks.
Hieronder bekijken we enkele voorbeelden:
Mocken van het Request en de Response
Het is mogelijk de route handlers af te zonderen. Dat levert een meer geïsoleerde unit test op. In plaats van een Request te doen via supertest
, kan je een object gebruiken. Dit vereist wel dat je je code wat anders organiseert. Meerbepaald: je moet de geteste functies benoemen en achteraf via app.get
en dergelijke registreren.
Nu is het dus mogelijk dezelfde code uit te voeren zoals je een gewone functie oproept, door zelf een Request
en een Response
als argument te geven:
Dit voorbeeld bevat eigenlijk twee types van mocks. De request en de response zelf zijn mocks, in de zin dat het geen echte request en response zijn. Ze hebben ook niet het juiste type en, om niet verdwaald te raken in de details van het typesysteem, hebben we ze gewoon als any
beschouwd.
Voor res hebben we twee onderdelen voorzien, namelijk send
en status
. Dit zijn de mocks die typisch bedoeld worden als het over Jest gaat. Het zijn niet gewoon plaatsvervangende objecten, ze zijn echt van het type Mock
, een speciaal soort functie. In het geval van send maakt het niet echt uit wat deze functie doet. In het geval van status
moet de functie in kwestie het object (dus res
) teruggeven waar ze deel van uitmaakt, zodat de volgende call de gemockte versie van send
gebruikt.
De expect
s op het einde staan dan toe om na te gaan dat de verwachte stappen zijn uitgevoerd. Het resultaat van deze functies was niet belangrijk, maar we willen wel weten dat ze op een bepaalde manier zijn uitgevoerd, d.w.z. dat de code gedaan heeft wat we dachten dat ze zou doen.
Mocken van het bestandensysteem
Stel, je hebt een Express-route die informatie leest van een bestand. Normaal gesproken zou je de fs
module van Node.js gebruiken om het bestand te lezen. Voor testdoeleinden kun je echter de fs
module mocken om consistente en gecontroleerde testresultaten te garanderen.
In een Jest-test kunnen we zorgen dat de oproep van fs.readFile
vervangen wordt:
In dit voorbeeld gebruiken we spyOn
om een functie uit een bestaande module te vervangen door een mock. Deze mock krijgt een implementatie die compatibel is (wat betreft de signatuur) met de oorspronkelijke fs.readFile
. Hier is vrij veel gebruik gemaakt van any
, omdat één waarde vervangen door een totaal ander type waarde niet makkelijk uit te drukken is.
Deze code maakt uitvoerig gebruik van mocks om de principes te laten zien. In de praktijk zou je eerder supertest
gebruiken, enkel fs.readFile
mocken en dan de uitvoer testen.
Deze techniek, waarbij je tijdens de uitvoering stukken van een module overschrijft, heet monkey patching. Dit is vaak een snelle manier om mocks te installeren. Een alternatieve, explicietere manier is dependency injection.
Neveneffecten vermijden
Om te vermijden dat andere operaties die fs.readFile
nodig hebben niet fout lopen, moeten we zorgen dat de mock enkel in deze testfunctie gebruikt wordt. Daarom voegen we in de testfile deze regel toe:
Als we dit buiten de describe
-blokken doen, gebeurt dit na elke test.
Mocken van databasetoegang
Hier gebruiken we een vorm van setter-based dependency injection. We zorgen dat de waarde die we anders rechtstreeks zouden gebruiken op voorhand kan worden ingesteld.
Dezelfde techniek kan toegepast worden voor andere databases dan MongoDB.
In de applicatie hebben we een handler die hier gebruik van maakt:
We gebruiken mockResolvedValue
om de teruggegeven waarde in een Promise
te plaatsen, want de applicatie verwacht dat (er staat immers const data = await ...
).
Mocken van een extern request
We gebruiken fetch
om requests op externe services te doen. Omdat dit iets is dat je vaak wil mocken (om te vermijden dat netwerkstoringen testen doen falen, om te vermijden dat je API-limieten bereikt,...) is hier speciale ondersteuning voor.
We installeren eerst fetch-mock-jest (als development dependency).
De clientcode:
De testcode:
Mocks maken het vaak gemakkelijker om snel een grote reeks unit tests uit te voeren, maar je wil jezelf er wel van verzekeren dat de echte, volledige applicatie ook werkt. Daarom gebruik je ze beter niet in end-to-end testen.
Monkey patching of dependency injection?
Monkey patching is vaak handig om "snelle" aanpassingen te doen. Zeker bij gebruik van packages, wanneer de broncode niet zo makkelijk toegankelijk is, kan dit handig werken. Het vereist dan ook geen aanpassing van de broncode. Het nadeel is dat het onbedoelde neveneffecten kan introduceren.
Dependency injection vereist dat je je code anders schrijft. Het zal duidelijker zijn wanneer bepaalde onderdelen vervangen (kunnen) worden, maar het brengt extra werk met zich mee.
Last updated