Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Traits, Generics, and Testing Discipline

Goal: you can define a trait contract, write two implementations plus a test fake, and choose correctly between generics (impl Trait/bounds) and trait objects (dyn).

The trait is the contract

#![allow(unused)]
fn main() {
pub trait MatchStrategy {
    fn matches(&self, haystack: &str, needle: &str) -> bool;
    fn name(&self) -> &'static str;
}
}

This is the shape our production code uses for swap-able behavior: uteke’s index traits, Sinau’s StorageBackend (local vs S3 behind one interface). Callers depend on the contract, not an implementation — which is also what makes testing cheap: the fake IS an implementation.

Static vs dynamic dispatch

StyleSyntaxChoose when
Static (generics)fn query<S: MatchStrategy>(s: &S) or SearchEngine<S>strategy known at compile time; zero-cost, but one monomorphized copy per type
Dynamic (trait object)fn query(docs: &[Document], s: &dyn MatchStrategy) or Box<dyn MatchStrategy>strategy chosen at runtime (config, request param); one copy, tiny vtable hop

The M3 lab’s engine is generic over S: MatchStrategy but main stores Box<dyn MatchStrategy> selected from CLI args — both styles in one program, and the tests exercise both.

Iterator chains over manual loops

#![allow(unused)]
fn main() {
self.docs.iter()
    .filter(|d| s.matches(&d.body, q))
    .map(|d| (d.id, score_of(d)))
    .filter(|(_, score)| *score > 0)
    .collect()
}

Reads top-to-bottom, composes, and lazily evaluates. Reach for itertools only when a chain genuinely needs it; most filters/maps don’t.

Testing discipline (the house rules)

  • Tests live in the same file: #[cfg(test)] mod tests — they compile against private items, and nobody forgets to update them.
  • Arrange–Act–Assert, one behavior per test, name = the behavior (word_strategy_respects_boundaries).
  • Every public capability gets: a happy path, an empty/edge case, and a failure mode.
  • A trait contract gets a conformance test per implementation — plus a fake for tests of code that depends on the trait.

Lab

labs/m3-search — three strategies (ExactSearch, CaseInsensitiveSearch, WordSearch), a generic SearchEngine<S> with naive title-doubled ranking, and the &dyn variant query_dyn. Queries: cargo run -p m3-search deploy ci.

Done when: cargo test -p m3-search is green (6 tests), and you can say when dyn costs you anything (it doesn’t here) and why the fake-free design still tests all strategies (conformance tests).