Když zelené testy nahradí dobrý návrh
U TDD se často říká, že vede k lepšímu návrhu. Nejdřív test, pak implementace, nakonec zelený build. Zní to disciplinovaně. Čistě. Profesionálně.
Jenže právě v tom je i jeho nechtěný efekt: TDD velmi snadno mění definici hotovo.
Místo „kód je zjevně správný, čitelný a dobře navržený“ začne hotovo znamenat „testy jsou zelené“.
A to není totéž.
TDD nevyrábí jen testy. Vyrábí i určitý způsob myšlení
Ten postup známe:
Napíšu testy. Snažím se, aby byly zelené. Když jsou zelené, mám hotovo. Když něco nesedí, upravím implementaci tak, aby to prošlo.
V normální podobě to zní nevinně. V přehnané podobě už je ale dobře vidět, kam ten tlak míří: ne k co nejzřejměji správnému kódu, ale ke kódu, který je dostatečně podepřený testy, aby se tvářil funkčně.
To není obvinění, že TDD vždy vede ke špatnému kódu. Je to upozornění, že optimalizační tlak jde jinam, než se často tvrdí.
TDD systematicky zvýhodňuje jednu otázku:
Co ještě musím upravit, aby testy prošly?
Jenže kvalitní návrh často začíná jinou otázkou:
Jak napsat kód tak, aby z něj bylo co nejvíc vidět, že je správný?
Dva mentální póly
Je užitečné si představit dva extrémy.
První pól je tento:
Hlavně ať jsou testy zelené. Kód doladím tak, aby vyhověl testovací sadě.
Druhý pól je tento:
Musím napsat tak dobrý kód, aby byl sám o sobě co nejčitelnější, nejjednodušší a co nejzřejměji správný.
V čisté podobě se v praxi skoro nevyskytuje ani jeden. Ale právě proto jsou užitečné. Vymezují prostor, ve kterém se opravdu pohybujeme.
A TDD nás často táhne směrem k prvnímu pólu.
Ne proto, že by to bylo jeho deklarovaným cílem. Ale proto, že dává velmi silný signál hotovosti: build je zelený, tedy je hotovo.
Zelené testy nejsou důkaz dobrého návrhu
Tohle je podle mě klíčový bod.
Zelené testy dokazují, že implementace uspokojila určitou testovací sadu. Nedokazují, že je návrh dobrý. Nedokazují, že je kód srozumitelný. Nedokazují, že je jednoduchý. Nedokazují, že budou budoucí změny levné.
A přesto se v praxi tyto věci velmi často směšují.
Testy prošly, takže je to správně. Testy prošly, takže je to dobře navržené. Testy prošly, takže můžeme přestat přemýšlet.
Jenže právě tady vzniká falešný pocit hotovosti.
Kód může být lokálně otestovaný a zároveň:
- strukturálně nepřehledný,
- plný pomocných vrstev jen kvůli testovatelnosti,
- rozlámaný na rozhraní, která doménově nedávají smysl,
- závislý na call-flow, které bude při příštím refaktoru překážet.
Zelené testy a dobrý návrh spolu mohou souviset. Ale jedno není důkazem druhého.
TDD zvýhodňuje to, co se dobře testuje jako první
A to je další problém.
Nejlépe se test-first stylem píše malá, izolovaná logika. Funkce. Transformace. Výpočet. Validace. Tam TDD dává smysl a je fér mu to přiznat.
Jenže běžný projekt se často nerozbíjí hlavně tam.
Rozbíjí se:
- na rozhraních mezi vrstvami,
- ve změně shape dat,
- v implicitních očekáváních okolí,
- v kontraktech mezi částmi systému,
- v návaznostech, které lokální unit test nevidí.
A právě to se TDD test-first stylem chytá mnohem hůř.
TDD tedy často nevyrábí testy, které nejlépe chrání projekt. Vyrábí testy, které se nejlépe píšou před implementací.
To je zásadní rozdíl.
A potom už máme unittesty hotové?
Tohle se často uvádí jako výhoda TDD.
„Když píšeme testy předem, máme je pak automaticky hotové. Nemusíme je dopisovat.“
Jenže to je výhoda jen tehdy, pokud právě tyto testy mají dlouhodobou hodnotu.
A tady se vrací problém z předchozího článku: v běžných projektech bývá dlouhodobá hodnota mnoha unittestů systematicky přeceňovaná.
Jinými slovy: TDD nám skutečně pomáhá rychle vyrobit unittesty. Jenže pokud je právě dominance unittestů součást problému, pak to není automaticky výhoda. Je to spíš efektivnější výroba toho, čeho už máme příliš mnoho ve špatném poměru vůči zbytku testovací sady.
Získáváme tedy benefit, který v tom okamžiku už benefitem být nemusí.
Ano, máme testy hotové. Ale možná máme hotové hlavně ty testy, které projekt dlouhodobě chrání méně, než si myslíme.
TDD může deformovat i architekturu
Tohle bývá vidět méně, ale je to důležité.
Když je hlavní tlak na to, aby šel kód pohodlně testovat po malých izolovaných jednotkách, začne se architektura přizpůsobovat této potřebě.
Vznikají rozhraní, která nejsou doménově přirozená, ale dobře se mockují. Vznikají vrstvy, jejichž hlavní hodnotou je, že jdou v testu oddělit. Vznikají malé třídy a malé metody ne proto, že by to zlepšovalo model systému, ale proto, že se na ně dobře píše test-first.
Výsledkem pak může být architektura optimalizovaná na testovatelnost, ne na srozumitelnost nebo robustnost.
To je vysoká cena.
Protože systém nežijeme jen při psaní testů. Žijeme s ním při údržbě, čtení, refaktoru a změnách.
TDD je silné. Jen v užším prostoru, než se tvrdí.
Aby to bylo fér: TDD není nesmysl.
Je silné tam, kde člověk právě tvoří malou logiku a potřebuje rychlou zpětnou vazbu. Pro parser, výpočet, převod dat, hraniční podmínky nebo malé pravidlo to může být výborný nástroj.
Tam opravdu pomáhá.
Jenže z toho ještě neplyne, že je to dobrý obecný model pro návrh běžného projektu. Už vůbec z toho neplyne, že TDD automaticky produkuje lepší architekturu nebo lepší dlouhodobou ochranu systému.
TDD dobře pomáhá s lokální správností. To ale není totéž jako dobrý návrh. A už vůbec to není totéž jako ochrana proti regresím v systémových návaznostech.
Co je praktičtější přístup
Nepotřebujeme další ideologii. Potřebujeme lepší otázky.
Ne „jak to udělat test-first“. Spíš „co se tady může rozbít a jak to nejlépe odhalíme“.
Ne „už jsou testy zelené“. Spíš „je ten kód sám o sobě přesvědčivý, čitelný a zjevně správný“.
Ne „máme už unittesty hotové“. Spíš „chrání ty testy opravdu to, co bude při příští změně bolet“.
To znamená dvě věci najednou:
První: netlačit všechno do TDD rytmu jen proto, že to vypadá disciplinovaně. Druhá: neplést si průchod testy s kvalitou návrhu.
Testy jsou nástroj. Ne důkaz elegance. Ne důkaz správné architektury. Ne důkaz, že už nemusíme přemýšlet.
Závěr
TDD není problematické proto, že vede k testům. Je problematické tam, kde se průchod testy začne plést s důkazem dobrého návrhu.
Jeho nechtěný efekt je jednoduchý: mění definici hotovo. Místo zjevně správného a dobře navrženého kódu začíná stačit kód, který uspokojí testovací sadu.
A to je mnohem nižší laťka, než se tváří.
Pokud má mít software dlouhý život, nestačí, aby byl otestovaný. Musí být také čitelný, přesvědčivý a konstrukčně poctivý. Ve chvíli, kdy zelené testy začnou tyto vlastnosti nahrazovat, přestává být TDD disciplínou návrhu a stává se disciplínou uspokojování testů.