Dit is een uittreksel uit het Unit Testing Beknopte eBook, door Marc Clifton, vriendelijk geleverd door Syncfusion.
De uitdrukking "bewijs van juistheid" wordt normaal gesproken gebruikt in de context van de waarachtigheid van een berekening, maar met betrekking tot het testen van eenheden heeft het aantonen van de juistheid in feite drie brede categorieën, waarvan alleen de tweede betrekking heeft op berekeningen zelf:
Er zijn veel aspecten van een toepassing waarbij het testen van eenheden meestal niet kan worden toegepast om de juistheid te bewijzen. Deze omvatten de meeste functies van de gebruikersinterface, zoals lay-out en bruikbaarheid. In veel gevallen is testen van eenheden niet de juiste technologie voor testvereisten en toepassingsgedrag met betrekking tot prestaties, belasting, enzovoort.
Het bewijzen van de juistheid houdt in:
Laten we eens kijken naar enkele voorbeelden van elk van deze categorieën, hun sterke en zwakke punten en problemen die we zouden kunnen tegenkomen met onze code.
De meest elementaire vorm van unit-testen is om te controleren of de ontwikkelaar een methode heeft geschreven die duidelijk het "contract" tussen de beller en de methode die wordt gebeld, vermeldt. Dit neemt meestal de vorm aan van het verifiëren dat slechte invoer naar een methode resulteert in een uitzondering die wordt geworpen. Een methode "verdelen door" kan bijvoorbeeld een ArgumentOutOfRangeException
als de noemer 0 is:
public static int Divide (int teller, int noemer) if (noemer == 0) gooi nieuw ArgumentOutOfRangeException ("Noemer kan geen 0." zijn); geef teller / noemer terug; [TestMethode] [ExpectedException (typeof (ArgumentOutOfRangeException))] public void BadParameterTest () Divide (5, 0);
Het verifiëren dat een methode contracttests uitvoert, is echter een van de zwakste eenheidscontroles die iemand kan schrijven.
Bij een sterkere eenheidscontrole moet worden gecontroleerd of de berekening juist is. Het is handig om uw methoden in te delen in een van de drie vormen van berekening:
Deze bepalen de soorten eenheidstests die u voor een bepaalde methode wilt schrijven.
De Verdelen
methode in de vorige steekproef kan worden beschouwd als een vorm van gegevensreductie. Het kost twee waarden en retourneert één waarde. Illustreren:
[TestMethod] public void VerifyDivisionTest () Assert.IsTrue (Divide (6, 2) == 3, "6/2 moet gelijk zijn aan 3!");
Dit is illustratief voor het testen van een methode die de ingangen gewoonlijk reduceert tot één resulterende uitvoer. Dit is de eenvoudigste vorm van handig testen van eenheden.
Datatransformatie-eenheidstests werken meestal op sets met waarden. Het volgende is bijvoorbeeld een test voor een methode die cartesiaanse coördinaten omzet in poolcoördinaten.
public static double [] ConvertToPolarCoordinates (dubbel x, dubbel y) double dist = Math.Sqrt (x * x + y * y); dubbele hoek = Math.Atan2 (y, x); return new double [] dist, angle; [TestMethod] public void ConvertToPolarCoordinatesTest () double [] pcoord = ConvertToPolarCoordinates (3, 4); Assert.IsTrue (pcoord [0] == 5, "Verwachte afstand gelijk aan 5"); Assert.IsTrue (pcoord [1] == 0.92729521800161219, "Verwachte hoek is 53.130 graden");
Deze test verifieert de juistheid van de wiskundige transformatie.
Lijsttransformaties moeten in twee tests worden verdeeld:
Bijvoorbeeld, vanuit het perspectief van unit testing, is het volgende voorbeeld slecht geschreven omdat het zowel de datareductie als de datatransformatie omvat:
public struct Naam public string FirstName get; vast te stellen; public string LastName get; vast te stellen; openbare lijstConcatNames (Lijst namen) Lijst concatenatedNames = nieuwe lijst (); foreach (Naam naam in namen) concatenatedNames.Add (name.LastName + "," + name.FirstName); retourneer aaneengeschakelde namen; [TestMethod] public void NameConcatenationTest () Lijst namen = nieuwe lijst () new Name () FirstName = "John", LastName = "Travolta", new Name () FirstName = "Allen", LastName = "Nancy"; Lijst newNames = ConcatNames (namen); Assert.IsTrue (newNames [0] == "Travolta, John"); Assert.IsTrue (newNames [1] == "Nancy, Allen");
Deze code wordt beter getest door een unit door de datareductie van de datatransformatie te scheiden:
public string Concat (Naam naam) return name.LastName + "," + name.FirstName; [TestMethod] public void ContactNameTest () Name name = new Name () FirstName = "John", LastName = "Travolta"; string concatenatedName = Concat (naam); Assert.IsTrue (geconcatenatedName == "Travolta, John");
De Language-Integrated Query (LINQ) -syntaxis is nauw gekoppeld aan lambda-expressies, wat resulteert in een gemakkelijk te lezen syntaxis die het testen van eenheden moeilijk maakt. Bijvoorbeeld deze code:
openbare lijstConcatNamesWithLinq (Lijst namen) retournamen. Selecteer (t => t.LastName + "," + t.FirstName) .ToList ();
is aanzienlijk eleganter dan de vorige voorbeelden, maar leent zich niet goed voor het testen van de eenheid, dat wil zeggen de datareductie van een naamstructuur naar een enkele door komma's gescheiden string, uitgedrukt in de lambda-functie t => t.LastName + "," + t.FirstName
. Om het apparaat van de lijstbewerking te scheiden, hebt u het volgende nodig:
openbare lijstConcatNamesWithLinq (Lijst namen) keer namen terug. Selecteer (t => Concat (t)). ToList ();
We kunnen zien dat testen van eenheden vaak een herziening van de code nodig heeft om de eenheden van andere transformaties te scheiden.
De meeste talen zijn "stateful" en klassen beheren vaak de status. De staat van een klasse, vertegenwoordigd door zijn eigenschappen, is vaak een nuttig ding om te testen. Beschouw deze klasse die het concept van een verbinding vertegenwoordigt:
public class AlreadyConnectedToServiceException: ApplicationException public AlreadyConnectedToServiceException (string-bericht): base (msg) public class ServiceConnection public bool Connected get; beschermde set; public void Connect () if (Connected) throw new AlreadyConnectedToServiceException ("Er is slechts één verbinding per keer toegestaan."); // Verbinding maken met de service. Connected = true; public void Disconnect () // Verbreek de verbinding met de service. Connected = false;
We kunnen eenheidstests schrijven om de verschillende toegestane en ongeoorloofde toestanden van het object te verifiëren:
[TestClass] public class ServiceConnectionFixture [TestMethod] public void TestInitialState () ServiceConnection conn = new ServiceConnection (); Assert.IsFalse (conn.Connected); [TestMethod] public void TestConnectedState () ServiceConnection conn = new ServiceConnection (); conn.Connect (); Assert.IsTrue (conn.Connected); [TestMethod] public void TestDisconnectedState () ServiceConnection conn = new ServiceConnection (); conn.Connect (); conn.Disconnect (); Assert.IsFalse (conn.Connected); [TestMethod] [ExpectedException (typeof (AlreadyConnectedToServiceException))] public void TestAlreadyConnectedException () ServiceConnection conn = new ServiceConnection (); conn.Connect (); conn.Connect ();
Hier verifieert elke test de juistheid van de staat van het object:
Staatsverificatie onthult vaak bugs in het overheidsbeheer. Zie ook de volgende "spotklassen 'voor verdere verbeteringen aan de voorgaande voorbeeldcode.
Externe foutafhandeling en herstel is vaak belangrijker dan testen of uw eigen code op de juiste momenten uitzonderingen genereert. Daar zijn verschillende redenen voor:
Dit soort uitzonderingen is moeilijk te testen omdat ze tenminste een fout moeten maken die meestal wordt gegenereerd door de service die u niet beheert. Een manier om dit te doen is de service te "bespotten"; dit is echter alleen mogelijk als het externe object is geïmplementeerd met een interface, een abstracte klasse of virtuele methoden.
De eerdere code voor de klasse "ServiceConnection" is bijvoorbeeld niet bespottelijk. Als u het statusbeheer ervan wilt testen, moet u fysiek een verbinding maken met de service (wat die dan ook is) die al dan niet beschikbaar is bij het uitvoeren van de unit-tests. Een betere implementatie kan er als volgt uitzien:
openbare klasse MockableServiceConnection public bool Connected get; beschermde set; protected virtual void ConnectToService () // Maak verbinding met de service. protected virtual void DisconnectFromService () // Verbreek de verbinding met de service. public void Connect () if (Connected) throw new AlreadyConnectedToServiceException ("Er is slechts één verbinding per keer toegestaan."); ConnectToService (); Connected = true; public void Disconnect () DisconnectFromService (); Connected = false;
Merk op hoe je met deze kleine refactoring nu een nepklasse kunt schrijven:
public class ServiceConnectionMock: MockableServiceConnection protected override void ConnectToService () // Niets doen. protected override void DisconnectFromService () // Niets doen.
waarmee u een eenheidscontrole kunt schrijven die het statusbeheer test, ongeacht de beschikbaarheid van de service. Zoals dit illustreert, kunnen zelfs eenvoudige architecturale of implementatiewijzigingen de testbaarheid van een klasse aanzienlijk verbeteren.
Je eerste verdedigingslinie om te bewijzen dat het probleem is verholpen, bewijst ironisch genoeg dat het probleem bestaat. Eerder zagen we een voorbeeld van het schrijven van een test die aantoonde dat de Divide-methode controleert op een noemerwaarde van 0
. Laten we zeggen dat een foutenrapport is opgeslagen omdat een gebruiker het programma heeft gecrasht tijdens het invoeren 0
voor de noemerwaarde.
De eerste orde van zaken is om een test te maken die deze voorwaarde uitoefent:
[TestMethode] [ExpectedException (typeof (DivideByZeroException))] public void BadParameterTest () Divide (5, 0);
Deze test passes omdat we bewijzen dat de bug bestaat door te controleren of de noemer is 0
, een DivideByZeroException
is opgevoed. Dit soort tests worden beschouwd als "negatieve tests", zoals zij voorbij lopen wanneer een fout optreedt. Negatief testen is net zo belangrijk als positief testen (wordt hierna besproken) omdat het het bestaan van een probleem verifieert voordat het wordt gecorrigeerd.
Het is duidelijk dat we willen bewijzen dat een probleem is opgelost. Dit is een "positieve" test.
We kunnen nu een nieuwe test introduceren, een test die zal testen of de code zelf de fout detecteert door een ArgumentOutOfRangeException
.
[TestMethode] [ExpectedException (typeof (ArgumentOutOfRangeException))] public void BadParameterTest () Divide (5, 0);
Als we deze test kunnen schrijven voor vaststelling van het probleem, zullen we zien dat de test mislukt. Uiteindelijk, na het oplossen van het probleem, slaagt onze positieve test en de negatieve test mislukt nu.
Hoewel dit een triviaal voorbeeld is, toont het twee concepten:
Ten slotte is het bewijzen dat een bug bestaat niet altijd gemakkelijk. Als algemene vuistregel zijn unit-tests die te veel setup en spot vereisen echter een indicator dat de code die wordt getest niet voldoende geïsoleerd is van externe afhankelijkheden en mogelijk een kandidaat is voor refactoring..
Het moet duidelijk zijn dat regressietests een meetbaar nuttig resultaat zijn van het testen van eenheden. Aangezien de code wijzigingen ondergaat, zullen er bugs worden geïntroduceerd die zullen worden onthuld als u een goede codedekking hebt in uw unittests. Dit bespaart effectief veel tijd bij het opsporen van fouten en, nog belangrijker, bespaart tijd en geld wanneer de programmeur de fout ontdekt in plaats van de gebruiker.
Applicatieontwikkeling begint meestal met een reeks vereisten op hoog niveau, meestal gericht op de gebruikersinterface, workflow en berekeningen. Idealiter verlaagt het team de zichtbaar aantal vereisten tot een reeks programmatische vereisten, dat zijn onzichtbaar voor de gebruiker, door hun aard.
Het verschil manifesteert zich in de manier waarop het programma wordt getest. Integratietesten is meestal in de zichtbaar niveau, terwijl het testen van eenheden op de fijnere korrel ligt onzichtbaar, programmatische correctheid testen. Het is belangrijk om te onthouden dat unit tests niet bedoeld zijn om integratietesten te vervangen; echter, net als bij applicatie-eisen op hoog niveau, zijn er low-level programmatische vereisten die kunnen worden gedefinieerd. Vanwege deze programmatische vereisten is het belangrijk om eenheidstests te schrijven.
Laten we een ronde-methode nemen. De methode .NET Math.Round rondt een getal af waarvan de breukcomponent groter is dan 0,5, maar wordt afgerond naar beneden als de breukcomponent 0,5 of minder is. Laten we zeggen dat dit niet het gedrag is dat we willen (om welke reden dan ook), en we willen afronden wanneer de fractionele component 0,5 of groter is. Dit is een computervereiste dat moet kunnen worden afgeleid van een integratie-eis van een hoger niveau, resulterend in de volgende methode en test:
public static int RoundUpHalf (double n) if (n < 0) throw new ArgumentOutOfRangeException("Value must be >= 0. "); int ret = (int) n; dubbele breuk = n - ret; if (breuk> = 0,5) ++ ret; retourneer ret; [TestMethod] openbare ongeldig RoundUpTest () int result1 = RoundUpHalf (1.5); int result2 = RoundUpHalf (1.499999); Assert.IsTrue (result1 == 2, "Expected 2."); Assert.IsTrue (result2 == 1, "Expected 1.");
Een aparte test voor de uitzondering moet ook worden geschreven.
Het nemen van toepassingsniveauvereisten die zijn geverifieerd met integratietests en deze heeft teruggebracht tot lagere computationele vereisten, is een belangrijk onderdeel van de algemene teststrategie voor eenheden aangezien het duidelijke rekenvereisten definieert waaraan de toepassing moet voldoen. Als u problemen ondervindt met dit proces, probeert u de vereisten voor de toepassing om te zetten in een van de drie computationele categorieën: datareductie, datatransformatie en statuswijziging.