Niet mocken!

Het heeft voor mijn gevoel tamelijk lang geduurd, maar unit-testing begint nu langzaam maar zeker normaal te worden in een software-ontwikkeltraject. Zelf kan ik niet meer zonder. Binnen een statisch getypeerde taal is het namelijk één van de weinige mogelijkheden om direct feedback van jouw software te krijgen. Toch lijkt in het huidige gebruik een belangrijk aspect van unit-testen verloren te gaan.

Om voor elke methode en class in je code een test te schrijven lijkt op het eerste gezicht veel werk met slechts beperkt nut. Bij mijn eigen introductie in het unit-testen had ik ook dat gevoel. Na erover nagedacht te hebben en het tijdens een project toegepast te hebben was ik echter om. Ze helpen echt bij het ontwikkelen. Sinds dat project ben ik een trouwe unit-tester.

Helaas zijn niet alle methodes even gemakkelijk te testen. Methodes en classes grijpen soms op complexe wijze in elkaar en hebben afhankelijkheden naar externe resources. Waardoor het vrijwel onmogelijk wordt een goede test te schrijven voor een methode zonder de rest van het programma te gebruiken.

Maar geen nood, zoals gebruikelijk met dit soort problemen in software-ontwikkeling, hebben de knappe koppen een oplossing gevonden in de vorm van een framework. De afhankelijkheden die het testen lastig maken kunnen nu gesimuleerd worden. Dit maakt het mogelijk om de methode die gebruik maakt van al die andere objecten toch te testen. Door mock-versies te maken van die objecten…

De volgende vraag is dan natuurlijk; Welke van deze handige frameworks gebruik IK? Het antwoord is misschien al aan de titel te lezen: Geen! Mocken is namelijk onzin! In al die tijd dat ik unit-tests schrijf heb ik nog nooit een mock-object nodig gehad.

“Ja maar”, roep je nu misschien, “iedereen heeft toch wel methodes die anders niet te testen zijn?”. Dat klopt, maar misschien is het een indicatie van een ander probleem. Dat er een unit-test geschreven wordt voor de methode wil zeggen dat er bepaalde logica in deze methode aanwezig is. Een methode zonder logica heeft namelijk weinig zin om te testen. Dat deze methode te lastig is om te testen wil ook zeggen dat deze methode communiceert met andere objecten of entiteiten. Dus de methode heeft logica en collaboratie. Dit zijn twee aparte taken. Een goede methode hoort echter maar een enkele taak te hebben. Het antwoord is dus niet een mock object introduceren, maar je code refactoren!

Unit-tests testen niet alleen de logica van de software. Ze testen ook de testbaarheid van jouw code. Dit klinkt misschien een beetje dubbel, maar de testbaarheid van je code wordt bepaald door de mate van ontkoppeling en encapsulatie. Met andere woorden, het design van jouw software. Als unit-testen lastig is in jouw applicatie dan is dat niet een reden om mock-objecten te introduceren of misschien maar helemaal niet te te testen. Het is een indicatie dat er fundamentele fouten in het design zitten.

Dit klink misschien een beetje abstract, maar uit eindelijk is het niet zo ingewikkeld. Een situatie die ik vaak tegenkom is bijvoorbeeld deze:

void doSomething(){
data = this.dao.ExcecuteSomeComplexDataFindingArgorithm();
// do something with data
}
Dit is min of meer de structuur die door bepaalde ontwikkelframeworks in de hand gewerkt wordt. Om deze methode te testen moet echter het dao-object aanwezig zijn. Dus kan dit niet getest worden zonder een mock dao object te gebruiken. Er zijn zelfs speciale frameworktechnieken bedacht om het eenvoudig te maken het dao-object te wisselen.

De juist oplossing is echter ontkoppeling. Het dao-object en de ‘do something’ hebben eigenlijk niets met elkaar te maken. Een eenvoudige ontkoppeling is een nieuwe methode te creëren voor onze logica die een argument van het type data ontvangt:

void doSomething(){
data = this.dao.ExcecuteSomeComplexDataFindingArgorithm();
doSomething(data)
}
void doSomething(data){
// do something with data
}
De methode doSomething(data) heeft nu geen afhankelijkheid meer met het dao object en kan eenvoudig getest worden. De methode doSomething zonder argumenten bevat geen eigen logica en hoeft niet getest te worden.

Op deze manier kan volgens mij elk mock-object vermeden worden. Soms is er iets meer nodig. Misschien een nieuwe class of moet de methode naar een ander object verplaatst worden, maar ik wil beweren dat het altijd mogelijk is en je design er altijd beter van wordt.

Misschien zijn er mensen die het niet met me eens zijn. Een enkele keer wil dat wel eens gebeuren. Ik zou deze mensen willen uitnodigen om fragmenten met niet testbare code in de reacties te plaatsten als een tegenvoorbeeld. Aan mij dan om het tegendeel te bewijzen.