3. Test-driven a documentation-driven implementace
Cíl kapitoly: Psát kód tak, aby test a dokumentace vznikaly před implementací a řídily ji — ne aby se dopisovaly po ní z povinnosti.
3.1 Proč testy před kódem
TDD není o „mít testy". Je o pořadí: test napsaný předem je specifikace chování; test dopsaný potom je fotografie toho, co kód náhodou dělá — včetně chyb. (Zadání paste serveru dělá z testů tvrdý požadavek — ~60 % objemu kódu a vyjmenované povinné případy, sekce Testování — právě proto, že bez nich se decrement-LRU logika správně napsat prakticky nedá.) Tři efekty TDD, kvůli kterým to celé je:
- Návrhový tlak. Kód, který jde těžko otestovat, je špatně navržený (skryté závislosti, mnoho zodpovědností). Test napsaný první tě donutí navrhnout volatelné, izolovatelné rozhraní.
- Definice hotového. „Hotovo" = akceptační kritéria z analýzy zezelenala. Bez testů je „hotovo" pocit.
- Síť pro refactoring. Zelená sada testů ti dovolí přepsat vnitřek bez strachu. Bez ní se kód přestane měnit, protože se ho každý bojí.
3.2 Cyklus red–green–refactor
- Red — napiš nejmenší test, který selže. Selhání si ověř (test, který nikdy nebyl červený, možná netestuje nic).
- Green — napiš nejmenší kód, který test splní. Odolej pokušení řešit budoucí případy.
- Refactor — teď, se zelenou sítí pod sebou, ukliď: pojmenování, duplicity, struktura. Testy musí zůstat zelené.
Krok má být malý — minuty, ne hodiny. Když nevíš, jaký test napsat, je to signál, že nerozumíš požadavku → zpátky do analýzy, ne dopředu do kódu. Konkrétní pořadí pro LRU store ze zadání: vložení pod limit → vložení přesně na limit → jednoduché evikce → vícekolový decrement cyklus → hraniční případy (MAX_PASTES == 1, shodné last_seen_tick). Od nejjednoduššího chování k záludnému; každý test tě donutí dopsat jen kousek logiky.
3.3 Pyramida testů v Rustu
Rust má tři vestavěné úrovně testů; drž jich hodně dole a málo nahoře:
Unit testy — v souboru s testovaným kódem:
#[cfg(test)] mod tests { use super::*; // vidí i privátní položky modulu }
Rychlé, bez IO, testují jednu jednotku logiky. Tady žije většina testů — v paste serveru celý store.rs (evikce) a render.rs (markdown, highlighting), přesně jak předepisuje zadání.
Integrační testy — adresář tests/, kompilují se jako samostatný crate, vidí jen veřejné API. Testují složení komponent — v paste serveru HTTP vrstvu: vytvoření přes form i JSON (AK-1), zobrazení pro každý MimeKind (AK-2), 404 po evikci (AK-4), chybové stavy bez panicu (AK-6). Praktický trik ze zadání: axum Router testuj přes tower::ServiceExt::oneshot — request projde celou aplikací bez otevírání TCP portu, takže testy běží rychle a paralelně.
Doc-testy — příklady v dokumentačních komentářích, spouštěné přes cargo test. Detail v 3.4 — jsou jádrem documentation-driven přístupu.
Struktura testu drž jednotně jako AAA: Arrange (připrav stav), Act (jedna testovaná akce), Assert (ověř výsledek). Název testu popisuje chování, ne funkci: insert_nad_kapacitu_dekrementuje_nejdele_neprohlizenou, ne test_insert_2.
3.4 Documentation-driven: rustdoc jako první krok
Postup, který spojuje dokumentaci a TDD: než implementuješ veřejnou funkci, napiš její rustdoc — včetně příkladu použití.
/// Vloží paste. Je-li store plný, provede decrement-LRU /// cyklus: nejdéle neprohlížené paste snižuje `hits`, /// dokud některá neklesne na 0 a není evikována. /// /// # Examples /// ``` /// let mut store = PasteStore::with_capacity(1); /// store.insert(paste_a.clone())?; /// store.insert(paste_b.clone())?; // A má hits=1 → evikována /// assert!(store.get(paste_a.id()).is_none()); /// # Ok::<(), StorageError>(()) /// ``` /// /// # Errors /// Vrací [`StorageError::TooLarge`], pokud obsah překračuje limit. pub fn insert(&mut self, paste: Paste) -> Result<(), StorageError> { ... }
Tenhle komentář dělá tři věci najednou: je to dokumentace pro uživatele, spustitelný test (cargo test příklad zkompiluje a spustí — dokumentace nemůže lhát, protože by CI spadla) a návrhová zkouška — když se příklad špatně píše, API je špatně navržené a zjistil jsi to před implementací.
Konvence rustdoc, které dodržuj u každé veřejné položky: první řádek = shrnutí jednou větou; sekce # Examples vždy; # Errors u funkcí vracejících Result; # Panics pokud může panikovat.
3.5 Property-based testing
Klasický test ověřuje jeden příklad. Property-based test (crate proptest) ověřuje vlastnost na stovkách generovaných vstupů:
proptest! { #[test] fn pocet_zaznamu_nikdy_neprekroci_kapacitu( ops in prop::collection::vec(any_op(), 0..1000) ) { let mut store = PasteStore::with_capacity(10); for op in ops { store.apply(op); } prop_assert!(store.len() <= 10); } }
Kde příkladové testy dokumentují chování, property testy hledají invarianty. Pro decrement-LRU store se nabízejí: „len() ≤ capacity po libovolné sekvenci operací", „get po insert vrací tutéž paste (dokud nebyla evikována)", „evikuje se vždy paste, jejíž hits klesly na 0, nikdy jiná", „tick monotónně roste". Když proptest najde protipříklad, automaticky ho zmenší na minimální selhávající sekvenci operací — u stavového algoritmu, jako je tenhle, k nezaplacení.
3.6 Mocking: kdy a jak
Pravidlo: mockuj hranice, ne vnitřek. Nahrazuj to, co je pomalé, nedeterministické nebo externí — IO, síť, databázi, čas, náhodnost. Nikdy nemockuj vlastní doménovou logiku; tu testuj doopravdy.
Díky návrhu z kapitoly 2 (trait jako port) je to laciné: pro repository napíšeš ruční fake — pro většinu případů lepší volba než mock framework, protože fake má reálné chování. Mock framework (mockall) použij, když potřebuješ ověřit interakci („metoda X byla zavolána právě jednou s argumentem Y") — což je řidší potřeba, než se zdá.
Speciální případ čas: nikdy SystemTime::now() uvnitř logiky a sleep() v testu. Paste server tohle řeší elegantně už návrhem — LRU pořadí neurčuje reálný čas, ale logické hodiny (tick), takže testy evikce žádné mockování času nepotřebují a jsou plně deterministické. Kdyby ale přibyla expirace podle reálného času (TTL), zaveď trait Clock a v testu ho posouvej ručně.
3.7 Nástroje
cargo test— vestavěný runner (unit + integrační + doc-testy)- cargo-nextest — rychlejší paralelní runner s lepším výstupem; doc-testy neumí, takže v CI kombinuj
cargo nextest run+cargo test --doc - cargo-llvm-cov — pokrytí kódu; číslo neuctívej (100 % coverage ≠ správnost, stejně jako 60 % objemu testů ze zadání je proxy, ne cíl) — používej ho k nalezení netestovaných větví, hlavně error paths
cargo test -- --nocapture— zobrazíprintln!výstup při ladění testu
Zdroje
- Kent Beck: Test-Driven Development: By Example (Addison-Wesley) — kanonický zdroj, část I stačí
- The rustdoc book — doc.rust-lang.org/rustdoc — dokumentační konvence a doc-testy
- Rust By Example: Testing — doc.rust-lang.org/rust-by-example/testing.html — tři druhy testů prakticky
- The proptest book — proptest-rs.github.io/proptest — property-based testing v Rustu
- Luca Palmieri: Zero To Production in Rust — zero2prod.com — celá kniha je TDD vývoj reálné služby; nejbližší existující věc téhle příručce v knižní podobě
- Martin Fowler: Mocks Aren't Stubs — martinfowler.com/articles/mocksArentStubs.html — rozdíl fake/stub/mock a kdy co
- tower::ServiceExt — docs.rs/tower —
oneshotpro testování axum routerů bez TCP