Unit Testing - Hoofdstuk 3

Ingediend door Dirk Hornstra op 30-aug-2021 09:20

We gaan aan de slag met Unit Testing, Principles, Practices and Patterns. Vorige keer hoofdstuk 1 behandeld: link, daarna hoofdstuk 2: link, nu is het tijd voor hoofdstuk 3.

De Timo-samenvatting

Gebruik xUnit voor je testen, met [Fact] en [Theory] kun je jouw testscenario's overzichtelijk uitwerken. Met een IDisposable-class en contructor kun je centraal je algemene zaken opstarten en bij afronden weer opruimen. Elke functie volgt het AAA-patroon: zaken opbouwen en inrichten, uitvoeren en daarna het resultaat valideren. En een functie moet een duidelijke Engelse naam hebben wat er getest wordt.

 

Hoe richt je een unit-test in? De auteur werkt met xUnit, de tag voor een test is [Fact]. We beginnen met AAA: arrange, act en assert. Dit is logisch, eerst declareer je de variabelen, geef je ze de beginwaardes, voer je de functie uit en daarna doe je een Assert om te controleren of de test gelukt of mislukt is. Er is ook een andere naam voor dit type opzet, Given-When-Then. Dezelfde stappen, alleen is dit "leesbaarder voor mensen die niet zo in code zitten". Als je meerdere AAA-secties in 1 test hebt, dan is het geen unit-test maar een integratie-test. Maak er losse tests van zodat je per functie maar 1 AAA-flow hebt. Bij integratie-tests mag je dit wel zo houden.

Zorg dat er geen if-statements in je test zit. Hiermee test je meerdere zaken, deze moet je splitsen naar eigen functies. Ook voor integratie-test geldt hiervoor geen uitzondering.

Hoe groot zijn de secties? De arrange-sectie is meestal het grootst. Als het veel groter wordt dan de Act en Assert functie is het verstandig om die Arrange in een eigen private functie te zetten en aan te roepen (daarmee houd je de test ook leesbaar). De auteur verwijst naar 2 patronen: Object Mother en Test Data Builder. Een act-sectie zou eigenlijk uit 1 regel moeten bestaan. Zijn dit er meer, dan is dit een indicatie dat de code niet helemaal fris is. Voor "utility" en "infrastructurele code" kan een uitzondering worden gemaakt, omdat die soms wel meerdere regels nodig hebben. De assert-sectie mag groter zijn dan 1 assert-statement. Je test het gedrag, dus je kunt meerdere resultaten hebben (en dus meerdere assertions). Als het er teveel worden, geeft dat aan dat de code die getest wordt waarschijnlijk verbeterd kan worden.

Sommige developers hebben daarna nog een teardown-fase, waarbij aangemaakte bestanden, records in databases worden opgeruimd. Unit-testen hebben dit over het algemeen niet, die gebruiken geen out-of-process afhankelijkheden, waardoor er geen zaken opgeruimd hoeven worden. Dat zit meer in het integratie-testen gebied.

De sut is het object wat getest wordt, in het boek wordt de variabele dan ook "sut" genoemd, zodat je weet: die zaken ervoor zijn afhankelijkheden, de "sut", daar gaat het om.

Om de test overzichtelijk te houden, scheidt de secties (AAA) met een lege regel, of als de code nogal wat regels in beslag neemt, gebruik comments, dus //Arrange, //Act, //Assert.

Het test-framework.

De auteur gebruikt xUnit (link). Er zijn alternatieven, nUnit (link) en MSTest. Dat laatste raadt de auteur af, ook het Microsoft ASP.NET Core team schijnt xUnit te gebruiken. Zijn voorkeur voor xUnit komt doordat bij een "test" alleen het attribuut [Fact] voldoende is. Je kunt de test-class een constructor geven: hiermee zorg je dat initialisatie altijd uitgevoerd wordt (je hoeft dus geen [Setup]-functie toe te voegen. En door de class te laten erven van IDisposable kun je een Dispose-functie toevoegen voor het opruimen: geen [TearDown]-functie meer nodig.

Fixture

Met fixtures stel je bepaalde zaken in, bijvoorbeeld het shop-object, waarbij het product Shampoo een voorraad van 10 stuks krijgt. De auteur toont een foutief voorbeeld, waarbij dat in de constructor gedaan wordt. En 2 testen doen validaties (1 op 5 aankopen: succes, 1 op 15 aankopen: failure). Als je echter het aantal in de constructor op 20 zet, geeft de ene nog steeds true, maar de ander nu ook. Je hebt 2 testen afhankelijk van elkaar gemaakt. Door met een factory-method te werken (er komt nu een functie CreateStoreWithInventory, waarbij je in de parameter het aantal mee kunt geven) trek je de boel los en is ook door het "lezen" van 1 test duidelijk wat er getest wordt. Zaken aanmaken in de constructor, dat doe je over het algemeen voor objecten die bijna overal in de code gebruikt worden (bijvoorbeeld database-toegang, toegang tot een map op het filesysteem).

Naamgeving

Een veel gebruikte naamgeving is methode-die-je-test_scenario_verwacht-resultaat. Maar vaak maakt dat niet duidelijk wat de test doet, in het boek wordt het voorbeeld gegeven van sum_twonumbers_returnssum en de sum_of_two_numbers. Het is wel duidelijk welke naam je in één oogopslag duidelijk maakt wat er getest wordt. Het heeft geen toegevoegde waarde om de naam van de functie van de SUT (object wat getest wordt) in de naam van de testfunctie op te nemen. In dit geval wordt IsDeliveryValid aangeroepen, maar het gaat ons alleen in de test-case om de afleverdatum (die in de toekomst moet liggen).

Je kunt je test-code refactoren door parameters. Het boek geeft het voorbeeld van het testen met leverdatums. We hadden al dat de datum van gisteren ongeldig is. Vandaag en morgen leveren kan/mag niet en de achterliggende gedachte is dat de leverdatum vandaag + 2 dagen moet zijn. In de code zien we nu dat de [Fact] vervangen is met [Theory] omdat nu meerdere vaste waarden meegegeven worden. Deze komen binnen als [InlineData(offset, verwacht resultaat)] en zo kun je er dus 4 toevoegen: -1, false; 0, false; 1, false; 2, true;

Die methode maakt het echter weer minder leesbaar. Als je straks 20 inlinedata-regels hebt, snap je niet meer wat de code eigenlijk doet. Dus de auteur laat het voorbeeld zien van de [InlineData] die binnen een [Theory] zit, maar die die voor elk item een aanroep doet naar een [Fact] functie, waar de eigen validatie in zit.

De data die in die InlineData zit, moet beschikbaar zijn tijdens Compile-tijd. Dus een System.DateTime.Now kun je niet gebruiken. Niet rechtstreeks, met het MemberData-attribuut kun je het alsnog toevoegen. In plaats van de [InlineData] krijg je dan een [MemberData(nameof(Data))] en vervolgens kun je een static functie Data maken die je een lijst terug geeft.

Het valideren van het testresultaat doe jet met een Assert.True(...), Assert.False(...) of Assert.Equal(a,b). De auteur adviseert om een "fluent library" te gebruiken zodat je de validatie kunt formuleren als result.Should().Be(30); Er zijn meerdere tools die dit kunnen, de auteur verwijst naar FluentAssertions: link.