Unit tests in C# met xUnit.net
Auteur: Danny Holstein
De ontwikkeling van applicaties kan complex zijn, waarbij een release snel plaatsvindt. Ook kan er in een vroeg stadium deelname zijn van eindgebruikers. Een applicatie wordt ontsierd wanneer gebruikers tegen allerlei problemen aanlopen.
Aan de kant van de developers is het echter onmogelijk om te wachten op alle feedback van eindgebruikers om zodoende de problemen op te sporen. Wanneer een probleem wordt verholpen, kan echter de situatie ontstaan waarbij een ander stuk van de code breekt of er kan dubbele code ontstaan.
Wanneer er extra methoden worden toegevoegd om een probleem te verhelpen, kan de applicatie veranderen in een ‘onderhouds-nachtmerrie’. Er kan zelfs technological debt (technologische schuld) ontstaan waarbij de vele, snelle en incomplete reparaties de neiging heeft om nog verder op te stapelen.
Bovenstaande problemen zijn te voorkomen door unit- en integratietests te schrijven voor een applicatie.
Theorie: de 3 A’s
Gewoonlijk zijn er vele unit tests, maar zijn er slechts enkele integratietests. Een unit test moet de functionaliteit verifiëren van de kleinste testbare delen van een applicatie en dat zijn gewoonlijk de methoden. Het is hierbij de bedoeling dat ieder mogelijk pad binnen een methode wordt getest. Met een integratietest wordt de gezamenlijke functionaliteit van methoden uitgetest.
De rode lijn voor iedere unit test is het herhalende patroon van de 3 A’s: Arrange, Act en Assert.
1. Arrange = alles wat nodig is voor de test wordt klaargezet. Het is duidelijk welke class en of methode(n) er worden getest.
2. Act = de test wordt uitgevoerd. De methoden worden aangeroepen met verschillende argumenten.
3. Assert = het werkelijke resultaat wordt vergeleken met het verwachte resultaat. Wanneer beide resultaten overeenkomen, dan is de test geslaagd. Maar als één Assert mislukt, dan mislukt ook de test.
Stubs en Mocks
Verder is het de bedoeling dat tests zo snel mogelijk worden uitgevoerd en niet afhankelijk zijn van externe dependencies. Het is hierbij bijvoorbeeld niet de bedoeling dat er verzoeken over HTTP worden gemaakt. Unit tests worden in isolatie uitgevoerd.
Dependencies en of verzoeken over HTTP kunnen gesimuleerd worden door middel van stubs en mocks. Een stub is een hulpobject dat er is om de test te assisteren – dit is vaak een dependency (zoals de HTTP-Client) die nagemaakt wordt. Een mock werkt meer als een datarecorder die zowel de aangeroepen methoden onthoudt alsook de waardes hiervan vastlegt.
Om de analogie van een auto-crashtest te gebruiken: de muur (het hulpobject) waar de auto tegen aanrijdt kan gezien worden als de stub. De Dummy die in de auto zit kan gezien worden als de mock. In de Assert-fase van een test wordt als het ware nagegaan waar de Dummy precies is geraakt en welke waardes er hierbij zijn vastgelegd.
In xUnit wordt verder gebruik gemaakt van de attributen: Fact en Theory. Bij Fact is er sprake van een methode die hetzelfde resultaat teruggeeft. Maar bij Theory veranderen de argumenten en het resultaat van een methode wel. De verschillende argumenten worden hierbij doorgegeven met InlineData of MemberData attributen.
Unit tests in de praktijk
In de praktijk levert het schrijven van unit tests het nodige werk op. Het is niet de meest spannende programmeerervaring, maar het kan wel zeer nuttig zijn. Tijdens het schrijven van de unit tests vielen mij toch ineens zaken op die voorheen over het hoofd zijn gezien, zoals: (type)fouten en dubbele stukken code.
Het schrijven van unit tests geeft ook een goede gelegenheid om een applicatie beter te documenteren. Hierdoor kan er nog meer dubbele code worden verwijderd.
Het maken van unit tests heeft vooral zin voor applicaties of delen daarvan waar bugs zijn te verwachten. In de praktijk liep ik tegen delen code aan die niet in isolatie waren te testen. De oplossing was om de methoden meer te splitsen en code te refactoren (het herstructureren van de code om de leesbaarheid en het onderhoud te verbeteren).
In de praktijk liep ik er tegenaan dat Lists niet met InlineData-attributen zijn door te geven aan een test, maar weer wel met MemberData-attributen en het gebruik van static methoden en properties. Het testen van public methoden was geen probleem. Het testen van private methoden echter, wordt gedaan door middel van Reflection (dat is in C# de mogelijkheid om programma-elementen te inspecteren en te manipuleren tijdens de runtime). Dit laatste helpt om de tests zo simpel mogelijk te houden.
Wanneer eenmaal het aparte Test-project is gemaakt, dan is er het voordeel dat fouten sneller en makkelijker opgespoord kunnen worden. Hierbij hoeft niet de hele applicatie gebruikt te worden, maar kan er toegespitst worden op één onderdeel. Wanneer de oorzaak is gevonden, dan is een gerichte oplossing eenvoudiger toe te passen, zonder dat dit gevolgen heeft voor de rest van de applicatie.
Afbeelding onder: de Test Explorer
Tot slot
Door middel van unit tests zijn veel bugs in een applicatie te voorkomen. Unit tests helpen zeker bij het onderhoud van de applicatie en dit kan hand-in-hand gaan met de documentatie van een applicatie. Met unit tests is het veiliger om veranderingen in de code te maken zonder dat er andere delen in de applicatie worden gebroken.
C# heeft niet alleen xUnit als test-framework, maar er is ook MSTest en NUnit. Het hangt af van de voorkeur en behoeften van een team om voor een bepaald test-framework te kiezen.
Het is aanvankelijk veel werk om unit- en integratietests te schrijven. Het is wel beter om als team op een systematische manier problemen met een applicatie te voorkomen in plaats dat eindgebruikers of klanten deze problemen opmerken.