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

  1. Analýza a požadavky
  2. Návrh a architektura
  3. Test-driven a documentation-driven implementace
  4. CI/CD a produkční provoz
  5. Checklisty a doporučené čtení

study/begginer-guide/analyza-pozadavky collapse

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:

PojemDefinice
PasteUložený obsah s UUID, mimetype a LRU metadaty
HitZobrazení paste; zvyšuje hits a aktualizuje last_seen_tick
TickGlobální generace přístupů; logické hodiny store
Kandidát evikcePaste s nejnižším last_seen_tick (nejdéle neprohlížená)
Decrement-LRUEvikční cyklus: kandidátovi se snižuje hits, evikuje se až při nule
EvikceOdstraně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:

ClientClientStoreStoreMAX_PASTES=2A: hits=1(nejdéleneprohlížená)B: uloženaPOST /paste (nový paste C)vyber kandidáta evikce = AA.hits -= 1 (1 → 0)hits == 0 → evikce Auložit paste CGET /paste/{A}404 Not Found

A hned scénář, který z prostého přečtení zadání snadno unikne — vícekolový decrement cyklus:

ClientClientStoreStoreMAX_PASTES=2A: hits=2(nejdéleneprohlížená)B: hits=1POST /paste (nový paste C)vyber kandidáta evikce = AA.hits -= 1 (2 → 1)hits != 0 → A NENÍ evikovánavyber dalšího kandidáta = BB.hits -= 1 (1 → 0)hits == 0 → evikce Buložit paste C

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):

  1. Doménový slovník — pojmy a jejich definice (viz 1.3)
  2. Funkční požadavky — číslované (FR-1, FR-2…), aby se na ně dalo odkazovat z testů, commitů a ADR
  3. Nefunkční požadavky — měřitelné, včetně vynucených omezení (NFR-1…)
  4. Akceptační kritéria — klíčové scénáře (klidně jako sekvenční diagramy), včetně těch záludných (vícekolový decrement)
  5. Hranice systému — co je uvnitř, co venku, co je out of scope
  6. 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 Storiesxp123.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

study/begginer-guide/navrh-architektura collapse

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í:

  1. Context — systém jako černá skříňka + uživatelé a okolní systémy
  2. Container — nasaditelné jednotky (binárka, DB, proxy) a komunikace mezi nimi
  3. Component — hlavní moduly uvnitř jedné binárky a jejich závislosti
  4. 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:

Paste serverinclude: při plné kapacitěincludeUživatelZobrazit formulářGET /Vytvořit pastePOST /paste — form i JSONZobrazit pasteGET /paste/:uuidProvést decrement-LRUevikciRenderovat dle mimetypeplain/html/md/binární

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í:

obsahuje (0 az MAX_PASTES)PASTE_STOREusizecapacityMAX_PASTES, konfiguraceu64tickglobalni generace pristupuPASTEUuididPKUUID v4bytescontenttextovy i binarni obsahMimeKindmimetypePlainText / Html / Markdown / OctetStreamu32hitspocitadlo pro decrement-LRUu64last_seen_tickgenerace posledniho pristupu

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í:

paste-server binárkaHTTPKlientprohlížeč / curlroutes.rsaxum handlery: HTTP <-> doménastore.rsPasteStore + decrement-LRUrender.rspulldown-cmark, syntect, mermaid→SVGmodel.rsPaste, MimeKind, AppError

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.
  • &T vs &mut T vs T v 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ší:

  1. Modul (mod) — organizace uvnitř crate; pub(crate) vs pub řídí, co je vidět ven
  2. 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)
  3. 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 v routes.rs, ne v store.rs.
  • panic!/unwrap patří jen tam, kde je stav skutečně nemožný (a napiš do expect("...") 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


study/begginer-guide/tdd-implementace collapse

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:

  1. 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í.
  2. Definice hotového. „Hotovo" = akceptační kritéria z analýzy zezelenala. Bez testů je „hotovo" pocit.
  3. 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

  1. Red — napiš nejmenší test, který selže. Selhání si ověř (test, který nikdy nebyl červený, možná netestuje nic).
  2. Green — napiš nejmenší kód, který test splní. Odolej pokušení řešit budoucí případy.
  3. 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