Anteckning
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
Det finns många fördelar med att skriva enhetstester. De hjälper till med regression, tillhandahåller dokumentation och underlättar god design. Men när enhetstester är svåra att läsa och spröda kan de orsaka förödelse på din kodbas. I den här artikeln beskrivs några metodtips för att utforma enhetstester för att stödja dina .NET Core- och .NET Standard-projekt. Du lär dig tekniker för att hålla dina tester motståndskraftiga och lätta att förstå.
Av John Reese med särskilt tack till Roy Osherove
Fördelar med enhetstestning
I följande avsnitt beskrivs flera orsaker till att skriva enhetstester för dina .NET Core- och .NET Standard-projekt.
Mindre tid att utföra funktionella tester
Funktionella tester är dyra. De innebär vanligtvis att öppna programmet och utföra en serie steg som du (eller någon annan) måste följa för att verifiera det förväntade beteendet. De här stegen kanske inte alltid är kända för testaren. De måste kontakta någon som är mer kunnig i området för att utföra testet. Att testa sig själv kan ta sekunder för triviala ändringar eller minuter för större ändringar. Slutligen måste den här processen upprepas för varje ändring som du gör i systemet. Enhetstester, å andra sidan, tar millisekunder, kan köras med en knapptryckning och kräver inte nödvändigtvis någon kunskap om systemet i stort. Testlöparen avgör om testet klarar eller misslyckas, inte individen.
Skydd mot regression
Regressionsfel är fel som introduceras när en ändring görs i programmet. Det är vanligt att testare inte bara testar sin nya funktion utan även testfunktioner som fanns i förväg för att verifiera att befintliga funktioner fortfarande fungerar som förväntat. Med enhetstestning kan du köra hela testpaketet igen efter varje version eller till och med efter att du har ändrat en kodrad. Den här metoden hjälper till att öka förtroendet för att den nya koden inte bryter mot befintliga funktioner.
Exekverbar dokumentation
Det kanske inte alltid är uppenbart vad en viss metod gör eller hur den beter sig med en viss indata. Du kan fråga dig själv: Hur fungerar den här metoden om jag skickar en tom sträng eller null? När du har en uppsättning väl namngivna enhetstester bör varje test tydligt förklara de förväntade utdata för en viss indata. Dessutom bör testet kunna verifiera att det faktiskt fungerar.
Mindre kopplad kod
När koden är nära kopplad kan det vara svårt att enhetstesta. Utan att skapa enhetstester för den kod som du skriver kan kopplingen vara mindre uppenbar. Att skriva tester för din kod gör att den frikopplas naturligt eftersom det annars är svårare att testa.
Egenskaper för bra enhetstester
Det finns flera viktiga egenskaper som definierar ett bra enhetstest:
- Snabb: Det är inte ovanligt att mogna projekt har tusentals enhetstester. Enhetstester bör ta kort tid att köra. Millisekunder.
- Isolerad: Enhetstester är fristående, kan köras isolerat och har inga beroenden för externa faktorer, till exempel ett filsystem eller en databas.
- Repeterbar: Att köra ett enhetstest bör alltid ge konsekventa resultat. Testet returnerar alltid samma resultat om du inte ändrar något mellan körningarna.
- självkontroll: Testet bör automatiskt identifiera om det har godkänts eller misslyckats utan någon mänsklig interaktion.
- : Ett enhetstest bör inte ta oproportionerligt lång tid att skriva jämfört med koden som testas. Om du upptäcker att det tar lång tid att testa koden jämfört med att skriva koden bör du överväga en mer testbar design.
Kodtäckning och kodkvalitet
En hög kodtäckningsprocent associeras ofta med en högre kodkvalitet. Själva mätningen kan dock inte fastställa kodens kvalitet. Att ange ett alltför ambitiöst mål för kodtäckningsprocent kan vara kontraproduktivt. Överväg ett komplext projekt med tusentals villkorsstyrda grenar och anta att du anger ett mål på 95% kodtäckning. Projektet har för närvarande 90 % kodtäckning med%. Den tid det tar att ta hänsyn till alla gränsfall i de återstående 5% kan vara ett massivt åtagande, och värdeförslaget minskar snabbt.
En hög kodtäckningsprocent är inte en indikator på framgång, och det innebär inte hög kodkvalitet. Den representerar bara mängden kod som omfattas av enhetstester. För mer information, se avsnittet om kodtäckning i enhetstestning .
Terminologi för enhetstestning
Flera termer används ofta i samband med enhetstestning: "fake", "mock"och "stub". Dessa termer kan tyvärr tillämpas felaktigt, så det är viktigt att du förstår rätt användning.
Fake: En fake är en generisk term som kan användas för att beskriva antingen en stub eller ett mock-objekt. Om objektet är en stub eller en mock beror på kontexten där objektet används. Med andra ord kan en falsk vara en stub eller en mock.
Mock: Ett mock-objekt är ett falskt objekt i systemet som avgör om ett enhetstest godkänns eller misslyckas. En mock börjar som en avbild och förblir en avbild tills den träder in i en
Assert
-operation.Stub: En stub är en kontrollbar ersättning för ett befintligt beroende (eller medarbetare) i systemet. Genom att använda en stub kan du testa koden utan att hantera beroendet direkt. Som standardinställning börjar en stub som en fejk.
Överväg följande kod:
var mockOrder = new MockOrder();
var purchase = new Purchase(mockOrder);
purchase.ValidateOrders();
Assert.True(purchase.CanBeShipped);
Den här koden visar en stub som kallas för ett mock-objekt. Men i det här scenariot är stubben verkligen en stubb. Syftet med koden är att skicka ordningen som ett sätt att instansiera objektet Purchase
(systemet under test). Klassnamnet MockOrder
är missvisande eftersom ordern är en stub och inte en mock.
Följande kod visar en mer exakt design:
var stubOrder = new FakeOrder();
var purchase = new Purchase(stubOrder);
purchase.ValidateOrders();
Assert.True(purchase.CanBeShipped);
När klassen har bytt namn till FakeOrder
är klassen mer allmän. Klassen kan användas som ett mockobjekt eller en stub, enligt kraven i testfallet. I det första exemplet används klassen FakeOrder
som en stub och används inte under den Assert
åtgärden. Koden skickar klassen FakeOrder
till klassen Purchase
bara för att uppfylla konstruktorns krav.
Om du vill använda klassen som ett hån kan du uppdatera koden:
var mockOrder = new FakeOrder();
var purchase = new Purchase(mockOrder);
purchase.ValidateOrders();
Assert.True(mockOrder.Validated);
I den här designen kontrollerar koden en egenskap på den falska (hävdar mot den), och därför är klassen mockOrder
ett hån.
Viktigt!
Det är viktigt att implementera terminologin korrekt. Om du kallar dina stubs för "hån" kommer andra utvecklare att göra falska antaganden om din avsikt.
Det viktigaste att komma ihåg om mockar jämfört med stubs är att mockar är precis som stubs, förutom Assert
-processen. Du kör Assert
-operationerna mot ett mockobjekt, men inte mot en stub.
Metodtips
Det finns flera viktiga metodtips att följa när du skriver enhetstester. Följande avsnitt innehåller exempel som visar hur du tillämpar metodtipsen för din kod.
Undvik beroenden utan infrastruktur
Försök att inte införa beroenden för infrastrukturen när du skriver enhetstester. Beroendena gör testerna långsamma och spröda och bör reserveras för integreringstester. Du kan undvika dessa beroenden i ditt program genom att följa explicita beroendeprincipen och med hjälp av .NET-beroendeinmatning. Du kan också hålla enhetstesterna i ett separat projekt från dina integreringstester. Den här metoden säkerställer att enhetstestprojektet inte har referenser till eller beroenden för infrastrukturpaket.
Följ namngivningsstandarderna för test
Namnet på testet bör bestå av tre delar:
- Namnet på den metod som testas
- Scenario där metoden testas
- Förväntat beteende när scenariot anropas
Namngivningsstandarder är viktiga eftersom de bidrar till att uttrycka testsyftet och programmet. Tester är mer än att bara se till att koden fungerar. De tillhandahåller även dokumentation. Bara genom att titta på sviten med enhetstester bör du kunna härleda beteendet för din kod och inte behöva titta på själva koden. När testerna misslyckas kan du dessutom se exakt vilka scenarier som inte uppfyller dina förväntningar.
original kod
[Fact]
public void Test_Single()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("0");
Assert.Equal(0, actual);
}
Använd bästa praxis
[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("0");
Assert.Equal(0, actual);
}
Ordna dina tester
Mönstret "Ordna, agera, hävda" är en vanlig metod för att skriva enhetstester. Som namnet antyder består mönstret av tre huvuduppgifter:
- Ordna dina objekt, skapa och konfigurera dem efter behov
- Act på ett föremål
- Bekräfta att något är som förväntat
När du följer mönstret kan du tydligt skilja det som testas från aktiviteterna Ordna och Bekräfta. Mönstret bidrar också till att minska möjligheten för påståenden att blandas med kod i Act-uppgiften.
Läsbarhet är en av de viktigaste aspekterna när du skriver ett enhetstest. Om du separerar varje mönsteråtgärd i testet markeras tydligt de beroenden som krävs för att anropa koden, hur koden anropas och vad du försöker hävda. Även om det kan vara möjligt att kombinera vissa steg och minska teststorleken är det övergripande målet att göra testet så läsbart som möjligt.
original kod
[Fact]
public void Add_EmptyString_ReturnsZero()
{
// Arrange
var stringCalculator = new StringCalculator();
// Assert
Assert.Equal(0, stringCalculator.Add(""));
}
Använd bästa praxis
[Fact]
public void Add_EmptyString_ReturnsZero()
{
// Arrange
var stringCalculator = new StringCalculator();
// Act
var actual = stringCalculator.Add("");
// Assert
Assert.Equal(0, actual);
}
Skriv minimalt klara tester
Indata för ett enhetstest bör vara den enklaste information som behövs för att verifiera det beteende som du testar för närvarande. Den minimalistiska metoden hjälper tester att bli mer motståndskraftiga mot framtida ändringar i kodbasen och fokusera på att verifiera beteendet under implementeringen.
Tester som innehåller mer information än vad som krävs för att klara det aktuella testet har större chans att införa fel i testet och kan göra avsikten med testet mindre tydlig. När du skriver tester vill du fokusera på beteendet. Om du ställer in extra egenskaper för modeller eller använder icke-nollvärden när det inte behövs, minskar det bara det du försöker bekräfta.
original kod
[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("42");
Assert.Equal(42, actual);
}
Använd bästa praxis
[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("0");
Assert.Equal(0, actual);
}
Undvik magiska strängar
Magic-strängar är strängvärden som hårdkodas direkt i enhetstesterna utan extra kodkommentare eller kontext. Dessa värden gör koden mindre läsbar och svårare att underhålla. Magiska strängar kan orsaka förvirring för läsaren av dina tester. Om en sträng ser ut utöver det vanliga kanske de undrar varför ett visst värde valdes för en parameter eller ett returvärde. Den här typen av strängvärde kan leda till att de tar en närmare titt på implementeringsinformationen i stället för att fokusera på testet.
Tips
Sätt som mål att uttrycka så mycket avsikt som möjligt i enhetstestkoden. I stället för att använda magiska strängar tilldelar du alla hårdkodade värden till konstanter.
original kod
[Fact]
public void Add_BigNumber_ThrowsException()
{
var stringCalculator = new StringCalculator();
Action actual = () => stringCalculator.Add("1001");
Assert.Throws<OverflowException>(actual);
}
Använd bästa praxis
[Fact]
void Add_MaximumSumResult_ThrowsOverflowException()
{
var stringCalculator = new StringCalculator();
const string MAXIMUM_RESULT = "1001";
Action actual = () => stringCalculator.Add(MAXIMUM_RESULT);
Assert.Throws<OverflowException>(actual);
}
Undvik kodningslogik i enhetstester
När du skriver enhetstesterna undviker du manuell strängsammanfogning, logiska villkor, till exempel if
, while
, for
och switch
och andra villkor. Om du inkluderar logik i din testsvit ökar risken för att buggar introduceras dramatiskt. Det sista stället där du vill hitta en bugg finns i din testsvit. Du bör ha hög förtroende för att dina tester fungerar, annars kan du inte lita på dem. Tester som du inte litar på, ger inget värde. När ett test misslyckas vill du ha en känsla av att något är fel med din kod och att det inte kan ignoreras.
Tips
Om det verkar oundvikligt att lägga till logik i testet kan du överväga att dela upp testet i två eller flera olika tester för att begränsa logikkraven.
original kod
[Fact]
public void Add_MultipleNumbers_ReturnsCorrectResults()
{
var stringCalculator = new StringCalculator();
var expected = 0;
var testCases = new[]
{
"0,0,0",
"0,1,2",
"1,2,3"
};
foreach (var test in testCases)
{
Assert.Equal(expected, stringCalculator.Add(test));
expected += 3;
}
}
Använd bästa praxis
[Theory]
[InlineData("0,0,0", 0)]
[InlineData("0,1,2", 3)]
[InlineData("1,2,3", 6)]
public void Add_MultipleNumbers_ReturnsSumOfNumbers(string input, int expected)
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add(input);
Assert.Equal(expected, actual);
}
Använda hjälpmetoder i stället för installation och nedtagning
Om du behöver ett liknande objekt eller tillstånd för dina tester, använd en hjälpmetod i stället för att Setup
- och Teardown
-attributen om de finns. Hjälpmetoder föredras framför dessa attribut av flera skäl:
- Mindre förvirring vid läsning av testerna eftersom all kod är synlig inifrån varje test
- Mindre chans att ställa in för mycket eller för lite för det angivna testet
- Mindre chans att dela tillstånd mellan tester, vilket skapar oönskade beroenden mellan dem
I enhetstestningsramverk anropas attributet Setup
före varje enhetstest i testpaketet. Vissa programmerare ser det här beteendet som användbart, men det resulterar ofta i uppsvällda och svåra att läsa tester. Varje test har vanligtvis olika krav för installation och körning. Tyvärr tvingar attributet Setup
dig att använda exakt samma krav för varje test.
Anmärkning
Attributen SetUp
och TearDown
tas bort i xUnit version 2.x och senare.
original kod
Använd bästa praxis
private readonly StringCalculator stringCalculator;
public StringCalculatorTests()
{
stringCalculator = new StringCalculator();
}
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
var stringCalculator = CreateDefaultStringCalculator();
var actual = stringCalculator.Add("0,1");
Assert.Equal(1, actual);
}
// More tests...
// More tests...
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
var result = stringCalculator.Add("0,1");
Assert.Equal(1, result);
}
private StringCalculator CreateDefaultStringCalculator()
{
return new StringCalculator();
}
Undvik flera act-uppgifter
När du skriver dina tester kan du bara försöka inkludera en Act-uppgift per test. Några vanliga metoder för att implementera en enskild Act-uppgift är att skapa ett separat test för varje Act eller använda parametriserade tester. Det finns flera fördelar med att använda en enda act-uppgift för varje test:
- Du kan enkelt urskilja vilken Act-uppgift som misslyckas om testet misslyckas.
- Du kan se till att testet bara fokuserar på ett enskilt fall.
- Du får en tydlig bild av varför dina tester misslyckas.
Flera Act-uppgifter måste bekräftas individuellt, och du kan inte garantera att alla Assert-uppgifter körs. I de flesta enhetstestningsramverk betraktas alla efterföljande tester automatiskt som misslyckade när en Assert-uppgift misslyckas i ett enhetstest. Processen kan vara förvirrande eftersom vissa arbetsfunktioner kan tolkas som misslyckade.
original kod
[Fact]
public void Add_EmptyEntries_ShouldBeTreatedAsZero()
{
// Act
var actual1 = stringCalculator.Add("");
var actual2 = stringCalculator.Add(",");
// Assert
Assert.Equal(0, actual1);
Assert.Equal(0, actual2);
}
Använd bästa praxis
[Theory]
[InlineData("", 0)]
[InlineData(",", 0)]
public void Add_EmptyEntries_ShouldBeTreatedAsZero(string input, int expected)
{
// Arrange
var stringCalculator = new StringCalculator();
// Act
var actual = stringCalculator.Add(input);
// Assert
Assert.Equal(expected, actual);
}
Verifiera privata metoder med offentliga metoder
I de flesta fall behöver du inte testa en privat metod i koden. Privata metoder är en implementeringsdetalj och finns aldrig isolerade. Någon gång i utvecklingsprocessen introducerar du en offentlig metod för att anropa den privata metoden som en del av implementeringen. När du skriver dina enhetstester, är slutresultatet för den offentliga metoden som kallar på den privata metoden det som du bryr dig om.
Tänk dig följande kodscenario:
public string ParseLogLine(string input)
{
var sanitizedInput = TrimInput(input);
return sanitizedInput;
}
private string TrimInput(string input)
{
return input.Trim();
}
När det gäller testning kan din första reaktion vara att skriva ett test för TrimInput
-metoden för att säkerställa att den fungerar som förväntat. Det är dock möjligt att metoden ParseLogLine
manipulerar sanitizedInput
-objektet på ett sätt som du inte förväntar dig. Det okända beteendet kan göra testet mot TrimInput
-metoden värdelöst.
Ett bättre test i det här scenariot är att verifiera den offentliga ParseLogLine
-metoden:
public void ParseLogLine_StartsAndEndsWithSpace_ReturnsTrimmedResult()
{
var parser = new Parser();
var result = parser.ParseLogLine(" a ");
Assert.Equals("a", result);
}
När du stöter på en privat metod letar du upp den offentliga metoden som anropar den privata metoden och skriver dina tester mot den offentliga metoden. Bara för att en privat metod returnerar ett förväntat resultat betyder det inte att systemet som så småningom anropar den privata metoden använder resultatet korrekt.
Hantera statiska stub-referenser med sömmar
En princip för ett enhetstest är att det måste ha fullständig kontroll över systemet under testning. Den här principen kan dock vara problematisk när produktionskoden innehåller anrop till statiska referenser (till exempel DateTime.Now
).
Granska följande kodscenario:
public int GetDiscountedPrice(int price)
{
if (DateTime.Now.DayOfWeek == DayOfWeek.Tuesday)
{
return price / 2;
}
else
{
return price;
}
}
Kan du skriva ett enhetstest för den här koden? Du kan prova att köra en Assert-uppgift på price
:
public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
{
var priceCalculator = new PriceCalculator();
var actual = priceCalculator.GetDiscountedPrice(2);
Assert.Equals(2, actual)
}
public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
var priceCalculator = new PriceCalculator();
var actual = priceCalculator.GetDiscountedPrice(2);
Assert.Equals(1, actual);
}
Tyvärr inser du snabbt att det finns vissa problem med testet:
- Om testsviten körs på tisdag går det andra testet igenom, men det första testet misslyckas.
- Om testpaketet körs någon annan dag godkänns det första testet, men det andra testet misslyckas.
För att lösa dessa problem måste du introducera en söm i din produktionskod. En metod är att omsluta koden som du behöver styra i ett gränssnitt och låta produktionskoden vara beroende av det gränssnittet:
public interface IDateTimeProvider
{
DayOfWeek DayOfWeek();
}
public int GetDiscountedPrice(int price, IDateTimeProvider dateTimeProvider)
{
if (dateTimeProvider.DayOfWeek() == DayOfWeek.Tuesday)
{
return price / 2;
}
else
{
return price;
}
}
Du måste också skriva en ny version av testpaketet:
public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
{
var priceCalculator = new PriceCalculator();
var dateTimeProviderStub = new Mock<IDateTimeProvider>();
dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Monday);
var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);
Assert.Equals(2, actual);
}
public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
var priceCalculator = new PriceCalculator();
var dateTimeProviderStub = new Mock<IDateTimeProvider>();
dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Tuesday);
var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);
Assert.Equals(1, actual);
}
Nu har testpaketet fullständig kontroll över DateTime.Now
-värdet och kan ersätta vilket värde som helst när metoden anropas.