Testing

Pyramida, unit/integration/e2e, mocky, TDD, flaky testy.

Test je kousek kódu, který automaticky spustí jiný tvůj kód a ověří, že dělá, co má. Zní to jako práce navíc, ale je to přesný opak — testy ti dávají rychlost a klid. Bez nich po každé změně ručně proklikáváš celou appku a doufáš. S nimi měníš kód odvážně, protože tě síť chytí. A v době, kdy se kód generuje rychle, jsou testy to, co odlišuje „vypadá to OK" od „funguje to".


Pár pojmů na úvod

  • Test = automatizovaná kontrola: „když zavolám tuhle funkci s tímhle vstupem, má vrátit tohle".
  • Refaktor = přepsat kód do lepší podoby, aniž bys změnil, co dělá navenek.
  • Regrese = když se znovu objeví chyba, která už jednou byla opravená.
  • Edge case (hraniční případ) = neobvyklý vstup, kde se to často láme (nula, prázdno, maximum, neplatná hodnota).
  • Happy path = běžný, bezproblémový průchod (všechno je v pořádku); opak = chybové cesty.

Proč vůbec testovat

  • Klid při změně — přepíšeš kód a za pár vteřin víš, jestli jsi něco rozbil.
  • Živá dokumentace — z testů je vidět, jak se kód má používat a co dělá při chybě.
  • Tlak na lepší návrh — kód, který jde špatně otestovat, bývá špatně navržený (moc provázaný → viz Architektura a dependency injection).
  • Žádné návraty bugů — chybu, která jednou nastala, podchytíš testem a víc se nevrátí.

Testovací pyramida — čeho má být kolik

Testy se dělí podle toho, jak velký kus systému ověřují. Dobrý poměr vypadá jako pyramida:

  • Unit testy — ověřují jednu funkci/třídu samostatně, okolí je nahrazené (viz „test doubles" níže). Běží v milisekundách. Těch má být nejvíc.
  • Integration testy — ověřují víc částí dohromady (např. service proti skutečné databázi). Pomalejší, ale chytí to, co unit nemůže (špatné SQL, nesedící formát dat).
  • E2E testy (end-to-end) — projdou celý systém zvenčí, jako skutečný uživatel (přes HTTP nebo prohlížeč). Nejrealističtější, ale pomalé a křehké → jen na pár nejdůležitějších cest (přihlášení, dokončení objednávky).

🚩 Obrácená pyramida (hodně pomalých E2E, málo unit) se nazývá ice-cream cone a je to past: testovací sada je pomalá, často „vločkuje" (jednou projde, jednou ne) a nikdo jí nevěří.

💡 Pyramida není dogma. U I/O-heavy backendů (kde je „chytrá" logika hlavně v dotazech a integraci) se dnes často těžiště posouvá k integračním testům — uvidíš to pod názvem Testing Trophy. Pořád platí „málo E2E", ale poměr unit vs integration zvaž podle toho, kde u tebe reálně bývají chyby.


Jak vypadá jeden test: Arrange-Act-Assert

Osvědčená struktura každého testu má tři kroky:

Arrange — připrav vstup a výchozí stav
Act     — proveď akci, kterou testuješ
Assert  — ověř, že výsledek je takový, jaký má být

Jeden test ověřuje jednu věc a jeho název říká co: třeba „vrátí chybu 409, když je kurz plný".


Test doubles — náhrady okolí (mock, stub, fake)

Aby byl unit test rychlý a spolehlivý, nahrazuje skutečné okolí (databázi, síť) falešnými verzemi. Souhrnně se jim říká test doubles:

  • Stub — vrátí předem připravená data („databáze vrátí tohohle uživatele").
  • Mock — to samé + navíc ověří, že byl správně zavolán („ověř, že se opravdu poslal e-mail").
  • Fake — funkční, ale odlehčená náhrada (databáze jen v paměti místo skutečné).

⚠️ Nemockuj všechno. Když nahradíš i vlastní logiku, test ve výsledku ověřuje jen tvoje náhrady, ne realitu. Nahrazuj jen hranice (síť, čas, cizí API), ne to, co vlastně testuješ.


TDD — psaní testu jako prvního

TDD (Test-Driven Development) je postup, kde napíšeš test dřív než kód. Cyklus se opakuje:

Nejdřív test (logicky selže, kód ještě není), pak nejjednodušší kód, ať projde, pak úklid. Nutí tě promyslet rozhraní a hraniční případy předem a zaručí, že test opravdu něco hlídá (viděl jsi ho spadnout). Není to povinnost na všechno, ale na složitější logiku je to mocný nástroj.


Co testovat (a co ne)

  • Byznys logiku a pravidla, hraniční případy (nula, prázdno, maximum, neplatný vstup).
  • Chybové cesty — co se stane, když něco selže (tam bývá víc bugů než v happy path).
  • Bug, který nastal — napiš test, který ho zopakuje, pak oprav (ochrana proti regresi).
  • ❌ Triviální věci (jednoduché gettery), cizí knihovny (ty testuje jejich autor) a vnitřní detaily (testuj chování, ne „zavolala se uvnitř tahle konkrétní metoda").

Code coverage (procento řádků pokrytých testy) je nástroj, ne cíl. 100 % nezaručuje správnost — můžeš projít každý řádek a nic pořádně neověřit. 60 % testů dobře mířených na rizikovou logiku je lepší než 95 % na nedůležitém kódu. Coverage ti hlavně ukáže, co testy nepokrývají.


Vlastnosti dobrého testu

Dobrý test je rychlý, nezávislý (nezáleží na pořadí ani na ostatních testech), opakovatelný (vždy stejný výsledek — žádná závislost na skutečném čase, náhodě nebo síti) a jednoznačný (buď projde, nebo spadne).

Flaky (vločkující) test jednou projde a jednou spadne bez jediné změny kódu. Je horší než žádný, protože ho lidi začnou ignorovat („to jen zlobí") a tím přestanou věřit i ostatním testům. Časté příčiny: závislost na čase, na pořadí, na sdíleném stavu nebo na reálné síti.


Kde to zapadá

  • CI (Continuous Integration) je systém, který automaticky spustí všechny testy při každém nahrání kódu — takže se chyba chytne dřív, než se dostane do hlavní větve (viz Infra & DevOps).
  • Contract testing ověří, že se tvoje API a jeho klient shodnou na rozhraní (užitečné mezi službami, viz API Design).
  • Testovací data a izolace — každý test ať si vytvoří a uklidí svůj stav. Praktické techniky, jak to senioři dělají: spustit každý test v transakci a na konci ji rollbacknout (databáze se vrátí do výchozího stavu zadarmo), nebo při paralelním běhu dát každému workeru vlastní schéma/databázi.
  • Determinismus — nedeterminismus (čas, náhoda) se v testu injektuje a zafixuje: podstrč hodiny (freeze time) a pevný seed pro náhodu, ať test dává vždy stejný výsledek (lék na vločkování).

Failure modes — jak testování selže

  • Flaky testy → tým je začne ignorovat → testy ztratí smysl.
  • Přemockováno → testy jsou zelené, ale produkce padá (testovaly se náhrady, ne realita).
  • Ice-cream cone → pomalá, křehká sada, kterou nikdo nepouští.
  • Coverage jako cíl → vznikají testy na nedůležitý kód kvůli číslu, rizikové cesty zůstanou holé.
  • Testy svázané s detaily → každý refaktor je rozbije, i když se chování nezměnilo.

🛠️ Cvičení

  1. Napiš unit test podle AAA. Pro funkci, co počítá cenu se slevou, otestuj: běžný případ, hranice (sleva 0 % a 100 %) a neplatný vstup (záporná sleva). Drž strukturu Arrange-Act-Assert.
  2. Nahraď hranici, ne logiku. Service posílá e-mail přes nějaký Mailer. Napiš test, který ověří výpočet a nahradí jen Mailer (ne vlastní logiku). Pak ukaž, jak by vypadal přemockovaný test.
  3. Zopakuj bug testem. Vymysli chybu (např. špatné zaokrouhlení), napiš test, který ji odhalí (spadne), pak „oprav" a test zezelená — tomu se říká test proti regresi.
  4. TDD kolo. Pro funkci „ověř heslo (min. 8 znaků, číslo, velké písmeno)" jeď Red-Green-Refactor: nejdřív padající test, pak minimum kódu, pak úklid.
  5. Najdi příčinu vločkování. V tomhle popisu jsou tři důvody, proč test vločkuje: čte aktuální čas, závisí na pořadí spuštění, volá reálné API. U každého navrhni opravu.
Náčrt řešení — rozbal, až si cvičení zkusíš sám
  1. Unit test podle AAA — pro slevu napiš tři testy (běžný případ, hranice 0 % a 100 %, záporná sleva) a v každém drž Arrange-Act-Assert: připrav vstup, zavolej, ověř. Pozor: jeden test ověřuje jednu věc a název má říkat co — neslučuj všechny případy do jednoho.
  2. Nahraď hranici, ne logiku — podstrč jen falešný Mailer (stub/mock na hranici) a otestuj skutečný výpočet; přemockovaný test by nahradil i vlastní logiku a ověřoval by jen své náhrady. Pozor: mockuj síť/čas/cizí API, ne to, co vlastně testuješ — jinak je test zelený a produkce padá.
  3. Zopakuj bug testem — napiš test, který chybu (špatné zaokrouhlení) vyvolá a tím spadne, pak oprav kód a test zezelená — to je ochrana proti regresi. Pozor: nejdřív musíš vidět test spadnout, jinak nevíš, jestli vůbec něco hlídá.
  4. TDD kolo — Red-Green-Refactor: padající test na pravidlo hesla, pak minimum kódu, ať projde, pak úklid; nutí promyslet hraniční případy předem. Pozor: nepiš rovnou hotové řešení — smysl je vidět červenou a přidávat minimum.
  5. Najdi příčinu vločkování — čtení aktuálního času → injektuj a zafixuj hodiny (freeze time); závislost na pořadí → každý test ať si vytvoří a uklidí svůj stav (izolace); reálné API → nahraď stubem. Pozor: flaky test je horší než žádný, protože mu lidi přestanou věřit.

🧠 Otázky & odpovědi

Proč pyramida a ne hodně E2E testů?

E2E testy jsou pomalé, křehké (vločkují) a drahé na údržbu — jako základ sady ji zpomalí a podkopou důvěru. Unit testy jsou rychlé, izolované a levné, proto jich má být nejvíc. Integration chytí, co unit nemůže (špatné SQL, nesedící formát dat). E2E nech jen na pár nejdůležitějších cest (přihlášení, objednávka). Hodně dole, málo nahoře.

Co znamená nemockovat všechno?

Když nahradíš i vlastní logiku, test ve výsledku ověřuje jen tvoje náhrady, ne realitu — projde, i když je kód špatně. Nahrazuj jen hranice: síť, čas, náhodu, cizí API. Vlastní logiku testuj doopravdy a pro databázi raději použij odlehčenou (in-memory) nebo skutečnou testovací databázi. Cíl je testovat chování, ne vnitřní detaily.

Proč je code coverage nástroj, a ne cíl?

100% pokrytí neznamená správnost — můžeš projít každý řádek a přitom nic pořádně neověřit. A honba za číslem vede k testům na nedůležitém kódu. Coverage je užitečná hlavně opačně: ukáže, co testy nepokrývají. 60 % dobře mířených testů na rizikovou logiku je lepší než 95 % na triviálním kódu.

Proč je flaky test horší než žádný?

Flaky test jednou projde a jednou spadne bez změny kódu. Lidi ho začnou ignorovat („to jen zlobí") — a tím přestanou věřit celé sadě, takže přehlédnou i skutečné selhání. Časté příčiny: závislost na čase, na pořadí testů, na sdíleném stavu nebo na reálné síti. Oprav se to izolací a nahrazením nedeterministických věcí (hlavně času).

K čemu je TDD, když můžu testy psát až potom?

TDD (Red-Green-Refactor) tě nutí promyslet rozhraní a hraniční případy předem a zaručí, že test opravdu něco hlídá (viděl jsi ho spadnout, než jsi napsal kód). Navíc tě tlačí psát testovatelný, a tedy lépe navržený kód. Není to povinnost na všechno — na složitější logiku skvělé, na triviální nebo průzkumný kód míň.