Mocking in Rust With Mockall and When Fakes Are Better
Test code that calls out to services with a hand-written fake or a Mockall mock. Compare setup cost, what each can assert and the cases where a fake is the better tool.

A test that needs an SMTP server is not a unit test. Replacing the mailer with a double is the standard fix, and Rust gives two ways. A mock records and verifies calls; a fake implements the behavior in memory. The choice affects how often tests break during refactoring. This post puts the same test both ways and gives a rule. It is the isolation chapter of the testing in Rust guide, and uses the injection pattern from dependency inversion with traits.
Rust 1.90, mockall 0.13.
The trait and the code under test
#[cfg_attr(test, mockall::automock)]
pub trait Mailer {
fn send(&self, to: &str, subject: &str) -> Result<(), String>;
}
pub fn welcome(mailer: &impl Mailer, email: &str) -> Result<(), String> {
if !email.contains('@') {
return Err("invalid email".into());
}
mailer.send(email, "Welcome")
}With Mockall
#[cfg(test)]
mod tests {
use super::*;
use mockall::predicate::eq;
#[test]
fn sends_one_welcome_mail() {
let mut mailer = MockMailer::new();
mailer
.expect_send()
.with(eq("[email protected]"), eq("Welcome"))
.times(1)
.returning(|_, _| Ok(()));
assert!(welcome(&mailer, "[email protected]").is_ok());
}
#[test]
fn invalid_email_sends_nothing() {
let mut mailer = MockMailer::new();
mailer.expect_send().never();
assert!(welcome(&mailer, "nope").is_err());
}
}#[automock] writes MockMailer. expect_send sets arguments and call count. When the mock goes out of scope, it checks its expectations, and times(1) failing prints which expectation was unmet. The never() test asserts that nothing was sent: an interaction that a fake would have to count by hand.
With a fake
use std::cell::RefCell;
#[derive(Default)]
struct FakeMailer { sent: RefCell<Vec<(String, String)>> }
impl Mailer for FakeMailer {
fn send(&self, to: &str, subject: &str) -> Result<(), String> {
self.sent.borrow_mut().push((to.into(), subject.into()));
Ok(())
}
}
#[test]
fn records_the_mail() {
let mailer = FakeMailer::default();
welcome(&mailer, "[email protected]").unwrap();
assert_eq!(mailer.sent.borrow().len(), 1);
}No macro, no framework. The test asserts on state, the list of sent mail, and not on how send was called.
Which one
| Situation | Better tool |
|---|---|
| Assert that something was not called | mock |
| Verify exact arguments or call order | mock |
| Dependency has state (a repository, a cache) | fake |
| Many tests share the same dependency | fake |
| Large trait, you need two methods | mock |
| Behavior changes often, tests should survive refactors | fake |
A mock encodes how the code under test uses the dependency. If you restructure welcome to call send through a helper or batch two calls into one, mock tests fail though the behavior is the same. Fakes assert outcomes, so they survive. The risk with fakes is drift: the fake does something the real thing does not. Keep a few tests against the real implementation to catch that.
Async
For async fn in traits, mockall supports #[automock] on traits using #[async_trait]. Native async methods in traits (stable since Rust 1.75) work when the trait is used with generics. Check the version of mockall you use for the exact support.
What not to mock
Types you do not own (a reqwest::Client) and anything you can run for real cheaply (an in-memory SQLite, a local test server with wiremock). A mock of the database says the query was issued, not that it is right. See the database test posts for real-database patterns.
Limits
Mocks need a trait, so the code must be designed for it. Time and randomness are dependencies too: inject a clock through a trait rather than patching. The crate is documented at docs.rs/mockall.


