Testovat je lidské
28.08.2019
Testovat je lidské

Při troše štěstí byste mohli mít z minulého dílu pocit, že test-driven development (TDD) není úplně špatný nápad; v ideálním případě ho v tuto chvíli už považujete za životní nutnost (podobně jako deodorant v open spacu nebo neomezenou konzumaci na firemním večírku), takže nebudu testovat (mrk mrk) vaši trpělivost dlouhým úvodem a rovnou půjdeme rozmetat některé testovací mýty.

ěnčapo ej onhcešV

„U nás testy nepíšeme, protože nemáme čas."

Totální evergreen, oblíbený zejména tam, kde se testy píší až po implementaci produkčního kódu. Ono vskutku moc nedává smysl stavět lešení až okolo dokončeného domu. Časová náročnost správně provedených testů je ovšem záporná, čili ve skutečnosti nemáte čas, protože nepíšete testy. Ano, naprosto vážně vám tu tvrdím, že napsat více kódu je rychlejší než napsat méně.

Produktivitu vývojáře samozřejmě nelze měřit jako počet úderů za minutu × 60 × 8. Troufám si tvrdit, že větší část z pracovní doby zaberou (kromě obligátního fotbálku a dohadování, kam na oběd) činnosti jako komunikace s kolegy, manažery a zákazníky, návrh struktury nové funkcionality, zkoumání dokumentace, psaní dokumentace, refactoring (doufejme) a debugging (bohužel). Testy za vás hovor se šéfem nebo zákazníkem nevyřídí, ale všechny ostatní uvedené věci dokážou řešit lépe!

Tak například design nové funkce: I jednoduchá featura je většinou tak implementačně složitá, že není možné ji pojmout celou předem. Takové pokusy v extrémním případě vedou k neblaze proslulé analysis paralysis, česky k tzv. záseku. Unit testy psané popředu přitom nabízí elegantní řešení: Začni z kteréhokoliv konce, zabývej se pouze izolovanými částmi a překvapivě kvalitní design se časem vyloupne téměř sám od sebe.

Nebo taková dokumentace - většinou přežívá předaleko od kódu, kterého se týká (ideálně za sedmero servery a sedmero přihlášeními v sedmém kruhu pek… ehm, firemní wiki), nikoho nebaví ji psát a už vůbec ne udržovat, takže časem začne lhát, tudíž jí lidé přestanou věřit a ve finále ji nikdo nečte. Testy jsou přitom nejlepší dokumentace, protože tahle forma je vždycky přesná (testy jsou spustitelný program), aktuální (protože jinak by ani nešly spustit) a jednoznačná (jako každý program).

Refactoring je vyloženě křiklavý případ: Pokud se přidržíme klasické definice, která zní, že refactoring je změna struktury programu, která nemění jeho externí chování, tak je jasné, že pro bezpečný (a zároveň i rychlý) refactoring potřebujeme záruku, že v jeho průběhu ono externí chování nezměníme (tj. nerozbijeme) - a tohle přesně zajistí automatizované testy s kvalitním pokrytím. Nebo z druhé strany: Pokud není možné v průběhu práce na projektu kdykoliv a neustále spolehlivě provádět drobné i větší refactoringy, časem se z něj stane zamotaná, nepřehledná hmota s neexistující architekturou, se kterou se nikdo nechce špinit - legacy kód jako vyšitý.

V tuhle chvíli je už asi i zřejmé, jak je to s debuggingem. Pokud máte sadu vhodně napsaných testů (izolovaných, dobře zacílených) s kvalitním pokrytím, počet okamžiků, kdy budete zírat do debuggeru, škrábat se na hlavě a nemít ani ponětí, jak ve vašem programu vznikl ten obří kouřící kráter, se začne blížit k nule. Váš debugger skončí zaprášený hluboko v menu vašeho IDE, ale v tomhle případě to není špatně.

Testy jsou kód, který se nikdy nedostane až ke koncovému uživateli. Někdo by tím pádem mohl naivně tvrdit, že jsou jaksi navíc nebo zbytné. Jak ale vidíme, testy zrychlují implementaci nových funkcí, udržují projekt v dobré kondici díky bezpečnému a rychlému refactoringu, slouží jako nejlepší možná dokumentace a téměř eliminují debugging. Tohle všechno znamená jediné - profit. Software je v mnoha ohledech zvláštní, občas paradoxní věc, a v tomto případě se nevyhnutelně dostáváme k závěru, který se vzpírá našim zkušenostem z „hmotného" světa: Vyrobit netriviální software s vysokou interní kvalitou je ve skutečnosti levnější.

„Nepíšeme testy, protože X se často mění."

V ideálním světě by neexistovali nerozhodní lidé, nejednoznačné požadavky a rozhraní s více než jednou verzí. Občas se ale prostě stane, že vnější faktory ovlivňující projekt jsou stabilní asi jako vládní koalice, to ale neznamená, že takový projekt nelze testovat!

Takže: Právě proto, že se X často mění, píšeme testy. Kolem onoho nestabilního, nepředvídatelného X, je třeba si postavit takovou hezkou vysokou zeď s dobře kontrolovaným místem pro průchod, protože DATA IS COMING (ano, Game of Thrones reference; pokud tohle čtete v roce 2020 nebo později, vůbec nevíte, o co jde).

Tohle vymezení pak umožní aplikaci rozdělit na dvě části, jednu menší, která se potýká s daty přicházejícími z divočiny (a tu důsledně otestovat; i když se bude často měnit, náklady na úpravy budou minimalizované), a na mnohem větší část, která bude stabilní (a i tu samozřejmě důsledně otestovat). Jak jsme si ukázali výše, důsledkem dobře aplikovaného TDD jsou kromě testů (no tohle!) právě i emergentní, překvapivě hezká rozhraní mezi částmi systému. Za zdí kromě zdivočelých HTTP requestů, nenormalizovaných databází a berserk byznysové logiky může být třeba i samotné UI. Hranice a testy na nich oceníte v každém případě.

„Při velikosti našeho projektu nemá cenu psát testy."

Je to takový argument žáby v hrnci, ve kterém se postupně zvyšuje teplota vody. Ve francouzské restauraci to možná funguje, při vývoji softwaru vám z toho bude akorát časem nevolno. Jak vůbec definovat „malý" projekt, který zatím nemá cenu testovat?

Počtem řádků kódu? Existuje bezpočet algoritmů, jejichž zápis je extrémně stručný, ale logika velmi složitá (ano, v takovém případě budou k nim příslušné testy výrazně delší než produkční kód - to je normální).

Nebo délkou vývoje? Kdy naposledy jste zažili projekt, který skončil tam, kde bylo původně naplánováno, nebo který se následně nerozvíjel nad původní záměr?

Co takhle podle počtu vývojářů? Nikdy není možné udržet v hlavě naráz celý projekt (tenhle okamžik přichází překvapivě rychle) a i v případě one man show je ve skutečnosti autorem projektu několik různých lidí: Libor právě teď, Libor včera, když došlo kafe, Libor ve čtvrtek ráno po nečekaném středečním večírku, Libor v pátek po obědě...

Další nebezpečí tohoto přístupu je v tom, že až (nikoliv když!) se projekt rozroste nad míru, kdy přestane být „malý" (ať už to znamená cokoliv), a autoři konečně dospějí do stavu neodvratné touhy po testech, je jednak problém testy dopsat zpětně do už existujícího kódu a druhak takové testy i produkční kód mají evidentně horší kvalitativní vlastnosti oproti variantě, kdy se testy píší dopředu.

Software je v mnoha ohledech zvláštní, občas paradoxní věc, a v tomto případě se nevyhnutelně dostáváme k závěru, který se vzpírá našim zkušenostem z „hmotného" světa: Na velikosti nezáleží.

„Testy nepíšeme, protože blá blá."

Za blá blá si dosaďte cokoliv z následujícího: „Protože je po nás nikdo nechce," „protože nebyly ve specifikaci," „protože nebyly ve sprintu," „protože management nechce" a tak dále a tak podobně. Minimálně unit testy jsou neoddělitelnou součástí konstrukce kvalitního (což jak už víme znamená levnějšího) softwaru, takže nemá význam je explicitně plánovat, protože se prostě píší neustále. Zároveň nikdy nesmíte prosit o povolení dělat svou práci pořádně, čímž se elegantně zbavujeme i manažerského faktoru.

Další oblíbené blá blá je „protože to není naše práce," resp. „protože máme testery." Toto je problém druhu a hierarchie testů. Testeři nemohou nahradit testy psané vývojáři (nebo dalšími profesemi) a naopak. Ve zdravém projektu je potřeba více druhů testů ve správném poměru a na to se už opravdu podíváme příště, slibuju!

No a pak tu máme poměrně fatalistické „protože v projektu žádné nebyly, když jsme k němu přišli." Tohle je nezáviděníhodná situace. Zpětné pokrytí rozměrného kódu je upřímně řečeno namáhavá, dlouhodobá práce, která vyžaduje speciální techniky. Je to vlastně klasický problém slepice vs. vejce s tím, že slepice i vejce pochází z pekla: Abychom mohli do legacy kódu přidat testy, je potřeba ho nejdřív refactorovat; abychom ho mohli bezpečně refactorovat, potřebujeme testy. Doplnění testů do takového projektu je ale taky bohužel jediná šance, jak s ním pohnout kupředu a nenechat ho zhroutit se pod vlastní vahou.

To jsou paradoxy, pane Turing!

Nejtěžší věcí na TDD není implementace testů, ale vůbec implementace změny myšlení v hlavách vývojářů a dalších zainteresovaných lidí. Principy, ale i efekty TDD jsou často neintuitivní, paradoxní a jdou kolikrát přímo proti zažité (a bohužel stále většinové) praxi, takže počáteční odpor, byť třeba i jen podvědomý, bývá silný.

Jedinou cestou, jak se ale dobrat všech benefitů, které správně prováděné TDD přináší, je vytrvat. TDD je jedna z věcí v životě, kterou je potřeba zažít na vlastní kůži. Na druhém břehu čeká elegantní, spolehlivý a levný software, na kterém je radost pracovat.

Jiří Hutárek