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.
I samband med enhetstestning har en enhet flera egenskaper.
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:
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 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:
SqlClient
fabriksleverantörsinstans.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.
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.
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:
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.
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.
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".
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.
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.
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:
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.
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.
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 testningVidare, 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.
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.
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.
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.
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:
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.