Mocking in Rust with mockall: A Practical Guide

Mocking in Rust with mockall: A Practical Guide

Rust's borrow checker and type system make certain mocking patterns harder than in dynamic languages, but the mockall crate brings first-class mocking support to Rust's test ecosystem. This guide covers the practical patterns you'll reach for in real projects.

Why Mock?

Mocks replace real dependencies in unit tests. Instead of hitting a database, making network calls, or reading from the filesystem, a mock records what methods were called and returns pre-programmed values.

The goal is to test a unit in isolation: verify that it calls its dependencies correctly, handles their responses correctly, and handles their errors correctly.

Installation

[dev-dependencies]
mockall = "0.13"

The Core Pattern: Trait + #[automock]

mockall works by generating a Mock* struct for any trait you annotate with #[automock]:

use mockall::automock;

#[automock]
trait EmailService {
    fn send(&self, to: &str, subject: &str, body: &str) -> Result<(), String>;
}

This generates MockEmailService, a struct that implements EmailService with configurable behavior.

Using the Mock

#[cfg(test)]
mod tests {
    use super::*;
    use mockall::predicate::*;

    #[test]
    fn test_send_welcome_email() {
        let mut mock = MockEmailService::new();

        mock.expect_send()
            .with(eq("user@example.com"), eq("Welcome"), any())
            .times(1)
            .returning(|_, _, _| Ok(()));

        let result = send_welcome_email(&mock, "user@example.com");
        assert!(result.is_ok());
    }
}

expect_send() sets up an expectation:

  • .with(...) specifies argument matchers
  • .times(1) asserts the method is called exactly once
  • .returning(...) provides the return value

When the test ends, mockall checks all expectations. If any unmet, the test fails.

Argument Matchers

mockall provides a rich set of matchers via the predicate module:

use mockall::predicate::*;

mock.expect_get_user()
    .with(eq(42))          // exact equality
    .returning(|_| Ok(user));

mock.expect_search()
    .with(str::contains("rust"))   // string contains
    .returning(|_| vec![]);

mock.expect_process()
    .with(function(|x: &i32| *x > 0))   // custom predicate
    .returning(|_| Ok(()));

For argument types that don't implement PartialEq, use always() to match any value:

mock.expect_log()
    .with(always())        // match any argument
    .return_const(());

Return Values

Fixed Return Values

mock.expect_is_premium()
    .return_const(true);   // always returns true

Computed Return Values

mock.expect_get_user()
    .returning(|id| {
        Ok(User { id, name: format!("User {}", id) })
    });

Returning Errors

mock.expect_fetch()
    .returning(|_| Err("network error".into()));

Call Count Expectations

mock.expect_notify()
    .times(3);          // exactly 3 calls

mock.expect_log()
    .times(1..=5);      // between 1 and 5 calls

mock.expect_heartbeat()
    .times(..);         // any number of calls (0 or more)

mock.expect_cleanup()
    .times(0);          // never called

Ordered Calls

Verify that methods are called in a specific sequence:

use mockall::Sequence;

let mut seq = Sequence::new();

mock.expect_open()
    .times(1)
    .in_sequence(&mut seq)
    .returning(|| Ok(()));

mock.expect_write()
    .times(1)
    .in_sequence(&mut seq)
    .returning(|_| Ok(()));

mock.expect_close()
    .times(1)
    .in_sequence(&mut seq)
    .returning(|| Ok(()));

If write is called before open, the test fails.

Mocking Structs with #[automock] on impl Blocks

When you own the struct and can't use a trait, mock the impl block directly:

use mockall::automock;

struct HttpClient;

#[automock]
impl HttpClient {
    fn get(&self, url: &str) -> Result<String, String> {
        // real implementation
        Ok(String::new())
    }
}

This generates MockHttpClient with the same interface.

Mocking External Traits with mock!

For traits from external crates, use the mock! macro:

use mockall::mock;

mock! {
    pub HttpClient {
        fn get(&self, url: &str) -> Result<String, reqwest::Error>;
        fn post(&self, url: &str, body: &str) -> Result<String, reqwest::Error>;
    }
}

Dependency Injection Pattern

mockall works best when your code receives its dependencies via constructor injection or as generic parameters:

Via Generic Bounds

fn send_report<E: EmailService>(service: &E, data: &ReportData) -> Result<(), String> {
    let body = render_report(data);
    service.send("admin@example.com", "Daily Report", &body)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_send_report() {
        let mut mock = MockEmailService::new();
        mock.expect_send()
            .times(1)
            .returning(|_, _, _| Ok(()));

        let data = ReportData::default();
        assert!(send_report(&mock, &data).is_ok());
    }
}

Via Box<dyn Trait>

struct OrderProcessor {
    email: Box<dyn EmailService>,
}

impl OrderProcessor {
    fn new(email: Box<dyn EmailService>) -> Self {
        Self { email }
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_order_confirmation() {
        let mut mock = MockEmailService::new();
        mock.expect_send()
            .times(1)
            .returning(|_, _, _| Ok(()));

        let processor = OrderProcessor::new(Box::new(mock));
        processor.confirm_order(order_id);
    }
}

Static Methods and once

For one-time calls that should return different values:

mock.expect_generate_id()
    .once()
    .returning(|| 1001);

mock.expect_generate_id()
    .once()
    .returning(|| 1002);

Each .once() expectation consumes one call in FIFO order.

Common Mistakes

Forgetting mut on the mock. expect_* methods take &mut self, so the mock variable must be declared mut.

Expecting after use. Set up all expectations before passing the mock to the system under test.

Clone required for capture. If your returning closure captures a value that needs to be returned multiple times, it must be Clone. Use return_const for simple cases.

Missing Send for async. Async mocks require Send + Sync. mockall supports this via #[async_trait] combined with mockall.

Testing with HelpMeTest

Unit tests with mocks validate logic in isolation. Once your service is deployed, HelpMeTest monitors the real integrations — the ones that can't be mocked — 24/7 against your live environment. No source code needed.

Summary

  • #[automock] generates Mock* structs from traits
  • .expect_method() chains set up expectations, matchers, call counts, and return values
  • Use predicate::eq, predicate::str::contains, and predicate::function for argument matching
  • Inject dependencies as generics or Box<dyn Trait> to make code mockable
  • Call count verification is automatic — unmet expectations fail the test at drop time

Read more