Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Existuje mnoho výhod v psaní jednotkových testů. Pomáhají s regresí, poskytují dokumentaci a usnadňují dobrý návrh. Ale když jsou jednotkové testy obtížně čitelné a křehké, mohou způsobit problémy ve vaší kódové základně. Tento článek popisuje některé osvědčené postupy pro navrhování testů jednotek, které podporují projekty .NET Core a .NET Standard. Naučíte se techniky, jak udržet testy odolné a snadno pochopitelné.
Příklady kódu používají testovací architekturu xUnit.net, která poskytuje atributy [Fact] a [Theory] a třídu Assert. Úvod do xUnit.net najdete v tématu Jednotkové testování jazyka C# v .NET pomocí dotnet test a xUnit. Osvědčené postupy platí také pro další testovací frameworky .NET.
John Reese díky Roy Osherove
Výhody testování jednotek
Následující části popisují několik důvodů, proč psát testy jednotek pro projekty .NET Core a .NET Standard.
Kratší doba provádění funkčních testů
Funkční testy jsou nákladné. Obvykle zahrnují otevření aplikace a provedení řady kroků, které vy (nebo někdo jiný) musíte dodržovat, aby bylo možné ověřit očekávané chování. Tyto kroky nemusí být pro tester vždy známé. Musí se spojit s někým, kdo je v oblasti znalější, aby mohli test provést. Samotné testování může trvat několik sekund pro triviální změny nebo několik minut pro větší změny. Nakonec se tento proces musí opakovat pro každou změnu, kterou v systému provedete. Naopak, testy jednotek zaberou milisekundy, lze je spustit stisknutím tlačítka a nemusí nutně vyžadovat žádné znalosti celého systému. Spouštěč testů určuje, zda test projde či selže, nikoli jednotlivec.
Ochrana před regresí
Regresní vady jsou chyby, které se zavádějí při změně aplikace. Je běžné, že testeři nejen testují svou novou funkci, ale také testovací funkce, které existovaly předem, aby ověřily, že stávající funkce stále fungují podle očekávání. Pomocí testování jednotek můžete po každém sestavení nebo i po změně řádku kódu znovu spustit celou sadu testů. Tento přístup pomáhá zvýšit jistotu, že nový kód neporuší stávající funkce.
Spustitelná dokumentace
Nemusí být vždy zřejmé, co konkrétní metoda dělá nebo jak se chová vzhledem k určitému vstupu. Můžete se zeptat sami sebe: Jak se tato metoda chová, pokud jí předám prázdný řetězec nebo hodnotu null? Pokud máte sadu dobře pojmenovaných testů jednotek, měl by každý test jasně vysvětlit očekávaný výstup pro daný vstup. Kromě toho by test měl být schopný ověřit, že skutečně funguje.
Méně propojený kód
Pokud je kód úzce propojený, může být obtížné jednotkové testování. Bez vytváření testů jednotek pro kód, který píšete, může být párování méně zřejmé. Psaní testů pro váš kód přirozeně rozpojuje jeho části, protože jinak je obtížnější je testovat.
Charakteristiky dobrých jednotkových testů
Existuje několik důležitých vlastností, které definují dobrý jednotkový test:
- Fast: Není neobvyklé, že vyspělé projekty mají tisíce jednotkových testů. Jednotkové testy by měly běžet krátce. Milisekundy.
- izolované: Jednotkové testy jsou samostatné, mohou být spuštěny izolovaně a nemají žádné závislosti na vnějších faktorech, jako je souborový systém nebo databáze.
- opakovatelné: Spuštění jednotkového testu by mělo konzistentně odpovídat jeho výsledkům. Test vždy vrátí stejný výsledek, pokud mezi spuštěními nic nezměníte.
- Samokontrola: Test by měl automaticky zjistit, zda prošel nebo neuspěl bez zásahu člověka.
- včasné: Jednotkový test by neměl zabrat nepřiměřeně dlouho na napsání ve srovnání s kódem, který je testován. Pokud zjistíte, že testování kódu trvá ve srovnání s psaním kódu hodně času, zvažte testovatelný návrh.
Pokrytí kódu a kvalita kódu
Vysoké procento pokrytí kódu je často spojeno s vyšší kvalitou kódu. Samotné měření ale nemůže určit kvalitu kódu. Nastavení příliš ambiciózního cíle v procentech pokrytí kódu může být kontraproduktivní. Zvažte složitý projekt s tisíci podmíněných větví a předpokládejme, že nastavíte cíl 95% pokrytí kódu. Projekt v současné době udržuje pokrytí kódu na úrovni 90 %%. Časový rozsah potřebný k tomu, aby byly vzaty v úvahu všechny hraniční případy ve zbývajících 5%, může být ohromným úkolem a přidaná hodnota se rychle sníží.
Vysoké procento pokrytí kódu není indikátorem úspěchu a neznamená vysokou kvalitu kódu. Představuje jenom množství kódu, které pokryjí jednotkové testy. Další informace o pokrytí kódu při testování jednotek najdete v tématu .
Terminologie testování jednotek
V kontextu testování jednotek se často používá několik termínů: fake, mocka stub. Tyto termíny se bohužel dají chybně použít, takže je důležité pochopit správné použití.
Poznámka:
Testovací literatura a nástroje používají termíny falešný, zástupce a mock nekonzistentně. Následující definice představují běžné .NET použití, kde testovací double může vracet hodnoty i ověřovat interakce. V klasické literatuře xUnit a testovací literatuře o dublování (například Fowler a Meszaros) používají tyto zdroje tato slova úžeji: zástupný objekt poskytuje data, maketa ověřuje interakce a imitace je funkční alternativní implementace.
Falešný: Falešný je obecný termín, který lze použít k popisu buď zástupného nebo napodobeninářského objektu. Zda je objekt zástupný nebo mockovací závisí na kontextu, ve kterém se objekt používá. Jinými slovy, falešný může být zástupný nebo napodobený.
Mock: Mock objekt je falešný objekt v systému, který rozhoduje, zda jednotkový test projde nebo selže. Napodobenina začíná jako falešná a zůstává falešnou, dokud nevstoupí do operace
Assert.Náhradník: Náhradník je kontrolovatelnou náhradou existující závislosti (nebo spolupracovníka) v systému. Pomocí zástupných procedur můžete otestovat kód bez přímého zpracování závislosti. Ve výchozím nastavení začíná zástupce jako napodobenina.
Vezměte v úvahu následující kód:
var mockOrder = new MockOrder();
var purchase = new Purchase(mockOrder);
purchase.ValidateOrders();
Assert.True(purchase.CanBeShipped);
Tento kód ukazuje zástupný kód, který se označuje jako mock. Ale v tomto scénáři je zástupce skutečně jen zástupcem. Účelem kódu je předat pořadí jako prostředek pro vytvoření instance objektu Purchase (systém pod testem). Název třídy MockOrder je zavádějící, protože pořadí je jen náčrt, nikoli falešná napodobenina.
Následující kód ukazuje přesnější návrh:
var stubOrder = new FakeOrder();
var purchase = new Purchase(stubOrder);
purchase.ValidateOrders();
Assert.True(purchase.CanBeShipped);
Když je třída přejmenována na FakeOrder, třída je obecnější. Třídu lze použít jako napodobení nebo zástupný proceduru podle požadavků testovacího případu. V prvním příkladu se třída FakeOrder používá jako zástupný kód a nepoužívá se během operace Assert. Kód předá třídu FakeOrder třídě Purchase pouze, aby splnil požadavky konstruktoru.
Pokud chcete třídu použít jako napodobení, můžete kód aktualizovat:
var mockOrder = new FakeOrder();
var purchase = new Purchase(mockOrder);
purchase.ValidateOrders();
Assert.True(mockOrder.Validated);
V tomto návrhu kód zkontroluje vlastnost na falešném objektu (kontrolou proti němu), a proto je třída mockOrder mock objekt.
Důležité
Je důležité správně implementovat terminologii. Pokud nazýváte své stuby "mocky", ostatní vývojáři si budou dělat nesprávné představy o vašem záměru.
Hlavní věc, kterou si pamatujte o mockách versus stubách, je, že mocky jsou jako stuby, kromě procesu Assert. Operujete s Assert operacemi proti mockovému objektu, ale ne proti pomocným modulům.
Osvědčené postupy
Při psaní testů jednotek je potřeba dodržovat několik důležitých osvědčených postupů. Následující části obsahují příklady, které ukazují, jak použít osvědčené postupy pro váš kód.
Vyhněte se závislostem infrastruktury
Pokuste se při psaní testů jednotek nezavádět závislosti na infrastruktuře. Závislosti zpomalují a činí testy křehkými a měly by být vyhrazeny pro integrační testy. Můžete se vyhnout těmto závislostem v aplikaci tím, že budete dodržovat zásadu explicitních závislostí a použijete injektování závislostí .NET. Testy jednotek můžete také ponechat v samostatném projektu od integračních testů. Tento přístup zajišťuje, že projekt testování jednotek nemá odkazy na balíčky infrastruktury ani na jejich závislosti.
Dodržování standardů pro pojmenování testů
Název testu by se měl skládat ze tří částí:
- Název testované metody
- Scénář, ve kterém se metoda testuje
- Očekávané chování při vyvolání scénáře
Chcete-li zdokumentovat očekávané chování kódu v rámci testu, použijte popisné a konzistentní názvy testů. Každý název určuje, jak se má kód chovat v určitém scénáři, zatímco test toto chování ověří. Jasné názvy vám pomůžou porozumět očekávanému chování ze sady testů bez kontroly testovaného kódu. Když test selže, pomůže vám jeho název určit, který scénář nesplnil vaše očekávání.
původní kód
[Fact]
public void Test_Single()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("0");
Assert.Equal(0, actual);
}
Použít osvědčené postupy
[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("0");
Assert.Equal(0, actual);
}
Uspořádání testů
Vzor "Uspořádat, Akt, Asertovat" je běžný přístup pro psaní jednotkových testů. Jak název napovídá, vzor se skládá ze tří hlavních úkolů:
- uspořádejte objekty, vytvořte je a nakonfigurujte podle potřeby.
- Jednat s objektem
- Assert, že něco je podle očekávání
Při sledování šablony můžete jasně oddělit to, co se testuje, od úkolů Arrange a Assert. Vzor také pomáhá snížit možnost kontrolních výrazů intermixovat s kódem v úloze Act.
Čitelnost je jedním z nejdůležitějších aspektů při psaní jednotkového testu. Oddělení každé akce vzoru v rámci testu jasně zvýrazní závislosti potřebné k volání kódu, jak se volá váš kód a co se pokoušíte uplatnit. I když může být možné zkombinovat některé kroky a zmenšit velikost testu, celkovým cílem je, aby byl test co nejčtenější.
původní kód
[Fact]
public void Add_EmptyString_ReturnsZero()
{
// Arrange
var stringCalculator = new StringCalculator();
// Assert
Assert.Equal(0, stringCalculator.Add(""));
}
Použít osvědčené postupy
[Fact]
public void Add_EmptyString_ReturnsZero()
{
// Arrange
var stringCalculator = new StringCalculator();
// Act
var actual = stringCalculator.Add("");
// Assert
Assert.Equal(0, actual);
}
Psaní testů, které minimálně splňují požadavky
Vstupem pro test jednotek by měly být nejjednodušší informace potřebné k ověření chování, které právě testujete. Minimalistický přístup pomáhá testům stát se odolnější vůči budoucím změnám v základu kódu a zaměřit se na ověření chování při implementaci.
Testy, které obsahují více informací, než je vyžadováno pro absolvování aktuálního testu, mají větší šanci na zavedení chyb do testu a mohou způsobit, že záměr testu bude méně jasný. Při psaní testů se chcete zaměřit na chování. Nastavení dodatečných vlastností u modelů nebo použití nenulových hodnot, pokud nejsou povinné, odvádějí pozornost od toho, co se snažíte potvrdit.
původní kód
[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("42");
Assert.Equal(42, actual);
}
Použít osvědčené postupy
[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("0");
Assert.Equal(0, actual);
}
Vyhněte se magickým řetězcům
Magické řetězce jsou řetězcové hodnoty přímo zapsané v jednotkových testech bez jakéhokoli dalšího komentáře či kontextu kódu. Díky těmto hodnotám je váš kód méně čitelný a obtížněji udržovatelný. Magické řetězce mohou způsobit nejasnosti čtenáři testů. Pokud řetězec vypadá neobvykle, mohou se ptát, proč byla pro parametr nebo návratovou hodnotu vybrána určitá hodnota. Tento typ řetězcové hodnoty může vést k tomu, aby se blíže podívali na podrobnosti implementace, a nemuseli se soustředit na test.
Návod
Udělejte si cílem vyjádřit co nejvíce záměru ve svém kódu testu jednotek. Místo použití magických řetězců přiřaďte konstantám pevně zakódované hodnoty.
původní kód
[Fact]
public void Add_BigNumber_ThrowsException()
{
var stringCalculator = new StringCalculator();
Action actual = () => stringCalculator.Add("1001");
Assert.Throws<OverflowException>(actual);
}
Použít osvědčené postupy
[Fact]
void Add_MaximumSumResult_ThrowsOverflowException()
{
var stringCalculator = new StringCalculator();
const string MAXIMUM_RESULT = "1001";
Action actual = () => stringCalculator.Add(MAXIMUM_RESULT);
Assert.Throws<OverflowException>(actual);
}
Vyhněte se vkládání logiky v jednotkových testech.
Při psaní jednotkových testů se vyhněte ručnímu zřetězení řetězců a logickým podmínkám, jako jsou if, while, for, switcha další podmínky. Pokud do testovací sady zahrnete logiku, výrazně se zvyšuje pravděpodobnost, že se zavádějí chyby. Poslední místo, kde chcete najít chybu, je ve vaší testovací sadě. Měli byste mít vysokou úroveň jistoty, že testy fungují, jinak jim nemůžete důvěřovat. Testy, kterým nedůvěřujete, neposkytují žádnou hodnotu. Když se test nezdaří, chcete mít pocit, že něco není v kódu v pořádku a že ho nemůžete ignorovat.
Návod
Pokud se přidání logiky do testu zdá být nevyhnutelné, zvažte rozdělení testu na dva nebo více různých testů, abyste omezili požadavky logiky.
původní kód
[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;
}
}
Použít osvědčené postupy
[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);
}
Použijte pomocné metody místo nastavení a rozebrání
Pokud chcete zachovat viditelné nastavení specifické pro test, vytvořte objekty, které potřebujete v každé testovací metodě, nebo volejte pomocnou metodu z testu. Upřednostněte tento přístup před atributy Setup a Teardown, pokud testy mají různé požadavky na nastavení:
- Každý test ukazuje nastavení, které vyžaduje.
- Každý test vytvoří pouze objekty a stav, který potřebuje.
- Změny v kódu společného nastavení nemají vliv na testy, které ho nepoužívají.
Metody nastavení jednotlivých testů se spouští před každým testem v jejich rozsahu platnosti, i když test nepotřebuje všechna nastavení. Když testy získávají různé požadavky, může běžná metoda nastavení vytvořit nepotřebné objekty nebo skrýt stav, na kterém konkrétní test závisí.
Společné nastavení pro každý test může být stále vhodné, pokud každý test ve třídě potřebuje stejnou inicializaci. Například MSTest vytvoří novou instanci třídy testů pro každý test, včetně každého datového řádku, takže inicializace konstruktoru se spustí samostatně pro každý test. Pro synchronní inicializaci preferujte konstruktor. Použijte TestInitialize , když potřebujete podporu asynchronní inicializace nebo časového limitu. Podrobnosti najdete v tématu životní cyklus MSTest.
Poznámka:
Atributy SetUp a TearDown se odeberou v xUnit verze 2.x a novější.
původní kód
Použít osvědčené postupy
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();
}
Vyhněte se více úkolům act
Když píšete testy, zkuste zahrnout do každého testu pouze jeden úkol Act. Mezi běžné přístupy k provedení jednoho úkolu v rámci fáze "Act" patří vytvoření samostatného testu pro každou fázi Act nebo použití parametrizovaných testů. Použití jednoho úkolu Act pro každý test má několik výhod:
- Pokud test selže, můžete snadno rozpoznat, která úloha Actu selhává.
- Můžete zajistit, aby se test zaměřoval jenom na jeden případ.
- Získáte jasný přehled o tom, proč vaše testy selhávají.
Více úkolů Act je potřeba provést jednotlivě a nemůžete zaručit, že se všechny úkoly Assert spustí. Ve většině architektur testování jednotek se po selhání úlohy Assert v testu jednotek všechny následné testy automaticky považují za selhání. Proces může být matoucí, protože některé funkční funkce mohou být interpretovány jako neúspěšné.
původní kód
[Fact]
public void Add_EmptyEntries_ShouldBeTreatedAsZero()
{
// Act
var actual1 = stringCalculator.Add("");
var actual2 = stringCalculator.Add(",");
// Assert
Assert.Equal(0, actual1);
Assert.Equal(0, actual2);
}
Použít osvědčené postupy
[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);
}
Ověření privátních metod pomocí veřejných metod
Ve většině případů nemusíte v kódu testovat privátní metodu. Soukromé metody jsou podrobnosti implementace a nikdy neexistují izolovaně. V určitém okamžiku vývojového procesu zavádíte veřejnou metodu, která volá privátní metodu jako součást jeho implementace. Když píšete testy jednotek, záleží vám na konečném výsledku veřejné metody, která volá privátní metodu.
Představte si následující scénář kódu:
public string ParseLogLine(string input)
{
var sanitizedInput = TrimInput(input);
return sanitizedInput;
}
private string TrimInput(string input)
{
return input.Trim();
}
Z hlediska testování by vaší první reakcí mohlo být napsat test k metodě TrimInput, abyste zajistili, že funguje podle očekávání. Je však možné, že metoda ParseLogLine manipuluje s objektem sanitizedInput způsobem, který neočekáváte. Neznámé chování může učinit test podle metody TrimInput zbytečným.
Lepším testem v tomto scénáři je ověření veřejné metody ParseLogLine:
public void ParseLogLine_StartsAndEndsWithSpace_ReturnsTrimmedResult()
{
var parser = new Parser();
var result = parser.ParseLogLine(" a ");
Assert.Equals("a", result);
}
Když narazíte na privátní metodu, vyhledejte veřejnou metodu, která volá privátní metodu, a zapište testy proti veřejné metodě. Protože privátní metoda vrací očekávaný výsledek, neznamená to, že systém, který nakonec volá privátní metodu, použije výsledek správně.
Spravujte stub statické odkazy pomocí švů
Jedním z principů testu jednotek je, že musí mít úplnou kontrolu nad systémem, který se testuje. Tento princip ale může být problematický, pokud produkční kód obsahuje volání statických odkazů (například DateTime.Now).
Prozkoumejte následující scénář kódu:
public int GetDiscountedPrice(int price)
{
if (DateTime.Now.DayOfWeek == DayOfWeek.Tuesday)
{
return price / 2;
}
else
{
return price;
}
}
Můžete pro tento kód napsat jednotkový test? Můžete zkusit spustit úlohu Assert na 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);
}
Bohužel, rychle zjistíte, že došlo k nějakým problémům s vaším testem:
- Pokud testovací sada běží v úterý, druhý test projde, ale první test selže.
- Pokud se sada testů spustí v jiný den, první test projde, ale druhý test selže.
K vyřešení těchto problémů musíte do produkčního kódu zavést šev. Jedním z přístupů je zabalit kód, který potřebujete ovládat v rozhraní, a mít produkční kód závislý na daném rozhraní:
public interface IDateTimeProvider
{
DayOfWeek DayOfWeek();
}
public int GetDiscountedPrice(int price, IDateTimeProvider dateTimeProvider)
{
if (dateTimeProvider.DayOfWeek() == DayOfWeek.Tuesday)
{
return price / 2;
}
else
{
return price;
}
}
Musíte také napsat novou verzi testovací sady:
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);
}
Teď má sada testů plnou kontrolu nad hodnotou DateTime.Now a může zavolat do metody libovolnou hodnotu.