Toon posts:

Mocking van pure functions.

Pagina: 1
Acties:

Vraag


  • Essie689
  • Registratie: Januari 2011
  • Laatst online: 23-09-2022
Ik had laatst een discussie mbt het mocken van classes in unit tests. Waarbij de andere persoon aan gaf echt altijd alle classes te mocken die hij met DI inladen in een class (welke hij wilde testen).

Nu zie ik wel degelijk het nut van het mocken van classes met functies die impure zijn. Maar mocken jullie ook pure functions als die in een ander object gebruikt worden? Of gebruiken jullie dan gewoon de classes/functies zelf?

Natuurlijk zijn die classes met alleen pure functies zelf wel gewoon volledig getest met unit tests.

Alle reacties


  • eamelink
  • Registratie: Juni 2001
  • Niet online

eamelink

Droptikkels

De drang om overal mocks te injecteren komt door een misconceptie over het begrip ‘testen in isolatie’.

Je aanpak is prima. Martin Fowler heeft er een aardig stukje over geschreven, en maakt onderscheid tussen mensen die van ‘solitary unit tests’ en mensen die van ‘social unit tests’ houden. Jij blijkbaar meer van social unit tests :)

Als je de Kent Beck manier (TDD) aanhoudt kom je ook op jouw resultaat uit. De pure functies die jij gebruikt zijn slechts een implementatie detail na refactoring en geen dingen die per se los getest moeten worden. Zoals je collega test; test hij implementatie-details in plaats van behavior.

Wat mij betreft ben je goed bezig; te meer omdat je er goed over nadenkt en de verschillen in strategie opmerkt :)

  • Hydra
  • Registratie: September 2000
  • Laatst online: 16:31
Heb je iets meer achtergrond? Als je een pure functie hebt die bijvoorbeeld de leeftijd van een Person object bepaalt (via de opgeslagen dob) is het natuurlijk vrij nutteloos een gewone data carrier / DTO class te gaan mocken. En dat heb ik ook echt nooit iemand zien doen. Dus ik begrijp denk ik niet wat je precies vraagt.

https://niels.nu


  • Essie689
  • Registratie: Januari 2011
  • Laatst online: 23-09-2022
eamelink schreef op zaterdag 30 januari 2021 @ 23:06:
De drang om overal mocks te injecteren komt door een misconceptie over het begrip ‘testen in isolatie’.

Je aanpak is prima. Martin Fowler heeft er een aardig stukje over geschreven, en maakt onderscheid tussen mensen die van ‘solitary unit tests’ en mensen die van ‘sociaal unit tests’ houden. Jij blijkbaar meer van social unit tests :)

Als je de Kent Beck manier aanhoudt kom je ook op jouw resultaat uit. De pure functies die jij gebruikt zijn slechts een implementatie detail na refactoring en geen dingen die per se los getest moeten worden. Zoals je collega test; test hij implementatie-details in plaats van behavior.

Wat mij betreft ben je goed bezig; te meer omdat je er goed over nadenkt en de verschillen in strategie opmerkt :)
Sorry afgelopen week was even wat drukker dan verwacht :). Ja er zijn echt verschillende soorten aanpak. Nu ben ik nog geen expert in testen, vandaar dat ik dus echt het zoeken ben naar wat het beste werkt.
Hydra schreef op zondag 31 januari 2021 @ 11:52:
Heb je iets meer achtergrond? Als je een pure functie hebt die bijvoorbeeld de leeftijd van een Person object bepaalt (via de opgeslagen dob) is het natuurlijk vrij nutteloos een gewone data carrier / DTO class te gaan mocken. En dat heb ik ook echt nooit iemand zien doen. Dus ik begrijp denk ik niet wat je precies vraagt.
Even een heel simpel voorbeeld:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Rectangle
{
  public function __constructor(
    public int $height,
    public int $width
  ) {}

  public function getHeight(): int
  {
    return $this->height;
  }

  public function getWidth(): int
  {
    return $this->width;
  }

  public function getSurface(): int
  {
    return $this->height * $this->width;
  }
}

Gezien het feit dat alles in deze class altijd dezelfde output geeft op basis van de constructor parameters ben ik van mening dat het niet nuttig is om deze class te mocken, zodra je een andere class aan het testen bent die gebruik maakt van deze rectangle class. Ik zou wel natuurlijk gewoon tests schrijven voor deze class. Dus ook al zou de Rectangle class een stuk complexer zijn, dan nog zou ik hem niet mocken zolang alle methods pure zijn.

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ID {
  public function __constructor(
    public string $id
  ) {
    // Validatie
  }

  public static function new(): self
  {
     return new self(md5(time()));
  }

  public function getId(): string
  {
     return $this->id;
  }
}

Natuurlijk is die md5(time()) een waardeloze manier om een id te generen, maar in dit geval zou ik de class wel mocken gezien het aanroep van de new() functie impure is.

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ID {
  public function __constructor(
    public string $id
  ) {
    // Validatie
  }

  public static function new(int $timeStamp): self
  {
     return new self(md5($timeStamp));
  }

  public function getId(): string
  {
     return $this->id;
  }
}

Door deze kleine wijziging is in eens alles in de class wel weer pure en zou ik de class niet langer mocken tijdens het testen van andere classes/functies die het bovenstaande object nodig hebben.

  • Hydra
  • Registratie: September 2000
  • Laatst online: 16:31
@Essie689 nee dat soort simpele data classes mock ik ook nooit.

https://niels.nu


  • n9iels
  • Registratie: November 2017
  • Niet online
Het gevaar van het niet mocken van classes die je bijv. via DI binnnen krijgt is dat je test false-negatives kan krijgen. Oftwel, als de bug in de class zit die jij via DI inlaad is de kans groot dat daarmee ook andere tests falen. Maar feitelijk ben je dan ook aan het integratie testen, dus ik denk dat de complexiteit hierbij ook een rol speelt. Als de class die je test erg complex is en de class die je inlaad ook kan het lastig worden.

Zelf probeer ik zoveel mogelijk isolatie toe te passen bij het testen. Maar ook enkel te mocken wat echt nodig is, waar mogelijk per test case.

  • Voutloos
  • Registratie: Januari 2002
  • Niet online
n9iels schreef op vrijdag 5 februari 2021 @ 21:04:
Het gevaar van het niet mocken van classes die je bijv. via DI binnnen krijgt is dat je test false-negatives kan krijgen. Oftwel, als de bug in de class zit die jij via DI inlaad is de kans groot dat daarmee ook andere tests falen.
Daar moet je gewoon op kunnen bouwen. Als die dependency meermaals niet stabiel of lekker getest blijkt, moet je de auteur daarop aanspreken.




Van mocken om het mocken word ik in ieder geval heel snel chagrijnig. Misschien vallen sommige integratietests achteraf nogal tegen omdat je de halve wereld mockt, maar laat dat alsjeblieft niet een argument zijn om bij nieuwe tests ook maar alles te mocken, puur ‘omdat die ene lelijke test ook zo werkt’.

Timestamp als parameter is ook wel lekker ook. Zo maar een voorbeeld, maar eentje waar ik me wat goed mee kan inleven. Wij hebben nogal wat functionaliteit aangaande tijden en planningen, en in onze testsuite kan de gebruikte ‘huidige tijd’ naar hartelust uitgekozen worden. :) Die parameter scheelt bij ons zeker tienduizenden regels aan mockgezeik.

{signature}


  • chielsen
  • Registratie: Oktober 2003
  • Laatst online: 29-03 23:50
In een unit test will je alleen de logica testen van de "unit". Als je niet mockt ben je dus eigenlijk een integratie test aan het uitvoeren. Dus ja, wel mocken.

  • Hydra
  • Registratie: September 2000
  • Laatst online: 16:31
chielsen schreef op zondag 7 februari 2021 @ 00:09:
In een unit test will je alleen de logica testen van de "unit". Als je niet mockt ben je dus eigenlijk een integratie test aan het uitvoeren. Dus ja, wel mocken.
Als het simpele data carrier classes zijn, is het complete overkill. Je moet dingen ook niet onnodig complex maken. Test code is ook risk.

https://niels.nu


  • Dricus
  • Registratie: Februari 2002
  • Laatst online: 11:36

Dricus

ils sont fous, ces tweakers

chielsen schreef op zondag 7 februari 2021 @ 00:09:
In een unit test will je alleen de logica testen van de "unit". Als je niet mockt ben je dus eigenlijk een integratie test aan het uitvoeren. Dus ja, wel mocken.
Er is een wijdverspreid misverstand over wat een "unit" precies is. Als je het boek Test Driven Development by Example van Kent Beck leest, dan zie je overduidelijk dat het niet gaat om het testen van een unit of structure (bijvoorbeeld een class of functie), maar om het testen van een unit of behaviour!



Daar komt ook nog eens een veelgeziene misinterpretatie van de i (isolated/independent) van de FIRST principles bij, namelijk dat je met unit tests een deel van je code (een unit of structure) in isolatie moet testen. Alleen daar gaat het helemaal niet om. Het gaat er bij isolated/independent om dat unit test onafhankelijk van elkaar moeten zijn. Het draaien van test1 mag geen invloed hebben op het al-dan-niet slagen van test2.

Één van de grote voordelen van unit testing in het algemeen en test driven development in het bijzonder is dat de tests een veilig vangnet vormen voor als je het design van je productiecode wilt refactoren. Het idee is dat je veilig kunt refactoren en dat je tests je helpen om zeker te weten dat je refactorings geen ongewenste bijeffecten hebben.

De mate waarin je vrij kunt refactoren is daarbij wel afhankelijk van de mate waarin je tests gekoppeld zijn aan het design van je productiecode. Hoe meer van dat soort "coupling", hoe meer unit tests refactoring moeilijk maken. Hoe losser die coupling, hoe makkelijker het refactoren wordt.

Het probleem met mocking is dat je daarmee een behoorlijk sterke dependency legt tussen unit tests en de structuur van je productiecode. Mocks maken het dus vaak (niet altijd) moeilijker om te refactoren, omdat je bij wijzigingen in je design ook elke keer je tests moet aanpassen.

De oplossing is om meer in units of behaviour te denken en om in je unit tests zo veel mogelijk gebruik te maken van stabiele interfaces binnen je productiecode. Het effect van deze manier van werken ten opzichte van de benadering van het testen van units of structure in isolatie is beter onderhoudbare code:
  • Refactoring is makkelijker
  • Je schrijft vaak minder unit tests, zonder op testdekking in te leveren
  • Unit tests lezen als executable specifications en helpen om de productiecode te begrijpen

Stel niet uit tot morgen wat je vandaag nog tot morgen kunt uitstellen...


  • Dricus
  • Registratie: Februari 2002
  • Laatst online: 11:36

Dricus

ils sont fous, ces tweakers

Aanvulling: Uiteraard legt Kent Beck zelf alles altijd beter en mooier uit dan wie dan ook ;) :
[...]

Programmer tests should be sensitive to behavior changes and insensitive to structure changes. If the program’s behavior is stable from an observer’s perspective, no tests should change.

[...]

Summary — programmer tests should:
  • Minimize programmer waiting.
  • Run reliably.
  • Predict deployability.
  • Respond to behavior changes.
  • Not respond to structure changes.
  • Be cheap to write.
  • Be cheap to read.
  • Be cheap to change.
That’s a difficult set of constraints to resolve. Some of them are contradictory. Figuring out which are more important and how best to satisfy the most important — that’s your job.
Mijn hele voorgaande epistel gaat in de kern over de vetgemaakte punten.

Stel niet uit tot morgen wat je vandaag nog tot morgen kunt uitstellen...


  • Dricus
  • Registratie: Februari 2002
  • Laatst online: 11:36

Dricus

ils sont fous, ces tweakers

En een laatste uitsmijter uit eerder gelinkte blog van Kent Beck:
I frequently see tests that assert, “Assert that this object sends this message to that object with these parameters and then sends this other message to that other object.” An assertion like this is basically the world’s clumsiest programming language syntax. If I care about the order of operations, I’ve designed the system wrong.
Overmatige mocking leidt tot unit tests die eruit zien zoals het vetgedrukte deel beschrijft. Als je unit tests er veelvuldig zo uit gaan zien, dan worden je unit tests een loden blok aan je been.

Stel niet uit tot morgen wat je vandaag nog tot morgen kunt uitstellen...


  • PainkillA
  • Registratie: Augustus 2004
  • Laatst online: 25-03 21:32
Voordeel van mocks is wel dat eht veel makkelijker is om verschillende output scenarios te configureren ipv dat je moet weten dat als je vars X en Y in de constructor stopt dat functie Z dan in die en die scenario dit returnen.

Natuurlijk kan je ook fake implementaties schrijven maar ook die moeten geconfigureerd kunnen worden.

Niet mocken kan eigenlijk vaak alleen als het te mocken object enorm eenvoudig is en bijna puur alleen een DTO zonder gedrag.

Ook als je bijv een dependency hebt welke een Api aanroept. Zonder mock ga je een echte http call doen. Met een mock kun j eenvoudig asserten dan er precies 1 call met bepaalde params is gedaan.

  • Voutloos
  • Registratie: Januari 2002
  • Niet online
PainkillA schreef op zondag 7 februari 2021 @ 13:14:
Ook als je bijv een dependency hebt welke een Api aanroept. Zonder mock ga je een echte http call doen. Met een mock kun j eenvoudig asserten dan er precies 1 call met bepaalde params is gedaan.
Een remote call is dan ook nooit pure, want de response wordt beïnvloedt door externe state. En dan heb je ook nog performance, rate limits, duizende details in de hele stack over meerdere machines, downtime en misschien ook nog kosten per call.

Dus of je wel of niet een remote call je test moet hebben is een no-brainer. :)

{signature}


  • Dricus
  • Registratie: Februari 2002
  • Laatst online: 11:36

Dricus

ils sont fous, ces tweakers

PainkillA schreef op zondag 7 februari 2021 @ 13:14:
Voordeel van mocks is wel dat eht veel makkelijker is om verschillende output scenarios te configureren ipv dat je moet weten dat als je vars X en Y in de constructor stopt dat functie Z dan in die en die scenario dit returnen.
Daar heb je niet persé mocks voor nodig. Je kunt in heel veel gevallen ook gewoon een Wikipedia: Test fixture schrijven (waarin je dan eventueel mocks gebruikt voor implementaties van repositories en dergelijke).
Natuurlijk kan je ook fake implementaties schrijven maar ook die moeten geconfigureerd kunnen worden.
Lang niet altijd. Een in-memory fake implementatie van een repository is meestal super eenvoudig te schrijven en behoeft in heel veel test scenario's nul configuratie.
Niet mocken kan eigenlijk vaak alleen als het te mocken object enorm eenvoudig is en bijna puur alleen een DTO zonder gedrag.
Huh?
Ook als je bijv een dependency hebt welke een Api aanroept. Zonder mock ga je een echte http call doen. Met een mock kun j eenvoudig asserten dan er precies 1 call met bepaalde params is gedaan.
En, zoals ik hierboven al schreef, wil je dat soort asserts als het even kan voorkomen, omdat je dan coupling hebt tussen je tests en je design. De meeste test doubles die ik schrijf zijn eigenlijk stubs of soms fakes (zie http://blog.cleancoder.co...5/14/TheLittleMocker.html voor een laagdrempelige uitleg over test doubles). Het doel is bijna altijd om unit tests zonder database, messaging of andere middleware te kunnen draaien, niet om te verifiëren of class X methode foo uit class Y wel met de juiste parameters aanroept.

Stel niet uit tot morgen wat je vandaag nog tot morgen kunt uitstellen...


  • PainkillA
  • Registratie: Augustus 2004
  • Laatst online: 25-03 21:32
Dricus schreef op zondag 7 februari 2021 @ 15:05:
[...]
Het doel is bijna altijd om unit tests zonder database, messaging of andere middleware te kunnen draaien, niet om te verifiëren of class X methode foo uit class Y wel met de juiste parameters aanroept.
Ok maar stel ik test dus een fuctie op een api client welke een request doet naar een externe partij. De functie heeft geen return value omdat er niets wordt opgehaald maar alleen verstuurd. Hoe ga ik dan zonder mock weten of de juiste logika is uitgevoerd? Ik kan een httpclient stub schrijven die geinjecteerd wordt en waarmee de call wordt gedaan maar om te meten of de juiste uitvoer is gedaan (namelijk precies 1 call van het type X naar endpoint Y) zou je dan toch echt ergens in je stub code moeten hebben met assertions? Wat in dit geval ong hetzelfde zou zijn als dat je met een mock zou werken?

  • Dricus
  • Registratie: Februari 2002
  • Laatst online: 11:36

Dricus

ils sont fous, ces tweakers

PainkillA schreef op zondag 7 februari 2021 @ 15:58:
[...]


Ok maar stel ik test dus een fuctie op een api client welke een request doet naar een externe partij. De functie heeft geen return value omdat er niets wordt opgehaald maar alleen verstuurd. Hoe ga ik dan zonder mock weten of de juiste logika is uitgevoerd? Ik kan een httpclient stub schrijven die geinjecteerd wordt en waarmee de call wordt gedaan maar om te meten of de juiste uitvoer is gedaan (namelijk precies 1 call van het type X naar endpoint Y) zou je dan toch echt ergens in je stub code moeten hebben met assertions? Wat in dit geval ong hetzelfde zou zijn als dat je met een mock zou werken?
Die API call is doorgaans een (klein) onderdeel van één of meerdere stukjes gedrag van je applicatie. Ik zou in zo'n geval tests schrijven voor die stukjes gedrag.

Voorbeeld: Je applicatie heeft een zoekbalk, die ala Google een lijstje suggesties geeft terwijl je typt. Daarvoor vinden op de achtergrond dus tijdens het typen API calls plaats. Nu kun je een test schrijven die "blaat" invoert in de zoekbalk en vervolgens bij een mock gaat checken of er API calls met "b", "bl", "bla", enzovoorts, uitgevoerd worden.

Een alternatieve manier van testen zou kunnen zijn om een simpele stub of een fake voor die API te maken die wat voorgedefinieerde suggesties teruggeeft en een fixture die je SUT opzet met die stub of fake. Vervolgens schrijf je diezelfde test, maar in plaats van te checken of de juist API calls plaatsvinden, check je of de lijst met suggesties de verwachte waardes bevat.

In het eerste geval heb je het gedrag van je code getest; de test is, in de woorden van Kent Beck, "sensitive to structure changes" én "sensitive to behavior changes".

In het tweede geval heb je een test die tegelijk een stukje gedrag van je applicatie beschrijft en test. De test weet niet of er op de achtergrond een API call plaatsvindt, hoe vaak dat gebeurt, welke classes/interfaces daarvoor gebruikt worden, etc. De test is alleen "sensitive to behavior changes". Structure changes hebben hoogstens effect op de fixture en/of je fakes/stubs, niet op de test. Aangezien je doorgaans véél meer tests dan fixtures of test doubles schrijft, voorkom je hiermee het sneeuwbaleffect wat een structure change heeft op je unit tests, als je veel gebruik maakt van het verifiëren van calls op mocks.

Stel niet uit tot morgen wat je vandaag nog tot morgen kunt uitstellen...


  • Lethalis
  • Registratie: April 2002
  • Niet online
Dricus schreef op zondag 7 februari 2021 @ 12:02:
[...]
Tests should be coupled to the behavior of code and decoupled from the structure of code. Seeing tests that fail on both counts. - Kent Beck
Dit lijkt me een goede insteek.

Niet testen om het testen, maar controleren of code daadwerkelijk aan de requirements voldoet. Bijvoorbeeld als een klant geblokkeerd is, dat een service deze klant dan ook echt tegenhoudt.

Mocking kan wel helpen als je bijvoorbeeld fake data wil voeren aan de service. Maar ik test dus meestal de publieke interface van een module / service / whatever.

De implementatiedetails vind ik minder boeiend. Maar goed, ik heb ook geen test fetish. Ik test vooral om groffe fouten er uit te halen en te voorkomen dat er op dat gebied onbedoeld regressies optreden door veranderingen.

Ask yourself if you are happy and then you cease to be.

Pagina: 1


Tweakers maakt gebruik van cookies

Tweakers plaatst functionele en analytische cookies voor het functioneren van de website en het verbeteren van de website-ervaring. Deze cookies zijn noodzakelijk. Om op Tweakers relevantere advertenties te tonen en om ingesloten content van derden te tonen (bijvoorbeeld video's), vragen we je toestemming. Via ingesloten content kunnen derde partijen diensten leveren en verbeteren, bezoekersstatistieken bijhouden, gepersonaliseerde content tonen, gerichte advertenties tonen en gebruikersprofielen opbouwen. Hiervoor worden apparaatgegevens, IP-adres, geolocatie en surfgedrag vastgelegd.

Meer informatie vind je in ons cookiebeleid.

Sluiten

Toestemming beheren

Hieronder kun je per doeleinde of partij toestemming geven of intrekken. Meer informatie vind je in ons cookiebeleid.

Functioneel en analytisch

Deze cookies zijn noodzakelijk voor het functioneren van de website en het verbeteren van de website-ervaring. Klik op het informatie-icoon voor meer informatie. Meer details

janee

    Relevantere advertenties

    Dit beperkt het aantal keer dat dezelfde advertentie getoond wordt (frequency capping) en maakt het mogelijk om binnen Tweakers contextuele advertenties te tonen op basis van pagina's die je hebt bezocht. Meer details

    Tweakers genereert een willekeurige unieke code als identifier. Deze data wordt niet gedeeld met adverteerders of andere derde partijen en je kunt niet buiten Tweakers gevolgd worden. Indien je bent ingelogd, wordt deze identifier gekoppeld aan je account. Indien je niet bent ingelogd, wordt deze identifier gekoppeld aan je sessie die maximaal 4 maanden actief blijft. Je kunt deze toestemming te allen tijde intrekken.

    Ingesloten content van derden

    Deze cookies kunnen door derde partijen geplaatst worden via ingesloten content. Klik op het informatie-icoon voor meer informatie over de verwerkingsdoeleinden. Meer details

    janee