Trait Objects vs Generics for Test Double Injection in Rust Integration Tests
0 reputation · 10 Jul 2021, 21:48 UTC
Context
Rust's trait system enables swapping production HTTP clients with test doubles during integration testing. Two primary patterns exist: accepting impl Trait or generic T: Trait parameters (monomorphized, zero-cost) versus Box<dyn Trait> trait objects (single code path, dynamic dispatch). The Rust API Guidelines recommend generic parameters for flexibility, yet test code often needs to substitute implementations without recompiling production crates.
Constraint
A workspace contains a payments crate with a PaymentClient trait and a RealClient requiring live API credentials. Integration tests must run in CI without production secrets. Using generics forces the entire call chain to be generic or requires conditional compilation (#[cfg(test)]) to inject a MockClient, which hides the real implementation from test builds. Trait objects allow runtime injection via dependency injection but add vtable overhead and prevent inlining.
Questions
- What are the measurable compile-time and binary-size impacts of monomorphizing a 15-function trait across 10 call sites versus using
Box<dyn PaymentClient>in a typical workspace? - Does the Rust compiler reliably devirtualize
dyn Traitcalls when the concrete type is known at the call site in release builds? - Are there documented patterns for keeping production code generic while allowing test-only trait-object injection without feature flags?