Miroslav Holec
Premium

Jak začít psát unit testy

Miroslav Holec   5. května 2021  update 24. května 2021

Tento článek byl napsán v roce 2021. Vývojářské technologie se neustále inovují a článek již nemusí popisovat aktuální stav technologie, ideální řešení a můj současný pohled na dané téma.

Chcete začít psát unit testy pro webové služby na platformě .NET 5? V tomto článku se seznámíte se vším, co byste měli znát, než se do psaní testů pustíte. Hledáte-li nástroje pro psaní testů nebo uvažujete nad integračními testy, podívejte se také na další díly ze seriálu o testování.

Psaní testů na platformě .NET 5

Než se pustíme do konkrétních detailů, shrňme si, jak funguje psaní testů na platformě .NET 5. V první řadě velmi podobně jako tomu bylo ve starém .NET Frameworku nebo novějším .NET Core. Principy se nijak nezměnily. Máme existující solution s několika projekty a chceme k nim napsat testy, které se budou automaticky spouštět. Pro přehlednost je budu obecně nazývat coded testy. Má to dobrý důvod. Nástrojům je totiž jedno, zda je coded test ve své konečné podobě jednotkový, integrační nebo se jedná o benchmark.

Pro psaní testů si vývojář vždy zakládá nový projekt ve své solution. Platí nepsané pravidlo, že by pro každý testovaný projekt měl být projekt testovací. Je-li složená solution z 5 projektů, pak k nim vznikne 5 testovacích projektů, typicky s příponou .Tests. Struktura může být například:

Solution
- MyApplication.Web
- MyApplication.Web.Tests
- MyApplication.Lib
- MyApplication.Lib.Tests

U menších aplikací vývojáři občas vytváří jeden testovací projekt a dávají do něj vše. To se hodí ve scénářích, kdy si chcete psát testy spíše výjimečně na vybrané části aplikace a vytvářet samostatné projekty se vám nechce.

Testovací projekt

Zmínil jsem pojem testovací projekt. Ten si většinou založíte přes Visual Studio nebo pomocí příkazové řádky a nástroje dotnet. Můžete si vybrat z několika testovacích frameworků, ze kterých osobně doporučuji zvolit xUnit.

dotnet-new-test-frameworks

Tím se založí projekt s velmi specifickým nastavením, které si můžete prohlédnout v csproj souboru. Zejména bych zmínil, že:

  • jako SDK je použito Microsoft.NET.Sdk
  • jako target framework je vybrán running framework, například net5.0
  • jsou přidány NuGet balíčky dle zvoleného testovacího frameworku

To je nastavení, se kterým si zřejmě nevystačíte. Téměř s jistotou budete potřebovat další užitečné nástroje. V článku "Nástroje pro psaní Unit Testů v .NETu" se dočtete o nástrojích, které se Vám pro psaní budou hodit.

Testovací projekt je ve své podstatě projekt jako každý jiný. Nebude obsahovat pouze samotné testy ale i podpůrnou infrastrukturu. Při psaní testů budete chtít stejně jako u jiného programového kódu zachovat jednoduchost a čitelnost. K testům se budete často vracet včetně kolegů a budete je potřebovat na první pohled pochopit. I z toho důvodu psaním testů strávíte mnoho času. O tom ale ještě později.

Unit Test

Máte-li připravený projekt se všemi nástroji, můžete se pustit do psaní prvních testů. Jejich organizace by měla odpovídat organizační struktuře složek v analogickém projektu. Je zvykem všechny testované části aplikace suffixovat slovem Tests.

Solution
- MyApplication.Lib
- - Services
- - - MyService.cs
- MyApplication.Lib.Tests
- - Services
- - - MyServiceTests.cs

Myšlenka je taková, že k jedné naprogramované třídě je vytvořena třída testovací. Má-li daná třída 2 metody, pak v testovací třídě bude při plném pokrytí minimálně tolik testů, jaký by byl součet cyklomatické komplexity všech metod. Když tedy naprogramujete 2 metody a každá má cyklomatickou komplexitu 2 (v každé je například jeden IF), pak s vysokou pravděpodobností budete mít napsané minimálně 4 testy. Nelze to však brát dogmaticky. Občas nebudete možná chtít testovat všechny cesty a občas na jednom průchodu budete chtít otestovat více věcí.

Názvy samotných testů (čili testovacích metod) mají historicky různé konvence. Důležité je vybrat si jednu, která Vám bude vyhovovat a dodržovat ji v celém řešení. Zapomeňte na rady z dokumentace MSDN. Praxe je taková, že i různé týmy vývojářů v Microsoftu mají různé konvence pro názvy testů. Příkladem budiž vybrané názvy testů k Entity Framework Core:

Detects_ToQuery_on_derived_keyless_types()
Instantiation_on_type_in_internal_namespace()
Detects_missing_id_property()
Passes_on_valid_keyless_entity_type()

Názvy nejsou podstatné, důležitý je kód.

Struktura Unit Testu

Při psaní konkrétního testu platí určitá pravidla. Průběh testu si lze rozložit akademicky na 3 části, přičemž každá popisuje fázi průběhu testu:

  • Arrange - příprava tříd a prostředí pro testování
  • Act - samotné otestování jednotky, například provoláním testované metody
  • Assert - vyhodnocení očekávání vůči skutečnosti

Není v tom žádná magie, protože běžný programátorský kód funguje stejně. Připravíte si data nebo prostředí, provedete potřebnou logiku a vrátíte výsledek.

Slova Arrange, Act a Assert do testu nepatří - nemá to smysl, protože:

  • Assert je vždy na konci metody
  • Act je jeden řádek těsně před Assertem
  • Všechno ostatní je Arrange

Kromě toho fáze Arrange většinou zasahuje mimo test metodu. Příprava dat před provedením testu je často součástí tzv. fixture (společné pro sadu testů). Nechceme přeci pro každý samostatný test neustále dokola chystat stejné prostředí.

Aby byl test čitelnější, je vhodné dodržovat určitá pravidla. Pro další text si navíc zavedeme nový pojem SUT (System Under Test). SUT označuje část systému, kterou testujeme. Zbytek prostředí se většinou snažíme nějak stabilizovat, aby se chovalo dle očekávání. Obvykle pro tento účel vytváříme různé druhy fake objektů. Z tohoto důvodu je vhodné testovanou třídu pojmenovat jako proměnnou sut, aby bylo na první pohled jasné, kde se v testu provádí Act.

V jednom unit testu by měl být pouze jeden Assert. Jedna metoda testuje jednu věc. I proto by měla být cyklomatická komplexita testu rovna 1. Konstrukce IF, ELSE, SWITCH a podobné do testů zásadně nepatří.

Code Coverage

Přijde Vám to složité? Máte naprostou pravdu. Psaní testů je zejména ze začátku stejně náročné jako nástup do nového projektu. Psaní testů má určitá pravidla a zabere Vám mnoho času. Různé studie se shodují, že s ohledem na míru pokrytí projektu testy se prodlouží čas vývoje dané aplikace o 40 - 60%. Více času strávíte u unit testů na začátku, než si kolem nich vybudujete vlastní infrastrukturu. Poté je již přidávání nových testů rychlejší.

Několikrát jsem již zmínil pojem pokrytí testy. V různých IDE se jedná o značení Code Coverage a jedná se o údaj (v procentech), který značí míru pokrytí testy v dané solution / projektu / složce. Samotné číslo bývá poměrně zavádějící, protože tooling často mylně zahrnuje do pokrytí i průchody, které se testují v rámci jiných scénářů. Jejich aranžmá totiž často pokryje něco, co ve skutečnosti otestované není.

Jak velkou část kódu pokrýt? Tuto otázku si budete muset v týmu odpovědět. Míra pokrytí je vždy kompromisem. Na jedné straně je čas strávený psaním testů a na straně druhé je snížení rizika rozbití projektu. Nefunkční část webové prezentace je nepříjemná, ale není většinou spojená s žádnou ztrátou. Nefunkční nákupní košík u e-shopu s milionovým obratem denně už problém je. Nakonec se hodí zmínit, že ani pokrytí projektu testy nemusí ještě znamenat bezchybnost implementace. I při psaní testu můžete udělat chybu nebo opomenout některý méně předpokládaný scénář. Koneckonců únor má 29 dní jen jednou za 4 roky, pokud zrovna není rok dělitelný 100 a nedělitelný 400. O tom ale programování zkrátka je.