Enhetsprovning Succinctly Vad är Unit Testing?

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

Enhetstest handlar om att bevisa korrekthet. För att bevisa att något fungerar korrekt måste du förstå vad båda enhet och a testa Egentligen är det innan du kan utforska vad som är provbar inom förmågan hos enhetsprovning.

Vad är en enhet?

I samband med enhetstestning har en enhet flera egenskaper.

Rena enheter

En ren enhet är den enklaste och mest idealiska metoden för att skriva ett test. En ren enhet har flera egenskaper som underlättar lätt testning.

En enhet borde (helst) inte ringa andra metoder

När det gäller enhetsprovning bör en enhet först och främst vara en metod som gör något utan att kalla några andra metoder. Exempel på dessa rena enheter finns i Sträng och Matematik klasser-de flesta av de utförda åtgärderna litar inte på någon annan metod. Till exempel följande kod (taget från något som författaren har skrivit)

public void SelectedMasters () string currentEntity = dgvModel.DataMember; sträng navToEntity = cbMasterTables.SelectedItem.ToString (); DataGridViewSelectedRowCollection selectedRows = dgvModel.SelectedRows; StringBuilder qualifier = BuildQualifier (selectedRows); UpdateGrid (navToEntity); SetRowFilter (navToEntity, qualifier.ToString ()); ShowNavigateToMaster (navToEntity, qualifier.ToString ()); 

bör inte betraktas som en enhet av tre skäl:

  • I stället för att ta parametrar, erhåller de värden som är inblandade i beräkningen från användargränssnittobjekt, specifikt en DataGridView och en ComboBox.
  • Det gör flera samtal till andra metoder som potentiellt är enheter.
  • En av metoderna verkar uppdatera visningen, förknippas med en beräkning med en visualisering.

Den första anledningen pekar på en subtil problem-egenskaper borde betraktas som metodsamtal. Faktum är att de ligger i det underliggande genomförandet. Om din metod använder egenskaper hos andra klasser, är detta ett slags metodsamtal och bör övervägas noggrant när du skriver en lämplig enhet.

Realistiskt är detta inte alltid möjligt. Ofta krävs ett samtal till ramen eller något annat API för att enheten ska kunna utföra sitt arbete. Dessa samtal bör dock inspekteras för att avgöra huruvida metoden kan förbättras för att skapa en renare enhet, exempelvis genom att extrahera samtalen till en högre metod och överföra resultaten av samtalen som en parameter till enheten.

En enhet borde bara göra en sak

En följd till "en enhet ska inte kalla andra metoder" är att en enhet är en metod som gör en sak och bara en sak. Ofta kallas andra metoder för att göra mer än en sak-en värdefull färdighet att veta när någonting faktiskt består av flera deltakar - även om det kan beskrivas som en uppgift på hög nivå vilket gör att det låter som en enda uppgift!

Följande kod kan se ut som en rimlig enhet som gör en sak: det infogar ett namn i databasen.

public int Insert (Person person) DbProviderFactory factory = SqlClientFactory.Instance; använder (DbConnection connection = factory.CreateConnection ()) connection.ConnectionString = "Server = localhost; Databas = myDataBase; Trusted_Connection = True;"; connection.Open (); använder (DbCommand command = connection.CreateCommand ()) command.CommandText = "infoga i PERSON (ID, NAME) värden (@Id, @Name)"; command.CommandType = CommandType.Text; DbParameter id = command.CreateParameter (); id.ParameterName = "@Id"; id.DbType = DbType.Int32; id.Value = person.Id; DbParameter name = command.CreateParameter (); name.ParameterName = "@Name"; name.DbType = DbType.String; namn.Size = 50; name.Value = person.Name; command.Parameters.AddRange (ny DbParameter [] id, namn); int raderAffected = command.ExecuteNonQuery (); returnera raderAffected; 

Men den här koden gör faktiskt flera saker:

  • Skaffa en SqlClient fabriksleverantörsinstans.
  • Instansera en anslutning och öppna den.
  • Instantiera ett kommando och initiera kommandot.
  • Skapa och lägga till två parametrar till kommandot.
  • Utför kommandot och returnera antalet rader som berörs.

Det finns en mängd problem med den här koden som diskvalificerar den från att vara en enhet och gör det svårt att minska till grundläggande enheter. Ett bättre sätt att skriva den här koden kan se ut så här:

public int RefactoredInsert (Person person) DbProviderFactory factory = SqlClientFactory.Instance; använder (DbConnection conn = OpenConnection (fabriks, "Server = localhost; Database = myDataBase; Trusted_Connection = True;")) använd (DbCommand cmd = CreateTextCommand (conn, "sätt in i PERSON (ID, NAME) Namn) ")) AddParameter (cmd," @Id ", person.Id); AddParameter (cmd, "@Name", 50, person.Name); int raderAffected = cmd.ExecuteNonQuery (); returnera raderAffected;  skyddad DbConnection OpenConnection (DbProviderFactory fabrik, string connectString) DbConnection conn = factory.CreateConnection (); conn.ConnectionString = connectString; conn.Open (); returnera conn;  skyddad DbCommand CreateTextCommand (DbConnection conn, sträng cmdText) DbCommand cmd = conn.CreateCommand (); cmd.CommandText = cmdText; cmd.CommandType = CommandType.Text; returnera cmd;  skyddad tomt AddParameter (DbCommand cmd, sträng paramName, int paramValue) DbParameter param = cmd.CreateParameter (); param.ParameterName = paramName; param.DbType = DbType.Int32; param.Value = paramValue; cmd.Parameters.Add (param);  skyddad tomt AddParameter (DbCommand cmd, sträng paramName, int storlek, sträng paramValue) DbParameter param = cmd.CreateParameter (); param.ParameterName = paramName; param.DbType = DbType.String; param.Size = storlek; param.Value = paramValue; cmd.Parameters.Add (param); 

Lägg märke till hur, förutom att se renare, metoderna OpenConnection, CreateTextCommand, och AddParameter är mer lämpade för enhetsprovning (ignorerar det faktum att de är skyddade metoder). Dessa metoder kan bara en sak och, som enheter, testas för att säkerställa att de gör den där saken korrekt. Härav blir det liten punkt att testa RefactoredInsert metod, eftersom den helt och hållet bygger på andra funktioner som har enhetstester. I bästa fall kan man skriva några undantagshanteringsprovfall, och eventuellt någon validering på fälten i Person tabell.

Provbart korrekt kod

Vad händer om metoden på högre nivå gör något mer än bara kalla andra metoder för vilka det finns enhetstester, säg, någon form av ytterligare beräkning? I så fall ska koden som utför beräkningen flyttas till sin egen metod, test ska skrivas för den, och igen kan metoden på högre nivå förlita sig på korrektheten av koden som den ringer. Detta är processen att konstruera korrekt korrekt kod. Korrektheten på metoder på högre nivå förbättras när allt de gör är att ringa metoder på lägre nivå som har bevis (enhetstest) av korrekthet.

En enhet borde inte (helst) ha flera kodvägar

Cyklomatisk komplexitet är banan för enhetstestning och applikationstestning i allmänhet, eftersom det ökar svårigheten att testa alla kodvägar. Ideellt sett kommer en enhet inte ha någon om eller växla uttalanden. Kroppen av dessa uttalanden bör betraktas som enheterna (förutsatt att de uppfyller de andra kriterierna för en enhet) och att kunna testas, bör utvinnas i sina egna metoder.

Här är ett annat exempel taget från författarens MyXaml-projekt (del av parsern):

om tagName == "*") foreach (XmlNode nod i topElement.ChildNodes) om (! (nod är XmlComment)) objectNode = nod; ha sönder;  foreach (XmlAttribute attr i objectNode.Attributes) if (attr.LocalName == "Name") nameAttr = attr; ha sönder;  annat ... etc ...

Här har vi flera kodvägar som involverar om, annan, och för varje uttalanden som:

  • Skapa installationskomplexitet, eftersom många villkor måste uppfyllas för att exekvera den inre koden.
  • Skapa testkomplexitet, eftersom kodvägarna kräver olika inställningar för att säkerställa att varje kodväg testas.

Det är självklart att villkorlig förgrening, slingor, fallutlåtanden etc. inte kan undvikas, men det kan vara värt att överväga att refactorera koden så att förhållandena och slingorna är separata metoder som kan testas självständigt. Då kan testen för högre nivåmetoden helt enkelt se till att staterna (representerade av förhållanden, loopar, växlar, etc.) hanteras korrekt, oberoende av de beräkningar de utför.

Beroende enheter

Metoder som har beroenden på andra klasser, data och statsinformation är mer komplexa att testa eftersom dessa beroenden översätter till krav för instantierade objekt, existens av data och förutbestämt tillstånd.

förutsättningar

I sin enklaste form har beroende enheter förutsättningar som måste uppfyllas. Enhetstestmotorer tillhandahåller mekanismer för att ompröva testberoende, både för individuella test och för alla test inom en testgrupp, eller "fixtur".

Faktiska eller simulerade tjänster

Komplicerade beroende enheter kräver tjänster som databasanslutningar som ska instanseras eller simuleras. I det tidigare kodexemplet Föra in Metoden kan inte testas utan att kunna ansluta till en faktisk databas. Denna kod blir mer testbar om databasinteraktionen kan simuleras, typiskt genom användningen av gränssnitt eller basklasser (abstrakt eller ej).

De refactored metoderna i Föra in kod som beskrivits tidigare är ett bra exempel eftersom DbProviderFactory är en abstrakt basklass, så man kan enkelt skapa en klass som härrör från DbProviderFactory för att simulera databasanslutningen.

Hantering av externa undantag

Beroende enheter, eftersom de ringer till andra API eller metoder, är också mer ömtåliga - de kan behöva explicit hantera fel som potentiellt genereras av de metoder som de ringer. I det tidigare kodprovet, den Föra in Metodens kod kan vikas in i ett provfångsblock eftersom det är säkert möjligt att databasanslutningen inte existerar. Undantagsbehandlaren kan returnera 0 för antalet rader som berörs, rapportering av felet genom någon annan mekanism. I ett sådant scenario måste enhetstesterna kunna simulera detta undantag för att säkerställa att alla kodvägar exekveras korrekt, inklusive fånga och till sist block.


Vad är ett test?

Ett test ger en användbar påstående om enhetens korrekthet. Tester som hävdar att en enhet är korrekt utövar vanligtvis enheten på två sätt:

  • Testa hur enheten fungerar under normala förhållanden.
  • Testa hur enheten fungerar under onormala förhållanden.

Normala test

Att testa hur enheten fungerar under normala förhållanden är överlägset det enklaste testet att skriva. När vi skriver en funktion skriver vi trots allt att det är ett uttryckligt eller implicit krav. Genomförandet återspeglar en förståelse av det kravet, vilket delvis omfattar vad vi förväntar oss som inmatningar till funktionen och hur vi förväntar oss att funktionen ska fungera med dessa insatser. Därför testar vi resultatet av funktionen som förväntade inmatningar, om resultatet av funktionen är ett returvärde eller en statlig förändring. Om enheten är beroende av andra funktioner eller tjänster, förväntar vi oss dessutom att de ska fungera korrekt och skriver ett test med det underförstådda antagandet.

Test av onormala förhållanden

Att testa hur enheten fungerar under onormala förhållanden är mycket svårare. Det måste bestämma vad ett onormalt tillstånd är, vilket vanligtvis inte är uppenbart genom att inspektera koden. Detta görs mer komplicerat när du testar en beroende enhet, en enhet som förväntar sig en annan funktion eller tjänst för att fungera korrekt. Dessutom vet vi inte hur en annan programmerare eller användare kan utöva enheten.


Enhetstester och andra testningspraxis

Enhetsprovning som en del av en omfattande testmetod

Enhetstest ersätter inte andra testmetoder. Det bör komplettera andra testmetoder, vilket ger ytterligare dokumentationsstöd och förtroende. Figur 1 illustrerar ett koncept av "applikationsutvecklingsflöde" - hur annan test integreras med enhetsprovning. Observera att kunden kan vara inblandad i något skede, men vanligtvis vid acceptansproceduren (ATP), systemintegration och användningsfaser.

Jämför detta med V-modellen för mjukvaruutveckling och testprocess. Medan det är relaterat till vattenfallsmodellen för mjukvaruutveckling (som i slutändan alla andra mjukvaruutvecklingsmodeller antingen är en delmängd eller en förlängning av), ger V-modellen en bra bild av vilken typ av provning som krävs för varje lager av mjukvaruutvecklingsprocessen:

V-modellen för testning

Vidare, när en testpunkt misslyckas i någon annan testutövning, kan en viss koddel vanligen identifieras som ansvarig för felet. När så är fallet blir det möjligt att behandla den delkoden som en enhet och skriva ett test för att först skapa felet och, när koden har ändrats, för att verifiera fixen.

Acceptanstestförfaranden

Ett godkännande testförfarande (ATP) används ofta som ett kontraktsmässigt krav för att bevisa att viss funktionalitet har implementerats. ATP-föreningar är ofta förknippade med milstolpar, och milstolpar är ofta förknippade med betalningar eller ytterligare projektfinansiering. En ATP skiljer sig från ett enhetstest eftersom ATP visar att funktionaliteten med avseende på hela linjeposten har implementerats. Ett enhetstest kan till exempel avgöra om beräkningen är korrekt. ATP kan dock validera att användarelementen tillhandahålls i användargränssnittet och att användargränssnittet visar resultatet av beräkningen enligt kravet. Dessa krav omfattas inte av enhetstestet.

Automatiserad användargränsstestning

En ATP kan initialt skrivas som en serie användargränssnitt (UI) -interaktioner för att verifiera att kraven har uppfyllts. Regressionstestning av applikationen eftersom den fortsätter att utvecklas är tillämplig på provning av enheter samt accepttestning. Automatiserad användargränssnittstestning är ett annat verktyg som är helt skilt från enhetstestning som sparar tid och arbetskraft samtidigt som testfel minskas. Som med ATP-enheter ersätter enhetstesterna inte på något sätt värdet av automatiserade användargränssnittstest.

Användbarhet och användarupplevelse testning

Enhetstest, ATP, och automatiserade användargruppprov ersätter inte på något sätt användbarhetsprovning - sätter applikationen framför användarna och får sin "användarupplevelse" -återkoppling. Användbarhetsprovning bör inte handla om att hitta beräkningsfel (buggar) och är därför helt utanför utförandet av enhetstester.

Prestanda och belastningstestning

Vissa enhetstestverktyg ger ett medel för att mäta prestanda för en metod. Exempelvis rapporterar Visual Studio testmotor om körtiden och NUnit har attribut som kan användas för att verifiera att en metod körs inom en tilldelad tid.

Helst bör ett testverktyg för .NET-språk uttryckligen genomföra prestandatest för att kompensera för just-in-time (JIT) kodsamling första gången koden exekveras.

De flesta belastningstesterna (och de relaterade prestandatesterna) är inte lämpliga för enhetsprov. Vissa typer av belastningstest kan också göras med enhetstestning, åtminstone till begränsningarna i hårdvaran och operativsystemet, såsom:

  • Simulera minnesbegränsningar.
  • Simulera resursbegränsningar.

Dessa typer av test kräver dock idealiskt stöd från ramverket eller OS API för att simulera dessa typer av belastningar för att programmet ska testas. Att tvinga hela operativsystemet att konsumera en stor mängd minne, resurser eller båda påverkar alla program, inklusive enhetstestprogrammet. Detta är inte ett önskvärt tillvägagångssätt.

Andra typer av belastningstestning, till exempel simulering av flera instanser att köra en operation samtidigt, är inte kandidater för enhetstestning. Exempelvis är det förmodligen inte möjligt att testa prestanda för en webbtjänst med en miljon transaktioner per minut med en enda maskin. Medan denna typ av test enkelt kan skrivas som en enhet, skulle det aktuella testet innebära en serie testmaskiner. Och i slutändan har du bara testat ett mycket smalt beteende hos webbtjänsten under mycket specifika nätverksförhållanden, vilket inte på något sätt representerar den verkliga världen.

Av denna anledning har prestandatest och belastningstest begränsad tillämpning med enhetstestning.