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.
Search across all documentation pages
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.
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.
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:
expect_* sets call count and arguments#[async_trait] or Rust 1.75+ native async traits| 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 |
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(())
}
}trait boundary at compile time.async_trait - confusing errors. Fix: enable mockall async support or use sync trait + block_on in tests only.Arc<dyn Trait> mocks - expectation races in parallel tests. Fix: per-test mock instance.Instant, Uuid, or HTTP in your own trait.| 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 |
No. Manual fakes and stubs are idiomatic; mockall helps when expectations matter.
mockall supports generics with #[automock] on trait; check docs for associated type limits.
Yes with async trait support; ensure runtime in #[tokio::test].
Prefer instance traits; for static-only APIs, inject a zero-sized token type implementing a trait.
returning(|_| Err(Error::Timeout)) on the expectation.
MockFoo::new() implements Foo; pass as concrete mock or Arc<dyn Foo> with care.
Keep traits in src; mocks generated in same module under #[cfg(test)] or test-only module.
Trait HttpClient for unit tests; wiremock for integration-level HTTP.
Trait Clock with now() -> Instant; fixed fake in tests.
Pure functions, serializers, and algorithms - use direct assertions instead.
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+.
Reviewed by Chris St. John·Last updated Jul 16, 2026