Proč vlastně testovat?
Odpověď je nasnadě – kód píší lidé a lidé chybují. Jsem si vědom toho, že touto větou jsem se jistě citelně dotkl ega některých čtenářů, ale berte to tak, je to pravda. Ano i vy, i kolega sedící vedle vás, za vámi, před vámi, všichni chybujeme. Příčin chybování je obrovské množství. Jsme unaveni, nemáme dobrou náladu, někdo nás vyrušil, chvátáme domů, přecenili jsme svoje síly, šéf či kolega nás vytočil, atd. atp. Dovolím si tvrdit, že počet všech možných situací, které mohou vést k chybě v kódu, se limitně blíží nekonečnu.
Bohužel i přesto, že s předešlým odstavcem drtivá většina (nejen) programátorů souhlasí, je občas obhajoba přínosu testování podobná boji s větrnými mlýny. Je to škoda, protože tato diskuse by měla znít spíše opačně – ti, kdo se testování svého kódu nevěnují, by měli věrohodně obhajovat proč tak činí.
Proč netestovat?
Za celou dobu, co se v PHP zabývám testováním a vůbec tvorbou testovatelného kódu, jsem posbíral několik zajímavých anti-testing argumentů:
1) žere to moc času…
… který můžu raději věnovat psaní kódu. Toto je asi nejčastější z nich a zaznívá tak půl na půl z úst programátorů a managerů. Těm prvním „se to nechce psát“, ti druzí nechtějí čas, který zabere psaní testů, platit. Ano, tvorba testů zabírá určitý čas, ale tento čas je investicí, která se zatraceně vyplácí. A tím nemyslím jen fakt, že veškeré chyby jsou objeveny a opraveny včas, často ještě před prezentací produktu zákazníkovi. Myslím tím i to, že testy jsou jakýmsi průvodcem tvorby kvalitnějšího kódu. Čím více času investujete do psaní testů, tím méně potom budete potřebovat na refaktoring, který si vyžádá nějaká změna.
2) ještě na to nebyl čas
Celou větou – my víme, že je to správné, ale zatím jsme se k tomu nedostali. Touto větou se budete hájit tak dlouho, než si uděláte opravdu velkou ostudu. A jen ostuda to bude v tom lepším případě. I když může vyústit v diskreditaci firmy, pro kterou pracujete. Nedokážu si představit, jak původní větu přetavit ve smysluplnou omluvu faktu, že účetní data e-shopu, který jste programovali, je možné kompletně vyhodit. Finanční úřad nepatří mezi ty chápavé a laskavé.
3) kód mé aplikace je příliš složitý
Snadná odpověď – pak je ten kód špatný. Dobrý kód = testovatelný kód. Zjistíte to velmi záhy, když budete potřebovat provést nějakou netriviální změnu. Ego „příliš složitého“ kódu pro testování bude rázem pryč a vy se budete potit při rozplétání nekonečné pavučiny závislostí.
4) snad vím, co dělám, ne?
Pozor, vstupujete do nebezpečné skupiny programátorů „všeználků“. Toto si ani nezaslouží komentář. Pokud máte v týmu podobně světového člena, zbavte se ho, o jeho výtvory se pak budete muset starat sami.
5) je to nuda
Věřte, že není. Nejen, že budete nuceni občas překonávat skutečně záludné překážky, ale jakmile si testování zažijete, záhy zjistíte, že díky němu píšete lepší kód. Jestli vám ani jedno nic neříká, pak pracujete ve špatném oboru.
Určitě zaslechnete i mnohé další anti-argumenty, jednou společnou radou proti nim by mohlo být – řiďte se vlastním úsudkem a vlastním svědomím. Pokud se rozhodnete pro cestu testovatelného kódu, nebudete litovat.
Druhy chyb
Tak jako každé rozsáhlejší téma, i testování vyžaduje určité teoretické znalosti, o které se v budoucnu budete moci opřít. Tento, úvodní, díl seriálu bych proto chtěl věnovat výtahu toho, co považuji okolo testování za nejdůležitější. Nejprve se pojďme podívat s jakými protivníky budeme mít tu čest.
Syntaktické chyby
Neboli prohřešky proti syntaxi jazyka – chybějící středník, závorka, … Tyto chyby jsou jedny z „nejhodnějších“, protože vás na ně upozorní už kompilátor a díky tomu nepůjde kód vůbec spustit. Tudíž nebude moci napáchat žádné škody.
echo "foo" echo "bar";
Sémantické chyby
Prohřešky proti sémantice jazyka. To je například špatné pořadí parametrů, špatný typ parametru apod. Tady už je situace horší. Ve většině případů nás na podobné chyby upozorní nějaká varovná zpráva nebo upozornění za běhu, ale to už bývá zoufale pozdě. Nehledě na to, že můžete mít zobrazování těchto zpráv potlačeno. Tyto chyby už mohou napáchat opravdu velké škody.
$joinedValues = "one, two, three"; $separateValues = explode($joinedValues);
Logické chyby
Jedny z nejhůře odhalitelných chyb, na které ve velké spoustě případů nebudete nijak upozorněni a jejich řádění může mít fatální následky. Patří sem chyby v cyklech (nedostatečný nebo přílišný počet iterací), chybné používání operátorů, neúmyslné přetypování proměnných a mnohé další.
$values = array(1, 2, 3, 4, 5); for ($i = 1; $i < count($values); $i++) { echo $values[$i]; }
Pomineme-li syntaktické chyby, které jsou nejsnáze odhalitelné, na oba nebezpečné druhy chyb bychom měli být dobře připraveni a v ideálním případě být schopni je odhalit ještě před ostrým spuštěním kódu. A to je přesně ta chvíle, kdy na scénu vstupují naši nejlepší kamarádi jménem testy, které nás na podezřelý kód včas upozorní. Pokud ještě navíc přeruší proces nasazení kódu do produkčního prostředí, tak jen lépe!
Úrovně testování
Jednotkové testy
Nejnižší možná úroveň, zde testujeme izolovaně a samostatně jednotlivé třídy. Na této úrovni nás zajímají především správné návratové hodnoty metod v závislosti na vstupních parametrech. Při jednotkovém testování bychom se měli snažit o maximální izolaci testované jednotky, měli bychom se vyvarovat:
- volání metod jiných skutečných tříd
- přistupování k síti (internet, intranet)
- používání databází
- používání filesystému
- atd…
Pro simulaci těchto činností se používají falešné objekty, které nevykonávají žádnou skutečnou činnost, pouze splňují nějaké požadované rozhraní. Tyto objekty se označují jako „mocks“ nebo „stubs“ (příp. ještě „fakes“) a bude o nich řeč později.
Integrační testy
Na této úrovni testujeme, jak spolu naše třídy nebo moduly navzájem spolupracují, testujeme, zda jeden modul splňuje požadavky jiného. Oproti jednotkovým testům zde už můžeme používat databázi, filesystem apod. protože nám jde především o testování spolupráce. Jako příklad integračního testu si můžeme představit test, který nám ověřuje funkčnost nového modulu, který pracuje s CouchDB namísto původního MySQL.
Funkční testy, akceptační testy
Nejvyšší úroveň testování, která se mnohdy odehrává na úrovni GUI, testujeme korektnost chování celé aplikace. Funkční testy jsou většinou automatizovány pomocí nástrojů jako je Selenium, Squish a jiné, akceptační testy mohou být i manuální a provádí je produktový manažer nebo zástupce zákazníka.
Regresní testy
Regresní testy nejsou samostatnou úrovní testování. Důvod je prostý – regresní testy mohou prostupovat všemi úrovněmi testování. Jejich významem je testování úspěšnosti opravy nalezené chyby a hlavně – její neopakovatelnosti. Pokud nalezneme v kódu chybu, pak v ideálním případě bychom měli nejprve napsat test (regresní test), který bude umět chybu vyvolat a teprve potom chybu opravit. Jen tak budeme mít jistotu, že chyba je opravena správně. Kromě toho nám regresní test bude hlídat, že si chybu do kódu nezavlečeme znovu (např. jinou úpravou nebo opravou jiné chyby).
Metody testování
Kromě úrovní testování existují i metody jak testovat, resp. jak přistupovat k testování.
Blackbox testing
Jak už název napovídá, testujeme jakoby černou skříňku. Známe pouze veřejné rozhraní této skříňky a ověřujeme, zda dělá to, co má. Zdrojový kód neznáme. Tato metoda se hodí na testování od úrovně integračních testů výše. Na unit testy není moc vhodná, protože bez znalosti zdrojového kódu nebudeme schopni metody pokrýt dostatečným množstvím testovacích případů.
Whitebox testing
Testujeme přímo kód třídy nebo konkrétní metody. Pokud nepostupujeme metodou Test-driven development (nejprve píšeme testy a kód až následně), tak píšeme testy „na míru“ kódu, tzn. snažíme se o co nejlepší pokrytí kódu testy. Už z předešlé věty je jasná největší nevýhoda této metody („pasujeme“ testy na kód, takže můžeme leccos přehlédnout), ale i přesto bych ji pro unit testy doporučil více než blackbox testing, protože při znalosti kódu jsme schopni vytvořit mnohem více testovacích případů, protože jednoduše vidíme, co metoda dělá.
Pravidla pro testy
Jak se říká – to nejlepší na konec – poslední důležitou kapitolou, kterou bych v tomto úvodním díle rád uvedl, jsou pravidla pro psaní vlastních testů. Tato sada pravidel má pět částí a souhrnně se označuje jako „pravidlo F.I.R.S.T.“ a měla by pro vás být alfou a omegou. Jak si totiž ukážeme později, na mnohá další pravidla a principy dobrého návrhu přijdeme díky této pětici víceméně sami.
Fast – rychlé
Testy by měly být rychlé. Budou-li vaše testy trvat dlouho, pak se vám ani dalším vývojářům nebude chtít je spouštět. A pokud je nebudete spouštět, pak vám nebudou k ničemu.
Independent – nezávislé
Testy by měly být nezávislé – nejen samy na sobě, ale také na prostředí, ve kterém jsou spouštěny. Nesmí dojít k situaci typu: test A nastaví podmínky pro test B, jinak test B neprojde. Závislost testů také může způsobit lavinu „failů“, ze které bude poměrně složité určit prvotní příčinu selhání.
Repeatable – opakovatelné
Stejně jako nezávislé, tak by testy měly být i opakovatelné. A opět – měly by být opakovatelné v jakémkoli prostředí. Test, který dvakrát projde a potřetí selže, je k ničemu.
Self-validating – samovyhodnocující
Za tímto pravidlem se skrývá prostá myšlenka – test by měl buď projít nebo selhat. Žádné romány v konzoli nebo v logu, nad kterými budete trávit hodiny, abyste stanovili, zda test prošel nebo ne. Buď OK nebo FAIL, nic jiného.
Timely – aktuální
Testy bychom měli psát (když ne ještě před vlastním kódem) co nejdříve po napsání ostrého kódu. Jen tak budeme schopni „stíhat“ sledovat, co se v kódu odehrává. Psát testy dodatečně s odstupem několika měsíců opravdu není nijak příjemná práce, nehledě na výslednou kvalitu testů.
Ač tato pětice pokrývá většinu potenciálních problémů, dovolil bych si ji ještě rozšířit o vlastní doporučení:
Krátké
Testy, konkrétně jednotlivé testovací metody, by měly být co nejkratší. Jednak se nikomu nebude chtít studovat test zabírající desítky řádek, kromě toho, jsme-li nuceni vytvořit test, kde desítky řádek slouží k nastavení prostředí pro běh testu, pak zaručeně porušujeme nějaké pravidlo dobrého návrhu nebo dobrého testu (viz. F.I.R.S.T. výše). Puristé tvrdí, že testovací metoda by měla obsahovat pouze jeden assert. To je už malinko extrémistická myšlenka – dovolím si tvrdit, že při tomto přístupu velice brzo narazíte na jeden ze dvou skutečně nejsložitějších problémů v IT – pojmenování (druhým je cache-invalidation :-).
Prosté
Testovací metody by neměly obsahovat žádnou řídící logiku. Mám na mysli podmínky, cykly, apod. Pokud test obsahuje logiku, pak je vysoká pravděpodobnost, že budeme mít chybu v testu, a to je, jak už jsme si řekli, nejhorší, co se nám může stát.
Na téma psaní dobrých testů by se daly napsat celé obsáhlé knihy, ale tím bychom se dostali daleko mimo rámec tohoto seriálu, který vás má především seznámit se základy testování v PHP. Příště už to bude trochu zábavnější a podíváme se na základy práce s testovacím frameworkem PHPUnit.
Přehled komentářů