Mocking & Test Doubles
Rust favors trait-based seams and manual fakes over heavy mocking frameworks. mockall generates mock implementations for traits when you need call expectations and verification.
Recipe
use mockall::automock;
#[automock]
pub trait PaymentGateway {
async fn charge(&self, cents: u64) -> Result<String, Error>;
}let mut mock = MockPaymentGateway::new();
mock.expect_charge().returning(|_| Ok("ch_123".into()));When to reach for this: External I/O boundaries (HTTP, payments, clock) that must be swapped in tests.
Working Example
use mockall::automock;
#[automock]
pub trait OrderRepo {
async fn save(&self, order: &Order) -> Result<(), Error>;
}
pub struct Service<G: PaymentGateway, R: OrderRepo> {
pay: G,
repo: R,
}
impl<G: PaymentGateway, R: OrderRepo> Service<G, R> {
pub async fn checkout(&self, order: &Order) -> Result<(), Error> {
self.pay.charge(order.total_cents).await?;
self.repo.save(order).await
}
}
#[tokio::test]
async fn checkout_charges_then_saves() {
let mut pay = MockPaymentGateway::new();
pay.expect_charge().with(eq(500)).returning(|_| Ok("ok".into()));
let mut repo = MockOrderRepo::new();
repo.expect_save().times(1).returning(|_| Ok(()));
let svc = Service { pay, repo };
svc.checkout(&Order { total_cents: 500 }).await.unwrap();
}What this demonstrates:
- Generic service accepts trait implementations
expect_*sets call count and arguments- Production uses real types; tests inject mocks
- Async traits need
#[async_trait]or Rust 1.75+ native async traits
Deep Dive
Fake vs Mock
| Double | When |
|---|---|
| Fake (in-memory map) | Simple stateful behavior |
| Stub (fixed responses) | Few call patterns |
Mock (mockall) | Strict call order and counts |
| Spy (record calls) | Verify side effects loosely |
Manual Fake Example
struct InMemoryRepo {
orders: Mutex<Vec<Order>>,
}
impl OrderRepo for InMemoryRepo {
async fn save(&self, order: &Order) -> Result<(), Error> {
self.orders.lock().unwrap().push(order.clone());
Ok(())
}
}- Often clearer than mocks for domain logic
- No code generation; full control over behavior
Gotchas
- Mocking concrete structs - cannot without traits. Fix: extract
traitboundary at compile time. - Over-specifying call order - brittle tests. Fix: assert outcomes, not every internal call.
- Async trait mock without
async_trait- confusing errors. Fix: enablemockallasync support or use sync trait +block_onin tests only. - Shared
Arc<dyn Trait>mocks - expectation races in parallel tests. Fix: per-test mock instance. - Mocking std types - not possible. Fix: wrap
Instant,Uuid, or HTTP in your own trait.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| In-memory fake | CRUD repos | Verifying exact call counts |
wiremock | HTTP client tests | Pure domain logic |
testcontainers | Real Postgres behavior | Fast unit feedback |
FAQs
Is mockall required?
No. Manual fakes and stubs are idiomatic; mockall helps when expectations matter.
How do I mock generic traits?
mockall supports generics with #[automock] on trait; check docs for associated type limits.
Can I expect async calls?
Yes with async trait support; ensure runtime in #[tokio::test].
What about static methods?
Prefer instance traits; for static-only APIs, inject a zero-sized token type implementing a trait.
How do I test errors?
returning(|_| Err(Error::Timeout)) on the expectation.
Mockall with dyn Trait?
MockFoo::new() implements Foo; pass as concrete mock or Arc<dyn Foo> with care.
How avoid mock leakage to prod?
Keep traits in src; mocks generated in same module under #[cfg(test)] or test-only module.
HTTP mocking strategy?
Trait HttpClient for unit tests; wiremock for integration-level HTTP.
Time mocking?
Trait Clock with now() -> Instant; fixed fake in tests.
When not to mock?
Pure functions, serializers, and algorithms - use direct assertions instead.
Related
- Test Organization - shared fakes
- Trait-Based Abstraction - design seams
- Testing Async Code - async tests
- Unit vs Integration Tests - where doubles live
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.