Eenheidstest bondig kijk voordat je springt

Dit is een uittreksel uit het Unit Testing Beknopte eBook, door Marc Clifton, vriendelijk geleverd door Syncfusion.

De vorige artikelen hebben een aantal zorgen en voordelen van het testen van eenheden besproken. Dit artikel is een meer geformaliseerde kijk op de kosten en baten van unit testing.

Eenheidstestcode versus code die wordt getest

De testcode van uw unit is een afzonderlijke entiteit van de code die wordt getest, maar toch deelt deze veel van dezelfde problemen als vereist door uw productiecode:

  • Planning
  • Ontwikkeling
  • Testen (ja, unit tests moeten worden getest)

Daarnaast kunnen unit tests ook:

  • Heb een grotere codebasis dan de productiecode.
  • Moet gesynchroniseerd worden als de productiecode verandert.
  • De neiging om architecturale aanwijzingen en implementatiepatronen af ​​te dwingen.

Unit Test Code Base kan groter zijn dan productiecode

Bij het bepalen of de tests kunnen worden geschreven tegen een enkele methode, moet men overwegen:

  • Is het contract geldig??
  • Werkt de berekening correct??
  • Is de interne status van het object correct ingesteld?
  • Retourneert het object naar een "normale" status als er een uitzondering optreedt?
  • Zijn alle codepaden getest?
  • Welke instel- of scheurvereisten heeft de methode?

Men moet zich realiseren dat het aantal regels code om zelfs een eenvoudige methode te testen aanzienlijk groter kan zijn dan het aantal lijnen van de methode zelf.


Onderhoud van eenheidstests

Het wijzigen van de productiecode kan unittests vaak ongeldig maken. Codewijzigingen kunnen grofweg in twee categorieën worden ingedeeld:

  • Nieuwe code of wijzigingen in bestaande code die de gebruikerservaring verbeteren.
  • Aanzienlijke herstructurering ter ondersteuning van vereisten die de bestaande architectuur niet ondersteunt.

De eerstgenoemde draagt ​​gewoonlijk weinig of geen onderhoudsvereisten voor bestaande eenheidstests. Dit laatste vereist echter vaak aanzienlijke aanpassingen van unit tests, afhankelijk van de complexiteit van de verandering:

  • Refactoring van concrete klasseparameters naar interfaces of abstracte klassen.
  • Refactoring van de klassenhiërarchie.
  • Een technologie van derden vervangen door een andere technologie.
  • Refactoring van de code om asynchroon te zijn of ondersteuningstaken.
  • anderen:
    • Voorbeeld: van een concrete databaseclass veranderen, zoals SqlConnection naar IDbConnection, zodat de code verschillende databases ondersteunt en de eenheidstests opnieuw moet worden bewerkt die methoden aanroepen die afhankelijk waren van concrete klassen voor hun parameters.
    • Voorbeeld: een model wijzigen om een ​​standaard serialisatie-indeling te gebruiken, zoals XML, in plaats van een aangepaste serialisatiemethodologie.
    • Voorbeeld: veranderen van een interne ORM naar een ORM van een derde partij, zoals Entity Framework, kan aanzienlijke wijzigingen in de set-up of demontages van eenheidstests vereisen.

Doet unit-testen een architectuurparadigma afdwingen?

Zoals eerder vermeld, dwingt unit-testing, met name in een testgestuurde proces, bepaalde minimale architectuur- en implementatieparadigma's af. Om het gemak van het opzetten of afbreken van sommige delen van de code nog verder te ondersteunen, kan unit testing ook profiteren van complexere architectuuroverwegingen, zoals het omkeren van controle.


Eenheidstestprestaties

Minstens, zouden de meeste klassen het bespotten van om het even welk voorwerp moeten vergemakkelijken. Dit kan de prestaties van de tests aanzienlijk verbeteren, bijvoorbeeld het testen van een methode die de integriteitscontrole van vreemde sleutels uitvoert (in plaats van te vertrouwen op de database om fouten later te rapporteren) zou geen complexe instelling of demontage van het testscenario in de database vereisen. zelf. Verder zou het niet de methode vereisen om de database daadwerkelijk te bevragen. Dit zijn allemaal prestatie hits op de test en afhankelijkheden toevoegen aan een live, geverifieerde verbinding met de database, en daarom mogelijk niet overweg met een ander werkstation dat exact dezelfde test uitvoert op hetzelfde moment. In plaats daarvan kan de eenheidstest, door de databaseverbinding te bespotten, het scenario eenvoudig in het geheugen instellen en het verbindingsobject doorgeven als een interface.

Eenvoudig spotten met een klasse is echter ook niet noodzakelijkerwijs de beste methode. Het is misschien beter om de code te refactoren, zodat alle informatie die de methode nodig heeft afzonderlijk wordt verkregen, waardoor de verwerving van de gegevens wordt gescheiden van de berekening van de gegevens. Nu kan de berekening worden uitgevoerd zonder te spotten met het object dat verantwoordelijk is voor het verkrijgen van de gegevens, wat de testopstelling verder vereenvoudigt.


Beperkende kosten

Er zijn een aantal kosten-mitigerende strategieën die moeten worden overwogen.

Correcte ingangen

De meest effectieve manier om de kosten van het testen van eenheden te verlagen, is om te voorkomen dat u de test moet schrijven. Hoewel dit vanzelfsprekend lijkt, hoe wordt dit dan bereikt? Het antwoord is om ervoor te zorgen dat de gegevens die worden doorgegeven aan de methode correct zijn, met andere woorden, correcte invoer, correcte uitvoer (het omgekeerde van "vuilnis in, afval"). Ja, u wilt waarschijnlijk nog steeds de berekening zelf testen, maar als u kunt garanderen dat de beller aan het contract voldoet, hoeft u de methode niet uit te testen om te zien of deze met onjuiste parameters omgaat (schendingen van het contract).

Dit is een beetje een hellend vlak omdat je geen idee hebt hoe de methode in de toekomst kan worden aangeroepen. Je zou zelfs willen dat de methode nog steeds zijn contract valideert, maar in de context waarin het momenteel wordt gebruikt, als u kunt garanderen dat aan het contract altijd wordt voldaan, dan heeft het geen zin om tests tegen het contract te schrijven.

Hoe zorg je voor correcte invoer? Voor waarden die afkomstig zijn van een gebruikersinterface, is het op de juiste manier filteren en besturen van de interactie van de gebruiker om de waarden vooraf te filteren één benadering. Een geavanceerdere benadering is om gespecialiseerde types te definiëren in plaats van te vertrouwen op typen voor algemeen gebruik. Overweeg de methode Divide die eerder is beschreven:


public static int Divide (int teller, int noemer) if (noemer == 0) gooi nieuw ArgumentOutOfRangeException ("Noemer kan geen 0." zijn);  geef teller / noemer terug; 

Als de noemer een gespecialiseerd type was dat een niet-nulwaarde garandeerde:

public class NonZeroDouble protected int val; public int Value krijg return val;  set if (value == 0) gooi nieuwe ArgumentOutOfRangeException ("Value can not as 0.");  val = waarde; 

de Divide-methode hoeft nooit voor deze case te testen:

///  /// Een voorbeeld van het gebruik van typespecificiteit om een ​​contracttest te vermijden. ///  public static int Divide (int teller, NonZeroDubbele noemer) return teller / noemer.Waarde; 

Wanneer men bedenkt dat dit de typespecificiteit van de toepassing verbetert en (hopelijk) herbruikbare typen tot stand brengt, realiseert men zich hoe dit vermijdt om een ​​hele reeks eenhedentests te moeten schrijven omdat code vaak typen gebruikt die te algemeen zijn.

Uitzonderingen van derden vermijden

Stel uzelf de vraag - moet mijn methode verantwoordelijk zijn voor het omgaan met uitzonderingen van derden, zoals webservices, databases, netwerkverbindingen, enz.? Er kan worden gesteld dat het antwoord "nee" is. Toegegeven, dit vereist wat verder werk van voren - de externe (of zelfs raamwerk) API heeft een omhulsel nodig dat de uitzondering afhandelt en een architectuur waarin de interne status van de toepassing kan worden teruggedraaid wanneer er een uitzondering optreedt en deze waarschijnlijk moet worden geïmplementeerd. Dit zijn hoe dan ook waarschijnlijk de moeite waard verbeteringen aan de applicatie.

Vermijd het schrijven van dezelfde tests voor elke methode

De eerdere voorbeelden - correcte invoer, gespecialiseerde typestelsels, waarbij uitzonderingen van derden worden vermeden - dragen allemaal bij tot meer algemene doeleinden en mogelijk herbruikbare code. Dit helpt om te voorkomen dat u dezelfde of vergelijkbare validatie van overeenkomsten, tests voor de afhandeling van uitzonderingsprocedures schrijft en u in plaats daarvan kunt richten op tests die valideren wat de methode zou moeten doen onder normale omstandigheden, namelijk de berekening zelf.


Kostenvoordelen

Zoals eerder vermeld, zijn er duidelijke kostenvoordelen voor het testen van eenheden.

Codering voor de vereiste

Een van de voor de hand liggende voordelen is het proces van het formaliseren van de interne codevereisten van externe bruikbaarheid / procesvereisten. Naarmate men deze oefening doorloopt, is richting met de algehele architectuur meestal een bijkomend voordeel. Meer concreet, het ontwikkelen van een reeks tests waaraan een specifieke vereiste is voldaan van de eenheid perspectief (in plaats van de integratietest perspectief) is een objectief bewijs dat de code de vereiste implementeert.

Vermindert stroomafwaartse fouten

Regressietesten is een ander (vaak meetbaar) voordeel. Naarmate de codebasis groeit, controleert het verifiëren of de bestaande code nog steeds werkt zoals bedoeld, een aanzienlijke handmatige testtijd en wordt het scenario "Oeps, we hebben niet getest voor dat" vermeden. Wanneer een fout wordt gerapporteerd, kan deze bovendien onmiddellijk worden gecorrigeerd, waardoor andere leden van het team vaak de grote moeite hebben om zich af te vragen waarom iets waarop ze vertrouwden plotseling niet correct werkt..

Testgevallen bieden een vorm van documentatie

Unit tests verifiëren niet alleen dat een methode zichzelf correct verwerkt wanneer slechte inputs of uitzonderingen van derden worden gegeven (zoals eerder beschreven, probeer dit soort tests te verminderen), maar ook hoe de methode zich naar verwachting onder normale omstandigheden gedraagt. Dit levert waardevolle documentatie op voor ontwikkelaars, met name nieuwe teamleden - via de unit-test kunnen ze eenvoudig de set-upvereisten en de use-cases inlezen. Als uw project een belangrijke architecturale refactoring ondergaat, kunnen de nieuwe unittests worden gebruikt om ontwikkelaars te begeleiden bij het herwerken van hun afhankelijke code.

Het afdwingen van een architectuurparadigma verbetert de architectuur

Zoals eerder beschreven, een meer robuuste architectuur door het gebruik van interfaces, omkering van besturing, gespecialiseerde typen, enz., Die allemaal het testen van eenheden vergemakkelijken-ook de robuustheid van de applicatie verbeteren. Vereisten veranderen, zelfs tijdens de ontwikkeling, en een goed doordachte architectuur kan deze wijzigingen aanzienlijk beter aan dan een applicatie die geen of weinig architecturale aandacht heeft.

Junior programmeurs

In plaats van een junior programmeur een hoge vereiste te geven om te worden geïmplementeerd op het niveau van de programmeur, kunt u in plaats daarvan een hoger niveau van code en succes garanderen (en een leservaring bieden) door de junior programmeurcode de implementatie te laten hebben tegen de test in plaats van de vereiste. Dit elimineert veel slechte praktijken of giswerk dat een junior programmeur uiteindelijk uitvoert (we zijn er allemaal geweest) en vermindert de herstelling die een meer ervaren ontwikkelaar in de toekomst moet doen.

Code Reviews

Er zijn verschillende soorten codebeoordelingen. Eenheidstests kunnen de hoeveelheid tijd die wordt besteed aan het beoordelen van code voor architecturale problemen verminderen omdat ze de neiging hebben om architectuur af te dwingen. Bovendien valideren eenheidstests de berekening en kunnen ze ook worden gebruikt om alle codepaden voor een bepaalde methode te valideren. Dit maakt coderingsbeoordelingen bijna overbodig - de unit-test wordt een zelfbeoordeling van de code.

Eisen voor testen converteren

Een interessant neveneffect van het converteren van externe bruikbaarheid of procesvereisten naar geformaliseerde codetests (en hun ondersteunende architectuur) is dat:

  • Problemen met de vereisten worden vaak ontdekt.
  • Bouwkundige eisen worden aan het licht gebracht.
  • Aannames en andere hiaten in de vereisten worden geïdentificeerd.

Deze ontdekkingen, als een resultaat van het eenheidscontroleproces, identificeren problemen eerder in het ontwikkelingsproces, wat meestal helpt om verwarring te verminderen, te herwerken en daardoor de kosten te verlagen.