Osvědčené postupy testování částí s využitím .NET Core a .NET Standard

Existuje mnoho výhod psaní testů jednotek; pomáhají s regresí, poskytují dokumentaci a usnadňují dobrý návrh. Obtížně čitelné a křehké testy jednotek ale můžou způsobit havárii na základu kódu. Tento článek popisuje některé osvědčené postupy týkající se návrhu testů jednotek pro projekty .NET Core a .NET Standard.

V této příručce se seznámíte s některými osvědčenými postupy při psaní testů jednotek, abyste udrželi odolné a snadno pochopitelné testy.

John Reese se speciální díky Roy Osherove

Proč test jednotek?

K použití testů jednotek existuje několik důvodů.

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é. Aby mohl test provést, musí se spojit s někým, kdo je v dané oblasti znalější. Testování samotného může trvat několik sekund než triviální změny nebo minuty pro větší změny. Nakonec se tento proces musí opakovat pro každou změnu, kterou v systému provedete.

Testy jednotek, na druhé straně, vzít milisekundy, lze spustit na stisknutí tlačítka, a nemusí nutně vyžadovat žádné znalosti systému ve velkém. Zda test projde nebo selže, je až do spouštěče testů, ne jednotlivce.

Ochrana před regresí

Regresní vady jsou vady, které jsou zavedeny 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ěřili, že dříve implementované funkce stále fungují podle očekávání.

S testováním částí je možné po každém sestavení nebo i po změně řádku kódu znovu spustit celou sadu testů. Získáte 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á, když jí předám prázdný řetězec? Null?

Pokud máte sadu dobře pojmenovaných testů jednotek, měl by být každý test schopen jasně vysvětlit očekávaný výstup pro daný vstup. Kromě toho by měla 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é test jednotek. 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ě odděluje váš kód, protože by bylo obtížnější testovat jinak.

Charakteristiky dobrého testu jednotek

  • Rychle: Není neobvyklé, že vyspělé projekty mají tisíce testů jednotek. Spuštění testů jednotek by mělo chvíli trvat. Milisekund.
  • Izolované: Testy jednotek jsou samostatné, mohou být spuštěny izolovaně a nemají žádné závislosti na žádných vnějších faktorech, jako je systém souborů nebo databáze.
  • Opakovatelné: Spuštění testu jednotek by mělo být konzistentní s výsledky, to znamená, že vždy vrátí stejný výsledek, pokud mezi běhy nic nezměníte.
  • Samokontrola: Test by měl být schopen automaticky zjistit, jestli prošel nebo selhal bez jakékoli lidské interakce.
  • Včas: V porovnání s testovaným kódem by neměl test jednotek trvat nepřiměřeně dlouho. Pokud zjistíte, že testování kódu trvá hodně času v porovnání s psaním kódu, zvažte návrh, který je testovatelný.

Pokrytí kódu

Vysoké procento pokrytí kódu je často spojeno s vyšší kvalitou kódu. Samotné měření ale nedokáže určit kvalitu kódu. Nastavení příliš ambiciózního cíle v procentech pokrytí kódu může být kontraproduktivní. Představte si složitý projekt s tisíci podmíněných větví a představte si, že nastavíte cíl pokrytí kódu 95 %. V současné době projekt udržuje 90% pokrytí kódu. Doba, kterou je třeba zohlednit u všech hraničních případů ve zbývajících 5 % může být obrovským závazkem a hodnota se rychle sníží.

Vysoké procento pokrytí kódu není indikátorem úspěchu ani neznamená vysokou kvalitu kódu. Představuje pouze množství kódu, který je pokryt testy jednotek. Další informace najdete v tématu pokrytí kódu testování částí.

Pojďme mluvit stejným jazykem

Termín napodobení je bohužel často zneužitý při komunikaci o testování. Následující body definují nejběžnější typy falešných zpráv při psaní testů jednotek:

Falešný - falešný je obecný termín, který lze použít k popisu zástupných procedur nebo napodobení objektu. Bez ohledu na to, jestli se jedná o zástupný proceduru nebo napodobení, závisí na kontextu, ve kterém se používá. Jinými slovy, falešný může být zástupný nebo napodobený.

Napodobení – Objekt napodobení je falešný objekt v systému, který rozhoduje, jestli test jednotek prošel nebo selhal. Napodobení začíná jako falešný, dokud se nevymáhá.

Zástupná procedura – zástupná procedura je kontrolovatelným nahrazením 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ástupný procedura jako falešný.

Vezměte v úvahu následující fragment kódu:

var mockOrder = new MockOrder();
var purchase = new Purchase(mockOrder);

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

Předchozí příklad by byl zástupný znak, který se označuje jako napodobení. V tomto případě je to zástupný procedura. Právě předáváte objednávku jako prostředek, abyste mohli vytvořit instanci Purchase (systém pod testem). Název MockOrder je také zavádějící, protože pořadí není napodobení.

Lepším přístupem by bylo:

var stubOrder = new FakeOrder();
var purchase = new Purchase(stubOrder);

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

Přejmenováním třídy na FakeOrder, jste udělali třídu mnohem obecnější. Třídu lze použít jako napodobení nebo zástupný proceduru, podle toho, co je pro testovací případ lepší. V předchozím příkladu FakeOrder se používá jako zástupný znak. Během kontrolního výrazu nepoužíváte FakeOrder žádný obrazec ani formulář. FakeOrder byla předána Purchase do třídy, aby splňovala požadavky konstruktoru.

Pokud ho chcete použít jako napodobení, můžete udělat něco jako následující kód:

var mockOrder = new FakeOrder();
var purchase = new Purchase(mockOrder);

purchase.ValidateOrders();

Assert.True(mockOrder.Validated);

V tomto případě kontrolujete vlastnost na fake (asserting proti němu), takže v předchozím fragmentu kódu je to mockOrder napodobení.

Důležité

Je důležité tuto terminologii správně získat. Pokud zavoláte zástupné procedury "napodobení", ostatní vývojáři budou dělat falešné předpoklady o vašem záměru.

Hlavní věc, kterou si pamatujete o napodobení versus zástupné procedury, je, že napodobení jsou stejně jako zástupné procedury, ale tvrdíte proti napodobení objektu, zatímco nevymáháte proti zástupné procedurě.

Osvědčené postupy

Tady jsou některé z nejdůležitějších osvědčených postupů pro psaní testů jednotek.

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 zpomalují testy a měly by být vyhrazené pro integrační testy. Těmto závislostem v aplikaci se můžete vyhnout pomocí zásady explicitních závislostí a použitím injektážezávislostch 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.

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 testuje.
  • Očekávané chování při vyvolání scénáře.

Proč?

Standardy pojmenování jsou důležité, protože explicitně vyjadřují záměr testu. Testy jsou více než jen zajištění toho, aby váš kód fungoval, a poskytují také dokumentaci. Jen když se podíváte na sadu testů jednotek, měli byste být schopni odvodit chování kódu, aniž byste se podívali na samotný kód. Kromě toho, když testy selžou, můžete přesně zjistit, které scénáře nesplňují vaše očekávání.

Chybně:

[Fact]
public void Test_Single()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("0");

    Assert.Equal(0, actual);
}

Lepší:

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("0");

    Assert.Equal(0, actual);
}

Uspořádání testů

Uspořádání, act, Assert je běžný vzor při testování jednotek. Jak už název napovídá, skládá se ze tří hlavních akcí:

  • Uspořádejte objekty, vytvořte je a nastavte podle potřeby.
  • Zareagovat na objekt.
  • Ověřte , že je něco podle očekávání.

Proč?

  • Jasně odděluje to, co se testuje, od uspořádání a kontrolních kroků.
  • Menší šance na intermixování kontrolních výrazů s kódem Act

Čitelnost je jednou z nejdůležitějších aspektů při psaní testu. Oddělení každé z těchto akcí v rámci testu jasně zvýrazní závislosti potřebné k volání kódu, způsob volání kódu a to, co se pokoušíte uplatnit. I když může být možné zkombinovat některé kroky a zmenšit velikost testu, primárním cílem je, aby byl test co nejčtenější.

Chybně:

[Fact]
public void Add_EmptyString_ReturnsZero()
{
    // Arrange
    var stringCalculator = new StringCalculator();

    // Assert
    Assert.Equal(0, stringCalculator.Add(""));
}

Lepší:

[Fact]
public void Add_EmptyString_ReturnsZero()
{
    // Arrange
    var stringCalculator = new StringCalculator();

    // Act
    var actual = stringCalculator.Add("");

    // Assert
    Assert.Equal(0, actual);
}

Zápis testů s minimálním absolvováním

Vstup, který se má použít v testu jednotek, by měl být nejjednodušší, aby bylo možné ověřit chování, které právě testujete.

Proč?

  • Testy se stanou odolnější vůči budoucím změnám v základu kódu.
  • Blíž k testování chování při implementaci.

Testy, které obsahují více informací, než je vyžadováno pro absolvování 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é, odečte jenom to, co se snažíte prokázat.

Chybně:

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("42");

    Assert.Equal(42, actual);
}

Lepší:

[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

Pojmenování proměnných v testech jednotek je důležité, pokud není důležitější než pojmenování proměnných v produkčním kódu. Testy jednotek by neměly obsahovat magické řetězce.

Proč?

  • Zabraňuje tomu, aby čtenář testu zkontroloval produkční kód, aby zjistil, co je hodnota speciální.
  • Explicitně ukazuje, co se snažíte místo toho, abyste se snažili dosáhnout.

Magické řetězce mohou způsobit nejasnosti čtenáři testů. Pokud řetězec nevypadá z obyčejné hodnoty, může se divit, 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.

Tip

Při psanítestůch V případě magických řetězců je dobrým přístupem přiřadit tyto hodnoty konstantám.

Chybně:

[Fact]
public void Add_BigNumber_ThrowsException()
{
    var stringCalculator = new StringCalculator();

    Action actual = () => stringCalculator.Add("1001");

    Assert.Throws<OverflowException>(actual);
}

Lepší:

[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 logice v testech

Při psaní testů jednotek se vyhněte ručnímu zřetězení řetězců, logickým podmínkám, například if, while, for, a switchdalším podmínkám.

Proč?

  • Menší šance na zavedení chyby v rámci testů
  • Zaměřte se na konečný výsledek, ne na podrobnosti implementace.

Když do testovací sady zavedete logiku, výrazně se zvýší pravděpodobnost, že do ní dojde k chybě. 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 nebudete 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.

Tip

Pokud se logika ve vašem testu zdá být nepohodná, zvažte rozdělení testu na dva nebo více různých testů.

Chybně:

[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;
    }
}

Lepší:

[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);
}

Preferovat pomocné metody pro nastavení a odbourání

Pokud pro testy požadujete podobný objekt nebo stav, preferujte pomocnou metodu než použití Setup a Teardown atributy, pokud existují.

Proč?

  • Méně nejasnosti při čtení testů, protože veškerý kód je viditelný z každého testu.
  • Menší pravděpodobnost nastavení příliš nebo příliš malého 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 Setup volá před každým a každým testem jednotek v rámci testovací sady. I když někteří můžou vidět tento nástroj jako užitečný nástroj, obvykle končí na bloudné a obtížně čitelné testy. Každý test bude mít obecně různé požadavky, aby se test zprovozní. Bohužel vás vynutí Setup , abyste pro každý test použili naprosto stejné požadavky.

Poznámka:

xUnit odebral setUp i TearDown od verze 2.x

Chybně:

private readonly StringCalculator stringCalculator;
public StringCalculatorTests()
{
    stringCalculator = new StringCalculator();
}
// more tests...
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
    var result = stringCalculator.Add("0,1");

    Assert.Equal(1, result);
}

Lepší:

[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
    var stringCalculator = CreateDefaultStringCalculator();

    var actual = stringCalculator.Add("0,1");

    Assert.Equal(1, actual);
}
// more tests...
private StringCalculator CreateDefaultStringCalculator()
{
    return new StringCalculator();
}

Vyhněte se více akcím

Při psaní testů zkuste zahrnout pouze jeden akt na test. Mezi běžné přístupy k používání pouze jednoho aktu patří:

  • Vytvořte samostatný test pro každou akci.
  • Použijte parametrizované testy.

Proč?

  • Když test selže, je jasné, která akce selhává.
  • Zajišťuje, že se test zaměřuje jenom na jeden případ.
  • Poskytuje celý obrázek, proč vaše testy selhávají.

Více aktů musí být samostatně Asserted a není zaručeno, že budou spuštěny všechny kontrolní výrazy. Ve většině architektur testování jednotek se jakmile kontrolní výraz v testu jednotek nezdaří, budou se testy při pokračování automaticky považovat za neúspěšné. Tento druh procesu může být matoucí, protože funkce, které ve skutečnosti fungují, se zobrazí jako selhání.

Chybně:

[Fact]
public void Add_EmptyEntries_ShouldBeTreatedAsZero()
{
    // Act
    var actual1 = stringCalculator.Add("");
    var actual2 = stringCalculator.Add(",");

    // Assert
    Assert.Equal(0, actual1);
    Assert.Equal(0, actual2);
}

Lepší:

[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 testováním jednotek veřejnými metodami

Ve většině případů by nemělo být potřeba otestovat privátní metodu. Soukromé metody jsou podrobnosti implementace a nikdy neexistují izolovaně. V určitém okamžiku bude k dispozici veřejná metoda, která v rámci své implementace volá privátní metodu. Měli byste se starat o konečný výsledek veřejné metody, která volá do privátní metody.

Vezměte v úvahu následující případ:

public string ParseLogLine(string input)
{
    var sanitizedInput = TrimInput(input);
    return sanitizedInput;
}

private string TrimInput(string input)
{
    return input.Trim();
}

První reakcí může být začít psát test, TrimInput protože chcete zajistit, aby metoda fungovala podle očekávání. Je však zcela možné, že ParseLogLine manipuluje sanitizedInput takovým způsobem, který neočekáváte, a vykresluje test proti TrimInput zbytečnému použití.

Skutečný test by měl být proveden proti veřejné metodě ParseLogLine , protože to je to, co byste měli nakonec starat.

public void ParseLogLine_StartsAndEndsWithSpace_ReturnsTrimmedResult()
{
    var parser = new Parser();

    var result = parser.ParseLogLine(" a ");

    Assert.Equals("a", result);
}

Pokud se v tomto pohledu zobrazí soukromá metoda, vyhledejte veřejnou metodu a napište testy proti této 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ě.

Statické odkazy na zástupných procedur

Jedním z principů testu jednotek je, že musí mít úplnou kontrolu nad systémem, který se testuje. Tento princip může být problematický, pokud produkční kód zahrnuje volání statických odkazů (například DateTime.Now). Uvažujte následující kód:

public int GetDiscountedPrice(int price)
{
    if (DateTime.Now.DayOfWeek == DayOfWeek.Tuesday)
    {
        return price / 2;
    }
    else
    {
        return price;
    }
}

Jak se dá tento kód testovat? Můžete vyzkoušet přístup, například:

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 si rychle uvědomíte, že máte několik problémů s testy.

  • Pokud se sada testů spustí v úterý, druhý test projde, ale první test selže.
  • Pokud je sada testů spuštěna v jiný den, první test projde, ale druhý test selže.

Pokud chcete tyto problémy vyřešit, budete muset do produkčního kódu zavést švy . Jedním z přístupů je zabalit kód, který potřebujete řídit 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;
    }
}

Vaše testovací sada se teď stane následujícím způsobem:

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á testovací sada plnou kontrolu DateTime.Now nad a může zastřešovat libovolnou hodnotu při volání do metody.