Rust Mocking: Using Traits as Test Seams
Mocking in Rust uses traits as test doubles. You program a mock's behavior—what calls to expect and what to return—to isolate the code under test. The `mockall` crate's `#[automock]` macro generates mocks from traits. The footgun is over-specifying behavior.
WHY IT EXISTS: Unit testing requires isolation. When testing a piece of code, you want to verify its logic independently of its dependencies. If your function calls a database or a web service, a real call would make the test slow, unreliable, and dependent on external systems. Mocking replaces these dependencies with predictable, in-memory fakes.
THE MENTAL MODEL: A mock object is a "stunt double" for a real component, defined by a Rust trait. You give it a script for the test: "When your get_user method is called with ID 123, return a User struct. Expect this to happen exactly once." Your code under test interacts with this stunt double, and the mock verifies that the interaction happens exactly as scripted.
HOW IT WORKS: With a library like mockall, you add an #[automock] attribute macro to your trait definition. This generates a mock struct, typically named MockMyTrait for a trait MyTrait. In your test, you instantiate this mock and configure its behavior. You set "expectations" on its methods, specifying required arguments, number of calls, and what value to return. For example, mock.expect_foo().with(predicate::eq(4)).times(1).returning(|x| x + 1); sets up a precise expectation. When you pass the mock to your function, it will panic if the actual calls deviate from this setup.
WHEN TO USE IT: Mocking is most effective when your code is designed around interfaces (traits). Use it to isolate your application logic from external boundaries like databases, network clients, or file systems. This allows you to simulate various scenarios, including error conditions or edge cases, without needing the real dependencies to be active.
WHEN NOT TO USE IT: Avoid mocking types you don't own, as their internal behavior can change and break your tests. It's also overkill for simple, pure functions or data objects; just use the real thing if it's fast and has no side effects. A common footgun is mocking too deeply, which can lead to brittle tests that break on minor, harmless refactoring.
ONE CANONICAL EXAMPLE: // 1. Define a trait and annotate it #[automock] trait MyTrait { fn foo(&self, x: u32) -> u32; }
// 2. Write a function that depends on the trait fn call_with_four(x: &dyn MyTrait) -> u32 { x.foo(4) }
// 3. In a test, create and configure the mock let mut mock = MockMyTrait::new(); mock.expect_foo() .with(predicate::eq(4)) .times(1) .returning(|x| x + 1);
// 4. Call the function with the mock and assert assert_eq!(5, call_with_four(&mock));
Read the original → docs.rs
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.