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 pro podporu projektů .NET Core a .NET Standard. Naučíte se techniky, jak udržet testy odolné a snadno pochopitelné.
John Reese díky Roy Osherove
Výhody testování jednotek
Následující části popisují několik důvodů pro psaní jednotkových testů 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í.
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. Těmto závislostem v aplikaci se můžete vyhnout pomocí zásad explicitních závislostí a použitím injektáže 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
Standardy pojmenování jsou důležité, protože pomáhají vyjádřit účel testování a aplikaci. Testy jsou více než jen zajištění toho, aby váš kód fungoval. Poskytují také dokumentaci. Pouhým pohledem na sadu jednicových testů byste měli být schopni odvodit chování kódu a nemusíte se dívat do samotného kódu. Když navíc testy selžou, můžete přesně zjistit, které scénáře nesplňují 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
, switch
a 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 pro testy požadujete podobný objekt nebo stav, použijte pomocnou metodu místo Setup
a Teardown
atributů, pokud existují. Pomocné metody jsou upřednostňované před těmito atributy z několika důvodů:
- Méně nejasnosti při čtení testů, protože veškerý kód je viditelný z každého testu
- Menší pravděpodobnost nastavit příliš vysoké nebo příliš nízké hodnoty pro daný test
- Menší pravděpodobnost sdílení stavu mezi testy, což mezi nimi vytváří nežádoucí závislosti
V architekturách testování jednotek se atribut Setup
volá před každým a každým testem jednotek v rámci testovací sady. Někteří programátoři vidí toto chování jako užitečné, ale často vede k nafoukaným a obtížně čitelným testům. Každý test má obecně různé požadavky na nastavení a spuštění. Atribut Setup
bohužel vynutí použití přesně stejných požadavků pro každý test.
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.