Miroslav Holec

Software & Cloud Architect

miroslavholec.cz / blog / principy-tipy-a-triky-pro-psani-unit-testu

Principy, tipy a triky pro psaní unit testů

Miroslav Holec

Miroslav Holec

Publikován 23. června 2015 , aktualizace: 29. března 2016

Tento článek je starší 18 měsíců a je proto možné, že popisuje postupy nebo technologie, které v uplynulé době mohly doznat výraznějších změn. Názory a myšlenky v tomto článku již nemusí vyjadřovat současné stanovisko autora nebo autorů. Článek byl napsán 23. června 2015.

V minulosti jsem se rozepsal o výhodách jednotkových testů a o tom, jak začít unit testy psát. Po krátké odmlce navážu na tyto články a vysvětlím základní principy psaní jednotkových testů. Pochopení a dodržování těchto principů je klíčové pro vznik robustních unit testů.

Unit Testing, Test First a TDD

Začnu demystifikací jednoho problému. Možná to bude šokující zpráva, ale psát testy neznamená dělat TDD. Je až k neuvěření, že se pojmy související s testováním ztotožňují a mystifikují. Nedávno jsem dokonce našel rovnici TDD = Refactoring + Test First, což je také dost zavádějící, protože samotný Test Driven Development je komplexní metodika. Nejhorší mi ale přijde, že v souvislosti s psaním testů všechno působí tak, jako kdyby neexistovala jiná možnost, než začít testem a skončit u samotné implementace jednotky.

Pokud s testováním aplikací začínáte, doporučuji nedělat Test First. Psaní testů je samo o sobě komplikovanou disciplínou. Vývojáře čeká pochopit testovací framework, naučit se mockovat a nakonec i napsat test, který bude splňovat řadu požadavků, o kterých se rozepisuji níže v tomto článku. Pro vývojáře, který nikdy žádný test nenapsal je Test First velká výzva. Z vlastní zkušenosti doporučuji napsat nejprve implementaci kódu a tu začít testovat. Je to proces, během kterého se lze nenásilně naučit principy psaní testů a odstranit špatné návyky související s vývojem aplikace, které brání psaní kvalitních a čitelných testů. Teprve když má vývojář tyto principy v podvědomí, stojí za to přejít na Test First.

Vyzkoušejte Test First

Přístup Test First má minimálně 3 velké výhody. V první řadě jsem nucen uvažovat o specifikaci před samotným vznikem kódu a tudíž eliminovat scénáře, které bych s jistotou nebral při psaní produkčního kódu v potaz. Problém je totiž ten, že při psaní produkčního kódu si třídu/metodu spojuje vývojář především s aktuálně řešeným problémem. A také to často dopadá tak, že třída/metoda je skutečně příliš specifická a neznovupoužitelná pro jiný, třeba i podobný případ. Jinak řečeno, Test First dost markantně pomáhá navrhovat lepší design aplikace. Třetí výhodou je kontrola mého kódu bez dalšího úsilí. Když už mám test, píšu jen implementaci, dokud není test zelený. S pluginem NCrunch nemusím testy ani spouštět, protože kompilace a vyhodnocování probíhá automaticky.

Nenechte se zchvátit zelenou horečkou

Unit Testy nejsou všemocné a jejich používání musí přinést určitou hodnotu. Nemá smysl ztrácet čas testováním rutin a domnívám se, že ani nemá smysl ztrácet čas budováním překomplikovaných testů obsahujících mraky mocků jen proto, aby se otestovaly dva průtoky jednou primitivní metodou. Psát test by mělo vývojáře postupem času automaticky napadnout jako nejsnazší cesta, jak skutečně ověřit správnou implementaci jednotky a zajistit ji ochranu proti rozbití.

Psát testy neznamená zemřít na zelenou horečku.

Chovejte se k testům jako k projektu

Z vlastní zkušenosti se mi osvědčilo přistupovat k testům jako k samostatnému projektu. V praxi to znamená, že i řada principů, které se používají v běžných aplikacích (OOP, různé patterny, helpery) lze použít pro vytvoření robustního testovacího projektu. Díky tomu se lze odstínit od opakování kódu (DRY) a zanechat testovací kód čistý. Ačkoliv se to nemusí zdát, číst testovací kód může být dost těžké a mnohdy méně srozumitelné než v případě aplikačního kódu.

Unit Test by neměla být špageta na jedno použití

Pište robustní testy

Že by měl jednotkový test ověřovat chování "nejmenší možné jednotky" je vcelku známé paradigma. Nejmenší jednotkou bývá obvykle nějaký teoretický scénář průchodu určitou metodou. Téměř vždy je nutné napsat více testů pro otestování jedné metody třídy. Dobrou představu o tom, kolik práce dá pokrýt metodu testama může reflektovat do určité míry cyklomatická komplexita. Čím je komplexita metody vyšší, tím více testů je nutné napsat (rovnice na to ale rozhodně neexistuje).

Základním požadavkem na každý jednotkový test je bezesporu jeho autonomita a nezávislost na ostatních testech. Testy by neměly měnit prostředí a ovlivňovat průběh ostatních testů. Tím se o krok blíží k dalšímu požadavku, kterým je schopnost neomezeného opakování. Spustí-li se test několikrát po sobě, jeho výsledek musí být vždy stejný. Test je tedy zcela deterministický a spolehlivý. Musí být.

Když se test rozbije, spravte ho

Testy nejsou nerozbitné. Bez ohledu na notorické dodržování SOLID principů stráví vývojář velké množství času opravou testů. Je to zcela normální důsledek toho, že většina napsaného kódu je v praxi předurčená k tomu, že se stejně později přepisuje. Zároveň je ale potřeba hledat cestu, jak tyto opravy "odbavit" co nejrychleji. Právě správné nastavení samotného testovacího projektu vytváří předpoklady pro to, aby doba oprav byla minimalizována a testování se stalo příjemnou a užitečnou činností.

Není špatné mít rozbitý test, špatné je mít neopravený test.

Testujte správnou úroveň abstrakce

Testy se dají psát na různé aplikační vrstvy. Jedno z klíčových pravidel testování je držet se určité vrstvy (nebo úrovně abstrakce). Závislosti testované entity se snažíme vždy odstřihnout (tedy testovat zase na jiné úrovni). V praxi se používá fakeování závislostí pomocí různých mocků, stubů nebo jiných praktik. Typickou chybou je tedy testování vyšších vrstev aplikace společně se závilostmi na nižších vrstvách (např.: controller + repositáře, services + repositáře).

Rozlišujte specifické a obecné kusy kódu

Pokud do svého projektu, který se orientuje na určitou oblast vložíte obecnou metodu, o které lze říct, že bude použitelná i na jiných projektech, snažte se ji i obecně testovat. Velký problém je vznik tříd a metod, které mají potenciál být znovupoužitelné testovat na základě vyvíjeného projektu pouze na konkrétní scénáře.

Dodržujte jmenné konvence

Snad nejlépe vystihne jmenné konvence následující příklad. Ten ověřuje, zda se podařilo extension metodě NthIndexOf najít správný výskyt znaku.

namespace Trinity.Tests.Extensions
{
    [TestFixture]
    public class StringExtensionsTests
    {
        /// <summary>
        /// Vrati cele cislo n-teho vyskytu pri mnoha vyskytech hledaneho prvku
        /// </summary>
        [Test]
        public void NthIndexOf_IntegerWhenExistsManyOccurences()
        {
            // Arrange
            string test = "Jmenuji se Miroslav";

            // Act
            int position = test.NthIndexOf("e", 2);

            // Assert
            Assert.That(position.Equals(9));
        }

Názvy projektů a složek

Pro jednotkové testy se zakládá samostatná C# library. Pokud je název mého testovaného projektu Trinity, testovací projekt se jmenuje Trinity.Tests. U větších projektů se pro každou C# library zakládá korespondující testovací library. U menších projektů lze od této praktiky upustit. Složky v testovacím projektu by měly odpovídat názvům složek v testovaném projektu. Jediný rozdíl je v tom, že testovaná třída má vždy suffix Tests.

V mém případě je z testu vidět, že testuji projekt Trinity, který má složku Extensions a v ní se nachází třída StringExtensions.

Jak pojmenovat testovací metody

V současné době existuje řada různých konvencí jak pojmenovat testovací metody. Já se snažím používat celkem jednoduchou konvenci NázevMetody_Scenar(). Díky tomu nemám půlmetrové názvy metod. Popis testu řeším komentářem, který je lidsky čitelnější.

Dát metodě název podle toho, co dělá obvykle není jednoduché. U testů je to ještě komplikovanější, protože se snažíme vystihnout aktuální situaci, scénář a očekávaný výsledek. Kdybych měl pojmenovat metodu v mém testu skutečně regulérně, asi by to vypadalo takto: NthIndexOf_ExistsManyOccurences_IntegerOfNthOccurence. Z mé zkušenosti není zdaleka tak důležité jak se testovací metoda pojmenuje jako dodržování jediné společné konvence v celém testovacím projektu.

Arrange, Act, Assert

V těle testovací metody se nachází tři komentáře. Každý z nich uvozuje určitou část životního cyklu testu. Ten se standardně rozlišuje na Arrange (příprava k provedení testu, mockování apod.), Act (samotné provedení testované jednotky) a Assert (testování výsledků). Řada vývojářů (a to i ti z Microsoftu) často různá A mezi sebou slučuje, což vede v konečném důsledku k horší čitelnosti a nepohodlnému debugování. V praxi to ale ničemu nevadí. Přehledné je tyto bloky pomocí komentářů skutečně uvádět.

Ostatní konvence

Pojmenování ostatních promenných, vlastností aj. už pouze vychází z běžných jmenných konvencí pro vybraný jazyk. Z vlastní zkušenosti doporučuji konečný výsledek k testování pojmenovávat jako result a během psaní testu nepoužívat var. Obojí výrazně zvyšuje čitelnost.

Asserty

Assert je statická třída, která umožňuje validovat výsledek proti určitému očekávání. Platí, že Assert lze napsat mnoha způsoby. Já preferuji čitelnost Assertu bez ohledu na to, zda existuje nějaká vhodnější metoda. V příkladu používám Assert.That(result.Contains("Number"), což můžu plynule přeložit jako Ověř.Že(výsledek.Obsahuje("Number").

Jen pro porovnání čitelnosti:

Assert.That(result.Contains("Number"));
Assert.IsTrue(result.Contains("Number"));
Assert.AreEqual(true, result.Contains("Number"));

A to ve všech případech používám mnou doporučený název proměnné result, který čitelnost také zvyšuje. Samozřejmě Assert.That je obecný a nemusí každému vyhovovat. Prakticky se dá bez něj obejít nebo ho použít na všechno. V některých testovacích frameworcích ho ani nenajdeme.

Naučte se testovací framework

Mnoho vývojářů přistupuje k testovacímu frameworku jako k něčemu, co umí pouze anotovat třídy, metody a následně použít nějaký ten assert. Speciálně NUnit je velmi mocný testovací framework, který poskytuje plno zajímavých funkcí, které umožňují psát velmi přehledné testy a realizovat scénáře, které byste jinak zbytečně zdlouhavě psali. Dobrý příklad je realizace různých testovacích scénářů.

[TestCase("word", "w o r d")]
[TestCase("na me", "n a m e")]
public string AddSpaces(string input, string expected)
{
	// Act
	string result = input.AddSpaces();

	// Assert
  	Assert.AreEqual(expected, result);
}

Připravte si dobré test cases

Při testování se vyplatí mít dobrá a stabilní testovací data. Vytvořte si virtuální testovací svět a používejte jej v celém projektu. Pokud testujete blogovací systém, vytvořte si testovací data skládající se z reálných článků, autorů i kategorií. Naučte vaše objekty tato data používat například pomocí extension metod ve vašem testovacím projektu.

Zpříjemněte si mockování

Speciálně mockování ukrajuje při psaní testů mnoho času. Pro testování controllerů v MVC aplikaci je opakujícím se scénářem mockování Requestu nebo HttpContextu. Nezřídka je nutné si sestavovat i vlastní RouteTable. Vytvořením bázové testovací třídy pro skupinu specifických testovacích tříd se dá ušetřit mnoho času.

Dejte tomu čas a nevzdávejte to

Psaní testů není zpočátku jednoduché. Je to jako když se začíná s úplně novou technologií. Neustále se řeší problémy, testy neustále červenají a rozbíjejí se. Je to ale dočasný stav, který s rostoucími zkušenostmi mizí. Opravování testů je stále snazší až nakonec vytvoříte stabilní testovací projekt, který budete jen rozšiřovat. Chce to ale kuráž. Červené testy nikdy neničte ale opravujte je. Jedině tak se naučíte s problémy vyrovnat, řešit je a nakonec jim i předcházet. Dělejte malé kroky. Nevzdávejte to.

Školení ASP.NET Core a Entity Framework Core

Budoucnost platformy .NET bude patřit technologiím ASP.NET Core a EF Core. Přijďte se naučit tyto moderní technologie používat na mém praktickém školení.

7.8.2017 - 9.8.2017 ASP.NET Core MVC
11.9.2017 - 12.9.2017 Entity Framework Core