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:
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.
Bevisande korrekthet innebär:
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.
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.
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:
Dessa bestämmer vilka typer av enhetstester du kanske vill skriva för en viss metod.
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.
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 ska delas in i två tester:
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 listaConcatNames (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");
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 listaConcatNamesWithLinq (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 listaConcatNamesWithLinq (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.
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:
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.
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:
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.
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.
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.
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.
Självklart vill vi bevisa att en bugg har blivit fixad. Detta är ett "positivt" test.
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:
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.
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.
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.