When the real dependency gets in the way
You are writing a function that fetches a user profile and formats it for display. The function works. You write a test. The test calls the real API. The test passes. Then the API goes down for maintenance. Your CI fails. You did not break anything, but the build is red. Or worse, the API returns different data today than yesterday, and your test starts flaking because it depends on external state.
You need to isolate your code from the outside world. You need a fake dependency that behaves exactly how you tell it to. You need a mock.
How mocking works
Mocking is about substitution. You replace a real dependency with a controlled fake. Think of a movie set. The actors are your code. The background scenery is the dependency. You do not fly a real plane to film a cockpit scene. You build a mock cockpit on a soundstage. You control the lights, the sounds, and the turbulence. You can make the plane crash on command to test how the actors react.
In Rust, a mock is a stand-in for a trait or struct. You program the mock to return specific values, panic, or count how many times a method was called. This lets you test your logic without touching the network, the database, or the file system. The mock verifies that your code called the dependency correctly. If your code calls a method the wrong number of times or with the wrong arguments, the mock panics and the test fails.
Mocking traits with mockall
The standard tool for mocking traits in Rust is the mockall crate. It uses a macro to generate mock structs that implement your traits. You add mockall to your Cargo.toml under [dev-dependencies] so it only affects tests.
[dev-dependencies]
mockall = "0.13.0"
The mock! macro generates a mock struct from a trait definition. You define the mock inside the macro, and mockall creates a struct named MockTraitName that implements the trait.
use mockall::mock;
// Define the trait your code depends on.
trait DataProvider {
fn get_value(&self) -> i32;
fn save(&mut self, value: i32) -> Result<(), String>;
}
// Generate the mock struct.
mock! {
pub Provider {}
impl DataProvider for Provider {
fn get_value(&self) -> i32;
fn save(&mut self, value: i32) -> Result<(), String>;
}
}
// Function under test.
fn process(provider: &mut dyn DataProvider) -> i32 {
let val = provider.get_value();
let _ = provider.save(val * 2);
val
}
#[test]
fn test_process() {
let mut mock = MockProvider::default();
// Configure expectations.
// The mock will return 10 when get_value is called.
mock.expect_get_value().returning(|| 10);
// The mock will return Ok(()) when save is called.
mock.expect_save().returning(|_| Ok(()));
let result = process(&mut mock);
assert_eq!(result, 10);
}
The mock! macro generates MockProvider. When you call mock.expect_get_value(), you are not calling the method yet. You are configuring the mock. You are setting up a script. The mock remembers this script. When get_value() is actually called, the mock checks the script and returns 10. If you call get_value() without setting an expectation, the mock panics. This is a feature. It catches cases where your code calls a method you forgot to mock.
Automocking and advanced expectations
Writing mock! blocks for every trait adds boilerplate. For simple traits, use #[mockall::automock]. This attribute goes above the trait definition and generates the mock automatically. The mock struct is named MockTraitName.
use mockall::automock;
#[automock]
trait Logger {
fn log(&self, message: &str);
}
#[test]
fn test_logger_mock() {
let mut mock = MockLogger::default();
mock.expect_log().returning(|_| {});
mock.log("test message");
}
Mocks can do more than return static values. You can match arguments, count calls, and return computed values.
Use withf to match arguments with a closure. The closure returns true if the arguments match.
mock.expect_save()
.withf(|val| val > 0)
.returning(|_| Ok(()));
Use times to specify how many times the method must be called. If the count is wrong, the test fails.
mock.expect_get_value().times(2).returning(|| 42);
Use returning with a closure to compute the return value based on arguments.
mock.expect_save()
.returning(|val| if val > 100 { Err("too big".into()) } else { Ok(()) });
Convention aside: mockall generates code that can be verbose. Build times can suffer if you mock large traits. Keep mocks in dev-dependencies and avoid mocking traits with many methods unless necessary. The community calls this the "mock surface" rule. Minimize the number of methods you mock.
Mocking HTTP with mockito
mockall mocks traits. What if you have an HTTP client that you did not write? You cannot add a trait to a third-party struct. mockito solves this by spinning up a real HTTP server on a random port. You tell the server what to respond to. Your code talks to the server. The server is the mock.
Add mockito to your Cargo.toml.
[dev-dependencies]
mockito = "1.4.0"
reqwest = { version = "0.11", features = ["blocking"] }
Create a server, define a mock endpoint, and run your test.
use mockito::Server;
#[test]
fn test_http_mock() {
let mut server = Server::new();
// Define the mock response.
let mock = server.mock("GET", "/users")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"id": 1, "name": "Alice"}"#);
// Your code uses the server URL.
let client = reqwest::blocking::Client::new();
let url = format!("{}/users", server.url());
let resp = client.get(&url).send().unwrap();
assert_eq!(resp.status(), 200);
let body = resp.text().unwrap();
assert!(body.contains("Alice"));
// Assert the mock was called.
mock.assert();
}
mockito handles the wire protocol. Your code makes a real HTTP request, but the request never leaves your machine. The server returns the canned response you defined. This is useful for integration tests where you want to test the full request flow without mocking individual client methods.
Convention aside: mockito servers can be slow to start. Reuse the server across multiple tests in a module when possible, or use Server::new_async for async tests. Do not spin up a new server for every single assertion.
Pitfalls and errors
Mocks introduce their own set of issues. Understanding these pitfalls saves debugging time.
Mock structs do not implement Send or Sync by default. If your code requires Send, the mock fails to compile. You get E0277 (trait bound not satisfied). Fix this by adding #[mockall::automock] with the send attribute, or by using mock! with the #[mockall::automock] style attributes.
#[automock(send)]
trait AsyncProvider {
async fn fetch(&self) -> String;
}
Call order matters. mockall checks call order by default. If you expect method A then method B, but the code calls B then A, the test fails. This can be too strict. Use in_any_order() if the order does not matter.
mock.expect_a().in_any_order().returning(|| 1);
mock.expect_b().in_any_order().returning(|| 2);
Forgetting to set expectations causes a panic. The panic message says "Method called without expectation." This means your code called a method you did not configure. Either add the expectation or mark the method as returning by default if the call is optional.
Mock behavior is a contract. If the mock panics, your test code broke the contract, not the production code. Treat the mock setup as part of the test logic.
Decision matrix
Use mockall when your code depends on traits and you need to control method calls, return values, and call counts in unit tests. Use mockito when you are testing code that makes HTTP requests and you want to spin up a local server that returns canned responses without mocking individual client methods. Use testcontainers or real fixtures when the interaction with the external system is complex and mocking would require replicating the system's logic inside your test. Reach for std::io::Cursor or in-memory implementations when the dependency is simple I/O and a full mock framework adds unnecessary weight.
Treat mocks as a last resort for code you do not own. Mock the interface, not the implementation. If you find yourself mocking many methods, your trait might be too large. Split the trait.