Enhetsprovning Succinctly Proving Correctness

Detta är ett utdrag från Unit Testing Succinctly eBook, av Marc Clifton, vänligt tillhandahållen av Syncfusion.

Uttrycket "bevisa korrekthet" används normalt i samband med sannolikheten för en beräkning, men med avseende på enhetsprovning har det visat sig att riktigheten faktiskt har tre brett kategorier, varav den andra avser beräkningar själva:

  • Verifiera att ingångar till en beräkning är korrekta (metodkontrakt).
  • Verifiera att ett metodsamtal resulterar i det önskade beräkningsresultatet (kallat beräkningsaspekten), uppdelad i fyra typiska processer:
    • Datatransformation
    • Datareduktion
    • Statlig förändring
    • Statens korrekthet
  • Extern felhantering och återställning.

Det finns många aspekter av en applikation där enhetsprovning vanligen inte kan tillämpas för att bevisa korrekthet. Dessa inkluderar de flesta användargränssnittsfunktioner som layout och användbarhet. I många fall är enhetstestning inte lämplig teknik för testning av krav och tillämpningsbeteende beträffande prestanda, belastning och så vidare.


Hur Enhetstester visar korrekthet

Bevisande korrekthet innebär:

  • Verifiering av kontraktet.
  • Verifiera beräkningsresultat.
  • Verifiering av dataomvandlingsresultat.
  • Verifiering av externa fel hanteras korrekt.

Låt oss titta på några exempel på var och en av dessa kategorier, deras styrkor, svagheter och problem som vi kan stöta på med vår kod.

Bevisligt kontrakt är genomfört

Den mest grundläggande formen av enhetstestning är att verifiera att utvecklaren har skrivit en metod som tydligt anger "kontraktet" mellan den som ringer och den metod som kallas. Detta brukar ske i form av att verifiera att dåliga ingångar till en metod resulterar i att ett undantag kastas. Exempelvis kan en "divide by" -metod kasta en ArgumentOutOfRangeException om nämnaren är 0:

public static int Divide (int täljare, int nämnare) om (nämnare == 0) släng nytt ArgumentOutOfRangeException ("nämnaren kan inte vara 0.");  returräknare / nämnare;  [TestMethod] [ExpectedException (typof (ArgumentOutOfRangeException)) public void BadParameterTest () Divide (5, 0); 

Att verifiera att en metod genomför kontraktskontroller är emellertid en av de svagaste testen man kan skriva.

Bevisa beräkningsresultat

Ett starkare enhetstest innebär att verifiera att beräkningen är korrekt. Det är användbart att kategorisera dina metoder i en av de tre beräkningsformerna:

  • Datareduktion
  • Datatransformation
  • Statlig förändring

Dessa bestämmer vilka typer av enhetstester du kanske vill skriva för en viss metod.

Datareduktion

De Dela upp Metoden i föregående prov kan anses vara en form av datareduktion. Det tar två värden och returnerar ett värde. För att illustrera:

[TestMethod] public void VerifyDivisionTest () Assert.IsTrue (Divide (6, 2) == 3, "6/2 bör vara lika med 3!"); 

Detta illustrerar att man testar en metod som reducerar ingångarna, vanligtvis till en resulterande produktion. Detta är den enklaste formen av användbar enhetstestning.

Datatransformation

Datatransformationsenhetstester tenderar att fungera på uppsättningar av värden. Till exempel är följande ett test för en metod som omvandlar kartesiska koordinater till polarkoordinater.

offentliga statiska dubbla [] ConvertToPolarCoordinates (dubbel x, dubbel y) double dist = Math.Sqrt (x * x + y * y); dubbelvinkel = Math.Atan2 (y, x); returnera ny dubbel [] dist, vinkel;  [TestMethod] public void ConvertToPolarCoordinatesTest () double [] pcoord = ConvertToPolarCoordinates (3, 4); Assert.IsTrue (pcoord [0] == 5, "Förväntat avstånd till lika 5"); Assert.IsTrue (pcoord [1] == 0,92729521800161219, "Förväntad vinkel till 53.130 grader"); 

Detta test verifierar riktigheten av den matematiska omvandlingen.

Lista transformationer

Lista transformationer ska delas in i två tester:

  • Kontrollera att kärntransformationen är korrekt.
  • Verifiera att listoperationen är korrekt.

Från exempel på enhetstestning är följande prov dåligt skrivet eftersom det innehåller såväl datareduktion som datatransformation:

public struct Name public string Förnamn get; uppsättning;  allmän sträng LastName get; uppsättning;  offentlig lista ConcatNames (List namn) Lista concatenatedNames = ny lista(); foreach (namnnamn i namn) concatenatedNames.Add (name.LastName + "," + name.FirstName);  returnera concatenatedNames;  [TestMethod] public void NameConcatenationTest () Lista namn = ny lista() New Name () FirstName = "John", LastName = "Travolta", nytt namn () FirstName = "Allen", LastName = "Nancy"; Lista newNames = ConcatNames (namn); Assert.IsTrue (newNames [0] == "Travolta, John"); Assert.IsTrue (newNames [1] == "Nancy, Allen"); 

Den här koden är bättre enhetstestad genom att separera datareduktionen från datatransformationen:

allmän sträng Concat (Namn) Return name.LastName + "," + name.FirstName;  [TestMethod] public void ContactNameTest () Namn namn = nytt Namn () FirstName = "John", LastName = "Travolta"; sträng concatenatedName = Concat (namn); Assert.IsTrue (concatenatedName == "Travolta, John"); 

Lambda Expressions and Unit Tests

Syntagmen för språkintegrerad sökning (LINQ) är nära kopplad till lambda-uttryck, vilket resulterar i en lättläst syntax som gör livet svårt för enhetstestning. Till exempel, den här koden:

offentlig lista ConcatNamesWithLinq (List namn) return names.Select (t => t.LastName + "," + t.FirstName) .ToList (); 

är betydligt elegantare än de tidigare exemplen, men det låter sig inte vara bra att testa den faktiska "enheten", det vill säga datareduktionen från en namnskonstruktion till en enda kommavalsbegränsad sträng uttryckt i lambda-funktionen t => t.LastName + "," + t.FirstName. För att skilja enheten från listoperationen krävs:

offentlig lista ConcatNamesWithLinq (List namn) return names.Select (t => Concat (t)). ToList (); 

Vi kan se att enhetstestning ofta kan kräva refactoring av koden för att skilja enheterna från andra transformationer.

State Change

De flesta språk är "stateful" och klasser hanterar ofta staten. En klass, som representeras av dess egenskaper, är ofta en bra sak att testa. Tänk på den här klassen som representerar konceptet för en anslutning:

allmän klass AlreadyConnectedToServiceException: ApplicationException public AlreadyConnectedToServiceException (strängmeddelande): bas (msg)  public class ServiceConnection public bool Connected get; skyddad uppsättning;  public void Connect () om (Connected) släng nytt AlreadyConnectedToServiceException ("Endast en anslutning i taget är tillåten.");  // Anslut till tjänsten. Connected = true;  public void Disconnect () // Koppla bort från tjänsten. Connected = false; 

Vi kan skriva enhetstester för att verifiera de olika tillåtna och otillåtna tillstånden för objektet:

[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 = ny ServiceConnection (); conn.Connect (); conn.Disconnect (); Assert.IsFalse (conn.Connected);  [TestMethod] [ExpectedException (typof (AlreadyConnectedToServiceException))] public void TestAlreadyConnectedException () ServiceConnection conn = new ServiceConnection (); conn.Connect (); conn.Connect (); 

Här verifierar varje test korrektheten av objektets tillstånd:

  • När den initialiseras.
  • När du uppmanas att ansluta till tjänsten.
  • När du uppmanas att koppla bort från tjänsten.
  • När mer än en samtidig anslutning försökas.

Statskontroll avslöjar ofta buggar i statsförvaltningen. Se även följande "Mocking Classes" för ytterligare förbättringar av föregående exempelkod.

Bevisa en metod hanterar en extern undantag

Extern felhantering och återställning är ofta viktigare än att testa om din egen kod genererar undantag vid rätt tidpunkt. Det finns flera anledningar till detta:

  • Du har ingen kontroll över ett fysiskt separat beroende, oavsett om det är en webbtjänst, databas eller annan separat server.
  • Du har inget bevis på huruvida någon annans kod är korrekt, vanligtvis ett bibliotek från tredje part.
  • Tjänster och programvara från tredje part kan göra ett undantag på grund av ett problem som din kod skapar men inte upptäcker (och skulle inte nödvändigtvis vara lätt att upptäcka). Ett exempel på detta är att vid borttagning av poster i en databas kastar databasen ett undantag på grund av poster i andra tabeller som refererar till de poster som ditt program raderar och därigenom bryter mot en främmande nyckelbegränsning.

Sådana undantag är svåra att testa eftersom de kräver att åtminstone skapa ett fel som normalt genereras av den tjänst som du inte kontrollerar. Ett sätt att göra detta är att "mocka" tjänsten; Detta är dock endast möjligt om det externa objektet implementeras med ett gränssnitt, en abstrakt klass eller virtuella metoder.

Mocking Classes

Till exempel är den tidigare koden för klassen "ServiceConnection" inte mockable. Om du vill testa sin tillståndshantering måste du fysiskt skapa en anslutning till tjänsten (vad som än är) som kanske inte är tillgänglig när du kör enhetstesterna. En bättre implementering kan se ut så här:

offentlig klass MockableServiceConnection public bool Connected get; skyddad uppsättning;  skyddad virtuell tomgång ConnectToService () // Anslut till tjänsten.  skyddad virtuell tomgång DisconnectFromService () // Koppla bort från tjänsten.  public void Connect () om (Connected) släng nytt AlreadyConnectedToServiceException ("Endast en anslutning i taget är tillåten.");  ConnectToService (); Connected = true;  public void Disconnect () DisconnectFromService (); Connected = false; 

Lägg märke till hur denna mindre refactoring nu låter dig skriva en mockklass:

public class ServiceConnectionMock: MockableServiceConnection protected override void ConnectToService () // Gör ingenting.  skyddad åsidosättningsfel DisconnectFromService () // Gör ingenting. 

vilket gör att du kan skriva ett enhetstest som tester statsledningen oavsett tjänstens tillgänglighet. Såsom detta illustrerar kan även enkla arkitektoniska eller implementeringsändringar förbättra testbarheten för en klass.

Bevisa ett fel är återskapande

Din första försvarskod för att bevisa att problemet har rättats är att det irriterar att problemet finns. Tidigare såg vi ett exempel på att skriva ett test som visade att divide-metoden kontrollerar ett nämnervärde av 0. Låt oss säga att en felrapport är arkiverad eftersom en användare kraschade programmet när de kom in 0 för nämnervärdet.

Negativ testning

Den första affärsordningen är att skapa ett test som utövar detta tillstånd:

[TestMethod] [ExpectedException (typof (DivideByZeroException)) public void BadParameterTest () Divide (5, 0); 

Detta test passerar eftersom vi bevisar att felet existerar genom att verifiera det när nämnaren är 0, en DivideByZeroException är upphöjd. Dessa typer av tester betraktas som "negativa test", som de passera när ett fel uppstår Negativ testning är lika viktig som positiv testning (diskuteras nästa) eftersom det verifierar förekomsten av ett problem innan det korrigeras.

Bevisa ett fel är fast

Självklart vill vi bevisa att en bugg har blivit fixad. Detta är ett "positivt" test.

Positiv testning

Vi kan nu introducera ett nytt test, en som kommer att testa att själva koden upptäcker felet genom att kasta en ArgumentOutOfRangeException.

[TestMethod] [ExpectedException (typof (ArgumentOutOfRangeException)) public void BadParameterTest () Divide (5, 0); 

Om vi ​​kan skriva detta test innan fixa problemet kommer vi att se att testet misslyckas. Slutligen, efter att problemet har fastställts, passerar vårt positiva test, och det negativa testet misslyckas nu.

Även om detta är ett triviellt exempel, visar det två begrepp:

  • Negativa test - som visar att något upprepade gånger inte fungerar - är viktigt för att förstå problemet och lösningen.
  • Positiva tester som visar att problemet har åtgärdats är viktigt, inte bara för att verifiera lösningen, men också för att upprepa testet när en förändring görs. Enhetstestning spelar en viktig roll när det gäller regressionstestning.

Slutligen är det inte alltid lätt att bevisa att det finns en bugg. Som en allmän tumregel är enhetsprovningar som kräver för mycket inställning och mocking en indikator på att koden som testas inte är tillräckligt isolerad från externa beroenden och kan vara en kandidat för refactoring.

Bevis ingenting bröt när du ändrar kod

Det bör vara uppenbart att regressionstestning är ett mätbart användbart resultat av enhetsprovning. Som kod genomgår ändringar, kommer buggar att introduceras som kommer att avslöjas om du har bra koddekning i dina testsatser. Detta sparar mycket tid i felsökning och, viktigare, sparar tid och pengar när programmeraren upptäcker buggen i stället för användaren.

Beviskraven är uppfyllda

Applikationsutveckling börjar typiskt med en hög uppsättning krav, vanligtvis orienterad kring användargränssnittet, arbetsflödet och beräkningarna. Idealt reducerar teamet synlig uppsättning krav ned till en uppsättning programmatiska krav, som är osynlig till användaren, av sin natur.

Skillnaden manifesterar sig i hur programmet testas. Integrationstestning är typiskt vid synlig nivå, medan enhetsprovning är vid det finare kornet av osynlig, programmatisk korrekthetstestning. Det är viktigt att komma ihåg att enhetsprov inte är avsedda att ersätta integrationstestning. Men precis som med ansökningar på hög nivå finns det låga programmatiska krav som kan definieras. På grund av dessa programmatiska krav är det viktigt att skriva enhetsprov.

Låt oss ta en rund metod. .NET Math.Round-metoden kommer att runda upp ett nummer vars fraktionskomponent är större än 0,5, men kommer att runda ner när fraktionskomponenten är 0,5 eller mindre. Låt oss säga att det inte är det beteende vi önskar (av vilken anledning som helst), och vi vill runda upp när fraktionskomponenten är 0,5 eller högre. Detta är ett beräkningsbehov som borde kunna härledas från ett högre krav på integration, vilket resulterar i följande metod och test:

statisk statisk int RoundUpHalf (double n) if (n < 0) throw new ArgumentOutOfRangeException("Value must be >= 0), int ret = (int) n, dubbel fraktion = n-ret, om (fraktion> = 0.5) ++ ret; Retur; [TestMethod] public void RoundUpTest () int result1 = RoundUpHalf (1.5); int result2 = RoundUpHalf (1.499999); Assert.IsTrue (result1 == 2, "Expected 2."); Assert.IsTrue (result2 == 1, "Expected 1.");

Ett separat test för undantaget bör också skrivas.

Att ta krav på applikationsnivå som verifieras med integrationsprovning och minska dem till lägre beräkningskrav är en viktig del av den övergripande enhetsteststrategin, eftersom den definierar tydliga beräkningskrav som ansökan måste uppfylla. Om det uppstår problem med denna process, försök att konvertera applikationskraven till en av de tre beräkningskategorierna: datareduktion, datatransformation och tillståndsändring.