Architektonická příručka pro junior Rust programátora
Tahle příručka pokrývá životní cyklus softwaru od zadání po produkci: analýza → návrh → implementace řízená testy a dokumentací → nasazení a provoz. Není to učebnice Rustu (syntaxi předpokládá — díry doplní The Rust Book), je to učebnice inženýrského řemesla s Rustem jako nástrojem.
Jak příručku číst
- Fáze nejsou vodopád. Pořadí kapitol odpovídá logické závislosti (nemůžeš navrhovat, co jsi neanalyzoval), ale v praxi se vracíš: implementace odhalí díru v analýze, provoz odhalí chybu návrhu. To je normální — podstatné je, že se vracíš vědomě a artefakty aktualizuješ.
- Dokument je artefakt jako kód. Požadavky, ADR, diagramy i changelog žijí v gitu vedle kódu, procházejí review a mají historii. Dokumentace, která žije jinde, umírá.
- Každá kapitola končí zdroji. Příručka je mapa; zdroje jsou území. U každého zdroje je napsáno, co konkrétně si z něj vzít.
- Průběžný příklad je záměrně jednoduchý — in-memory paste server (viz zadání) — aby ilustroval principy, ne aby tě zahltil doménou.
Obsah
- Analýza a požadavky
- Návrh a architektura
- Test-driven a documentation-driven implementace
- CI/CD a produkční provoz
- Checklisty a doporučené čtení
1. Analýza a požadavky
Cíl kapitoly: Naučit se převést mlhavé zadání na sadu ověřitelných požadavků dřív, než napíšeš první řádek kódu.
1.1 Proč analýza
Chyba v požadavcích, kterou odhalíš při analýze, tě stojí hodinu diskuse. Ta samá chyba odhalená v produkci stojí dny debugování, hotfix release a důvěru uživatelů. Cena chyby roste řádově s každou fází, ve které přežije. Analýza je proto nejlevnější místo, kde dělat chyby — a proto se vyplatí v ní chyby aktivně hledat.
Praktický důsledek: když dostaneš zadání, tvoje první práce není psát kód. Tvoje první práce je klást otázky, dokud nedokážeš zadání převyprávět vlastními slovy tak, že zadavatel řekne „ano, přesně tohle". Zkus si to hned na zadání paste serveru: dokážeš vlastními slovy popsat decrement-LRU cyklus (zadání, sekce 3), včetně případu, kdy první kandidát po snížení nespadne na nulu? Pokud ne, našel jsi první otázku k položení — před kódem, ne po něm.
1.2 Funkční vs. nefunkční požadavky
Funkční požadavky říkají, co systém dělá. V zadání paste serveru jsou to sekce 1–3: vytvoření paste (formulář i JSON, sekce 1), zobrazení podle mimetype (sekce 2), evikce při plné kapacitě (sekce 3).
Nefunkční požadavky (NFR) říkají, jak dobře a za jakých omezení to dělá. Juniorská chyba číslo jedna je NFR ignorovat — přitom právě ony určují architekturu. Rozdíl mezi paste serverem pro 10 uživatelů a pro 10 000 uživatelů není ve funkcích, ale v NFR.
Pro každý projekt si vynuť odpovědi minimálně na tyto kategorie — v závorce, jak na ně odpovídá zadání paste serveru:
- Kapacita — kolik dat, kolik záznamů, jaký růst? (
MAX_PASTES, konfigurovatelný limit; překročení řeší evikce, ne odmítnutí) - Latence — jaká odezva je přijatelná? p50 vs. p99. (Zadání ji neřeší a výslovně akceptuje O(n) lookup — všimni si, že i „výkon není cíl" je nefunkční požadavek, který musí zaznít)
- Dostupnost / persistence — co se smí stát při restartu? (Čistě in-memory, ztráta dat restartem je akceptovaná — zadání, sekce 4)
- Souběžnost — kolik současných klientů, jaký model přístupu ke stavu? (Sdílený stav za
Mutex, záměrně bez lock-free/sharded přístupů — zadání, sekce 4) - Bezpečnost — kdo smí co, jaké limity vstupů? (Bez autentizace; nesanitizované HTML je pojmenovaná known limitation; limit velikosti paste je v bonusu)
- Vynucená omezení — technologická a výuková. (Pouze
Vec, žádné asociativní kolekce — AK-5. Omezení „proč" nemusí být technické; tady je pedagogické, ale pořád tvaruje návrh)
NFR piš měřitelně. „Server má být rychlý" není požadavek. „p99 latence čtení < 50 ms při 100 RPS" je požadavek — dá se ověřit testem.
1.3 Doménový slovník (ubiquitous language)
Než začneš modelovat, sepiš slovník pojmů domény a používej ho konzistentně v zadání, v kódu i v testech. Když zadání říká „paste", struct se jmenuje Paste, ne Entry ani Item. Slovník paste serveru by vypadal takhle:
| Pojem | Definice |
|---|---|
| Paste | Uložený obsah s UUID, mimetype a LRU metadaty |
| Hit | Zobrazení paste; zvyšuje hits a aktualizuje last_seen_tick |
| Tick | Globální generace přístupů; logické hodiny store |
| Kandidát evikce | Paste s nejnižším last_seen_tick (nejdéle neprohlížená) |
| Decrement-LRU | Evikční cyklus: kandidátovi se snižuje hits, evikuje se až při nule |
| Evikce | Odstranění paste kvůli kapacitě (≠ smazání uživatelem, které je v bonusu) |
Všimni si posledního řádku: slovník zachycuje rozlišení, která by jinak žila jen v něčí hlavě. „Evikce" a „smazání" jsou různé operace s různými pravidly — když to řekneš v tabulce, nikdo je neslije do jedné metody. Tohle je jádro myšlenky ubiquitous language z Domain-Driven Designu: jeden jazyk sdílený zadavatelem, dokumentací a kódem. Každý překlad mezi „jazykem byznysu" a „jazykem kódu" je místo, kde vznikají chyby.
1.4 User stories a akceptační kritéria
User story je formát pro zachycení požadavku z pohledu uživatele:
Jako anonymní uživatel chci vložit text a dostat URL s UUID, abych mohl sdílet log s kolegou.
Dobrá story splňuje kritéria INVEST: Independent (nezávislá na jiných), Negotiable (detail je k diskusi), Valuable (má hodnotu pro uživatele), Estimable (dá se odhadnout), Small (zvládnutelná v jedné iteraci), Testable (dá se ověřit).
Poslední bod je nejdůležitější: ke každé story patří akceptační kritéria. Zadání paste serveru jich definuje šest (AK-1 až AK-6); klíčové scénáře si rozepiš do Given/When/Then podoby — tady AK-3 + AK-4, zobrazeno jako sekvenční diagram interakce klienta se store:
A hned scénář, který z prostého přečtení zadání snadno unikne — vícekolový decrement cyklus:
Všimni si: tahle kritéria jsou zároveň hotové testy — zadání je v sekci Testování výslovně vyžaduje („opakovaný decrement cyklus, kdy první kandidát nespadne na 0"). Analýza a testování nejsou oddělené fáze; dobrá analýza produkuje testy zadarmo. Grafický souhrn případů užití (use case diagram) najdeš v kapitole 2.4.
1.5 Hranice systému
Nakresli (klidně rukou) hranici: co je uvnitř systému, co je venku, a jaká rozhraní hranici protínají. Pro paste server: uvnitř je HTTP API, store s evikční logikou a rendering; venku je klient (curl/prohlížeč). Za povšimnutí stojí, že zadání drží čas uvnitř jako logické hodiny (tick) místo závislosti na systémovém čase — díky tomu je evikce deterministická a testovatelná.
Explicitní hranice ti odpoví na otázky typu: „Má server řešit autentizaci, nebo ji řeší reverse proxy před ním?" Každá nejasná hranice je budoucí spor o to, čí je to zodpovědnost. Stejně cenná je hranice scope: zadání má sekci Bonus (mazání, limit velikosti, konfigurace) — to je formalizované „out of scope, ale víme o tom", které brání jak zbytečné práci, tak výčitce „na tohle jsi nemyslel".
1.6 Výstupní artefakty fáze
Analýza není hotová, dokud nemáš sepsáno (stačí jeden markdown soubor v repu, např. docs/requirements.md):
- Doménový slovník — pojmy a jejich definice (viz 1.3)
- Funkční požadavky — číslované (FR-1, FR-2…), aby se na ně dalo odkazovat z testů, commitů a ADR
- Nefunkční požadavky — měřitelné, včetně vynucených omezení (NFR-1…)
- Akceptační kritéria — klíčové scénáře (klidně jako sekvenční diagramy), včetně těch záludných (vícekolový decrement)
- Hranice systému — co je uvnitř, co venku, co je out of scope
- Otevřené otázky — co ještě nevíš; nepředstírej, že seznam je prázdný
U paste serveru dostáváš body 2–5 v zadání téměř hotové — tvoje analytická práce je slovník (1), rozepsání záludných scénářů (4) a otázky (6).
Zdroje
- Karl Wiegers, Joy Beatty: Software Requirements (3rd ed., Microsoft Press) — referenční kniha o požadavcích, kapitoly o NFR a testovatelnosti
- Scott Wlaschin: Domain Modeling Made Functional (Pragmatic Bookshelf) — ubiquitous language a modelování domény typy; ačkoliv používá F#, přenositelnost do Rustu je téměř 1:1
- Bill Wake: INVEST in Good Stories — xp123.com/articles/invest-in-good-stories-and-smart-tasks
- Gherkin Reference (Cucumber) — cucumber.io/docs/gherkin/reference — formát Given/When/Then, myšlenkový základ pro akceptační kritéria výše
- Eric Evans: Domain-Driven Design (Addison-Wesley) — kapitola 2 (Communication and the Use of Language); zbytek knihy je na juniora těžký, ale tahle kapitola stojí za to
2. Návrh a architektura
Cíl kapitoly: Umět rozdělit systém na části se zdůvodněnými hranicemi, zdokumentovat rozhodnutí a využít typový systém Rustu jako architektonický nástroj.
2.1 Co je architektura
Pragmatická definice: architektura jsou rozhodnutí, která je drahé změnit později. Jestli funkce vrací Vec nebo iterátor, změníš za pět minut — to není architektura. Jestli je storage za rozhraním, nebo je Vec natvrdo prošitý celým kódem, to určuje, kolik bude stát budoucí výměna úložiště — to architektura je.
Z toho plyne priorita návrhu: hranice před obsahem. Vnitřek modulu můžeš přepsat kdykoliv; jeho veřejné rozhraní přepisuješ za cenu úprav u všech konzumentů.
2.2 Architektonické styly
Nauč se tři základní styly a hlavně kdy který použít:
Vrstvená architektura (layered) — handler → service → repository → storage. Volání jde jen směrem dolů. Jednoduchá na pochopení, dobrá výchozí volba pro CRUD služby. Riziko: vrstvy zdegradují na „průtokový ohřívač" (každá vrstva jen deleguje níž) — pak je jich moc.
Hexagonální architektura (ports & adapters) — doménová logika uprostřed, veškerá komunikace se světem (HTTP, DB, hodiny, fronty) přes porty (rozhraní definovaná doménou) a adaptéry (implementace portů pro konkrétní technologii). Klíčová vlastnost: doména nezná HTTP ani SQL, takže se testuje bez infrastruktury. V Rustu: port = trait, adaptér = impl.
Event-driven — komponenty komunikují zprávami/událostmi místo přímých volání. Odděluje producenty od konzumentů, přirozeně škáluje, ale za cenu ztráty přehlednosti toku („kdo tohle vlastně zpracuje?") a složitějšího debugování. Sáhni po něm, když potřebuješ, aby na jednu událost reagovalo víc nezávislých částí, ne jako výchozí styl.
Pro malé a střední služby je hexagonál obvykle nejlepší poměr cena/výkon: stojí pár traitů navíc a kupuje testovatelnost. Zadání paste serveru předepisuje totéž v měkčí, vrstvené podobě: routes.rs (HTTP adaptér) → store.rs/render.rs (logika bez axum typů) — viz sekce Separation of concerns v zadání.
2.3 Dokumentace architektury: C4 model
C4 dokumentuje systém ve čtyřech úrovních přiblížení:
- Context — systém jako černá skříňka + uživatelé a okolní systémy
- Container — nasaditelné jednotky (binárka, DB, proxy) a komunikace mezi nimi
- Component — hlavní moduly uvnitř jedné binárky a jejich závislosti
- Code — třídy/structy; tuhle úroveň většinou nekresli, zastarává okamžitě a generuje ji IDE
Pro malý projekt stačí úrovně 1–3, každá jako jeden diagram (Mermaid v markdownu přímo v repu). Pravidlo: diagram, který nikdo neaktualizuje, je horší než žádný — proto málo diagramů, blízko kódu, v gitu. Konkrétní příklady pro paste server jsou v následující sekci.
2.4 Diagramy v praxi: paste server
Tři diagramy, které pro projekt velikosti paste serveru stojí za nakreslení. První vzniká už při analýze, druhé dva při návrhu. Všechny jsou Mermaid — text v gitu, verzovaný a diffovatelný jako kód.
Diagram případů užití (use case)
Odpovídá na otázku „kdo se systémem co dělá" — je to vizuální souhrn funkčních požadavků ze zadání (sekce 1 a 2). Mermaid nemá nativní use case notaci, flowchart s aktérem a elipsami je ustálená náhrada:
Všimni si, co diagram dělá: evikce a rendering nejsou samostatné akce uživatele — jsou to include vztahy, tedy chování, které systém vykoná uvnitř jiného případu užití. Přesně tohle rozlišení („uživatel evikci nevolá, evikce se děje") je informace, kterou seznam požadavků nese hůř než obrázek.
ERD (datový model)
Paste server má triviální model — o to lépe se na něm ukazuje, co do ERD patří: entity, atributy s typy, klíče a kardinality vztahů. Model odpovídá structu Paste ze zadání:
Dvě věci k povšimnutí. Zaprvé tick žije ve store, ne v paste — je to globální čítač, jehož hodnota se při přístupu kopíruje do last_seen_tick položky; ERD tuhle zodpovědnost ukazuje na první pohled. Zadruhé ERD není obrázek databázových tabulek — tady žádná databáze není (úložiště je Vec v paměti), a diagram je přesto užitečný: modeluje pojmy a vztahy, ne persistence technologii.
Komponentový diagram (C4 úroveň 3)
Moduly binárky a směr závislostí — odpovídá struktuře src/ navržené v zadání:
Kontrolní otázky nad tímhle diagramem: Vedou všechny šipky správným směrem? (store.rs nesmí ukazovat na routes.rs — doména nezná HTTP.) Nechybí šipka, která v kódu reálně existuje? Diagram, který lže, je horší než žádný — proto ho drž malý a při změně závislostí aktualizuj v tomtéž PR.
2.5 Architecture Decision Records (ADR)
Každé netriviální rozhodnutí zapiš jako krátký ADR — jeden markdown soubor (docs/adr/0002-vec-only-storage.md). Příklad nad reálným rozhodnutím ze zadání paste serveru:
# ADR-0002: Úložiště pouze nad Vec, lineární vyhledávání
## Status: accepted
## Kontext
Zadání zakazuje asociativní kolekce (HashMap, BTreeMap,
VecDeque…) — cílem cvičení je algoritmizace a práce
s indexy a vlastnictvím (zadání, sekce 3, akceptační
kritérium 5).
## Rozhodnutí
PasteStore drží Vec<Paste>; vyhledávání podle UUID je
lineární O(n), kandidát evikce se hledá průchodem
přes last_seen_tick.
## Důsledky
+ Splňuje výukový cíl a AK-5
+ Jednoduchá implementace, snadno testovatelná
- O(n) lookup — přijatelné pro MAX_PASTES v řádu stovek
- Při zvednutí kapacity o řády by bylo nutné rozhodnutí
revidovat (index, HashMap) → mimo scope cvičení
ADR odpovídá na otázku, kterou si za půl roku položíš ty sám: „Proč jsme to sakra udělali takhle?" Kontext se zapomíná, kód ho nenese. Všimni si vazby na požadavky — rozhodnutí bez vazby na požadavek je podezřelé (buď chybí požadavek, nebo je rozhodnutí zbytečné).
2.6 Ownership jako architektonický nástroj
V jiných jazycích je „kdo vlastní tahle data" konvence v hlavě autora. V Rustu je to kompilátorem vynucený kontrakt — a to z ownership dělá návrhový nástroj:
- Jeden vlastník = jedno místo zodpovědnosti. Když navrhuješ modul, začni otázkou „kdo vlastní stav?" Struktura vlastnictví je kostra architektury.
&Tvs&mut TvsTv signatuře je architektonická výpověď:&self= jen čtu,&mut self= měním stav (a kompilátor vynutí exkluzivitu),self= konzumuji (volající to už nemá). Signatury čti a piš jako smlouvy.- Sdílený mutabilní stav je explicitní a drahý (
Arc<Mutex<T>>) — a to je dobře. Cena v syntaxi tě nutí ptát se, jestli sdílení opravdu potřebuješ, nebo jestli stav nemá vlastnit jedna task a ostatní s ní mluvit přes kanál (message passing).
V paste serveru je vlastnická mapa jednoduchá: main vytvoří Arc<Mutex<PasteStore>> a předá ho axumu přes State — jediná instance, sdílený přístup, žádný globál (zadání to popisuje jako idiomatickou náhradu Singletonu). Zadání zároveň chce, abys uměl zdůvodnit Mutex vs. RwLock: tady i „čtení" (GET) zapisuje (hits, last_seen_tick), takže RwLock by nic nepřinesl — přesně ten typ úvahy, který patří do ADR nebo komentáře.
2.7 Moduly, crates a viditelnost
Hranice v Rustu vynucuješ třemi mechanismy, od nejměkčí po nejtvrdší:
- Modul (
mod) — organizace uvnitř crate;pub(crate)vspubřídí, co je vidět ven - Crate v workspace — tvrdá hranice: závislosti jsou explicitní v
Cargo.toml, cyklus je nemožný (kompilátor ho zakáže — architektonická výhra zadarmo) - Publikovaná knihovna — hranice se semver kontraktem
Pro začátek: jeden binary crate s dobře řezanými moduly — přesně struktura main.rs / routes.rs / store.rs / render.rs / model.rs ze zadání (a z komponentového diagramu výše). Na workspace s více crates přejdi, až když chceš hranici vynutit (např. doména nesmí záviset na axum — dej doménu do vlastního crate bez té závislosti a kompilátor hlídá za tebe).
Návrh veřejného API modulu: exportuj minimum. Všechno pub je závazek; všechno pub(crate) můžeš zítra beztrestně přepsat.
2.8 Typový systém jako dokumentace a validace
Zásada: make invalid states unrepresentable — navrhni typy tak, aby neplatný stav nešel ani zkonstruovat.
Newtype místo primitivů. fn get(id: String) přijme cokoliv včetně prázdného řetězce a umožní prohodit argumenty. Newtype validuje na hranici:
pub struct PasteId(Uuid);
impl PasteId {
pub fn parse(s: &str) -> Result<Self, InvalidId> {
// validace tady, jednou, na hranici systému
}
}
Zbytek kódu pracuje s PasteId a nemusí nic kontrolovat — existence hodnoty je důkaz platnosti. Tomu se říká parse, don't validate: nevaliduj opakovaně, jednou naparsuj do typu, který neplatnost neumí vyjádřit. (Zadání tenhle vzor doporučuje výslovně — sekce Newtype pattern.)
Enum místo stringů a bool kombinací. Zadání to dělá vzorově: mimetype není String, ale enum MimeKind { PlainText, Html, Markdown, OctetStream } — neplatný mimetype neprojde přes hranici parsování a match nad enumem vynutí, že žádná renderovací větev nechybí (viz Strategy pattern v zadání: strategie přes match, ne nutně přes dyn Trait).
Typestate — pokročilejší vzor, kdy přechody stavů hlídá typ (Connection<Open> → Connection<Closed>), zmíněn pro úplnost; sáhni po něm, až budeš mít vzor v ruce.
2.9 Error handling jako součást návrhu
Chyby jsou API. Navrhni je vědomě:
- Knihovní/doménový kód: konkrétní error enum přes thiserror. Volající potřebuje na varianty reagovat — v paste serveru
AppError { NotFound, InvalidMimeType, PayloadTooLarge, … }. - Binárka/aplikační vrstva: anyhow pro chyby, na které se nereaguje, jen se propagují a logují (selhání startu, config).
- Hranice: na okraji systému se doménové chyby mapují na protokol — v axum idiomaticky přes
impl IntoResponse for AppError(NotFound→ 404,PayloadTooLarge→ 413). Doména nezná HTTP — mapování žije vroutes.rs, ne vstore.rs. panic!/unwrappatří jen tam, kde je stav skutečně nemožný (a napiš doexpect("...")proč) — nikdy na vstupy zvenku. Přesně to vynucuje akceptační kritérium 6 zadání: žádný panic při běžném provozu.
2.10 Trait jako port: hexagonál v Rustu
Spojení předchozích bodů do vzoru, který budeš používat pořád:
// Port — definuje ho doména, mluví jazykem domény
pub trait PasteRepository {
fn get(&self, id: &PasteId) -> Option<Paste>;
fn insert(&self, paste: Paste) -> Result<(), StorageError>;
}
Doménová logika závisí na traitu. VecStore (ten ze zadání) je adaptér pro produkci, testovací fake je adaptér pro testy. HTTP vrstva je taky jen adaptér — překládá requesty na volání domény. Výsledek: doménu otestuješ bez serveru, storage vyměníš bez zásahu do logiky, a závislosti tečou jedním směrem (dovnitř k doméně). Zadání to formuluje jako DIP: handlery závisí na veřejném API PasteStore, ne na detailu, že uvnitř je Vec.
Pozor na přestřelení: trait zaváděj tam, kde hranici potřebuješ (IO, čas, náhodnost, externí služby), ne kolem každé funkce. V paste serveru stačí, aby PasteStore byl konkrétní typ s čistým veřejným API — trait přidáš, až budou implementace dvě. A nezapomeň: čas je závislost — decrement-LRU používá tick (logické hodiny) místo SystemTime, což je mimochodem přesně to, co dělá evikci deterministicky testovatelnou.
2.11 Návrh veřejného API
Až budeš navrhovat rozhraní (modulu, crate, HTTP), projdi si Rust API Guidelines — checklist konvencí: pojmenování (as_/to_/into_), kdy brát &str vs String vs impl AsRef<str>, implementace standardních traitů (Debug, Clone, Default…), dokumentace každé veřejné položky. Konvence nejsou byrokracie — API, které vypadá jako std, uživatel (i ty za půl roku) použije správně bez čtení dokumentace.
Zdroje
- Simon Brown: C4 model — c4model.com — celý model včetně příkladů a notace
- Mermaid dokumentace — mermaid.js.org — syntaxe flowchart a erDiagram použitá výše
- Michael Nygard: Documenting Architecture Decisions — cognitect.com/blog/2011/11/15/documenting-architecture-decisions — původní ADR formát
- Alistair Cockburn: Hexagonal Architecture — alistair.cockburn.us/hexagonal-architecture — původní popis ports & adapters
- Rust API Guidelines — rust-lang.github.io/api-guidelines — checklist pro návrh rozhraní
- Jon Gjengset: Rust for Rustaceans (No Starch Press) — kap. 3 (Designing Interfaces) a 4 (Error Handling)
- Alexis King: Parse, don't validate — lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate — Haskell, ale princip je jazykově nezávislý
- Scott Wlaschin: Designing with types (série) — fsharpforfunandprofit.com/series/designing-with-types — make illegal states unrepresentable
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