How to mock dependencies

Implement a MockMessenger struct using RefCell to record messages for testing LimitTracker without external dependencies.

The problem with real dependencies

You're building a notification service. It takes a user ID, checks their preferences, and sends an email. You write a test for the happy path. You run it. Your inbox explodes. Or worse, the test hangs because the email provider is rate-limiting you, and now your CI pipeline is stuck waiting for a response that will never come.

You need a way to swap out the real email sender with something that just records what happened without touching the network. In Python, you might reach for mock.patch to replace the function globally. Rust has no equivalent. You cannot swap a function implementation at runtime. The compiler links everything statically. If you want to mock, you must design your code to accept the dependency as a parameter.

This forces a design choice: dependency injection. It feels like extra work upfront, but it makes your code testable by default. You define an interface, you pass an implementation, and in tests, you pass a fake.

Why Rust doesn't let you patch functions

Python and JavaScript developers often rely on monkey patching. You can replace requests.get with a dummy function inside a test block, and every part of the codebase that calls requests.get suddenly uses the dummy. Rust forbids this.

The compiler resolves function calls at compile time. If you call send_email, the binary contains exactly one version of that function. There is no global symbol table you can rewrite at runtime. This restriction eliminates a whole class of bugs where a mock leaks into production or where two tests interfere with each other by patching the same global state.

The trade-off is that you must structure your code to accept behavior as data. You define a trait that describes what the dependency does. You pass an object that implements that trait. In production, you pass the real object. In tests, you pass a mock. The rest of your code doesn't care which one it has.

This pattern is called polymorphism via trait objects. It's the Rust way to achieve loose coupling. You pay a small cost in boilerplate, and you gain total control over what your tests interact with.

The trait pattern

A trait defines a contract. It lists the methods a type must implement. When you want to mock a dependency, you start by extracting its behavior into a trait.

/// Defines the interface for sending messages.
/// Implementations can be real network calls or test mocks.
pub trait Messenger {
    fn send(&self, message: &str);
}

The &self receiver is key. It means the method takes an immutable reference to the implementer. This is the standard signature for operations that don't consume the object. Your production code takes a Box<dyn Messenger> or a generic parameter bounded by Messenger.

/// Tracks message limits and delegates sending to a Messenger.
pub struct LimitTracker {
    remaining_messages: u32,
    /// Holds the trait object. Box<dyn Trait> allows runtime polymorphism.
    messenger: Box<dyn Messenger>,
}

impl LimitTracker {
    pub fn new(messenger: Box<dyn Messenger>) -> LimitTracker {
        LimitTracker {
            remaining_messages: 10,
            messenger,
        }
    }

    pub fn send_message(&mut self, message: &str) {
        if self.remaining_messages > 0 {
            self.messenger.send(message);
            self.remaining_messages -= 1;
        }
    }
}

The Box<dyn Messenger> tells the compiler: "I don't care about the concrete type. I just need something that implements Messenger." This is a trait object. It erases the type and uses dynamic dispatch. The cost is a small indirection, which is negligible for I/O-bound operations like messaging.

Interior mutability with RefCell

Here's the friction point. The trait method takes &self. Rust enforces that &self means immutable access. If you want to record the message in your mock, you need to mutate state. You can't add a mut to the trait method signature without breaking the contract.

Rust solves this with interior mutability. RefCell<T> allows you to mutate the wrapped value through an immutable reference, by checking borrow rules at runtime instead of compile time.

use std::cell::RefCell;

/// A mock messenger that records sent messages for verification.
pub struct MockMessenger {
    /// RefCell allows mutation through &self by checking borrows at runtime.
    sent_messages: RefCell<Vec<String>>,
}

impl MockMessenger {
    pub fn new() -> MockMessenger {
        MockMessenger {
            sent_messages: RefCell::new(Vec::new()),
        }
    }
}

impl Messenger for MockMessenger {
    fn send(&self, message: &str) {
        // borrow_mut() creates a guard that tracks the mutable borrow.
        // The guard is dropped at the end of the block, releasing the lock.
        self.sent_messages.borrow_mut().push(message.to_string());
    }
}

RefCell maintains a borrow flag. When you call borrow_mut(), it checks if anyone else is holding a reference. If the flag is clear, it sets the flag and returns a guard. When the guard drops, the flag clears. If you try to borrow_mut() while a borrow is active, the program panics.

This panic is a safety net. It catches logic errors where you try to mutate state while it's being read. In tests, this is usually fine. You control the flow, and panics indicate a bug in your mock or test setup.

Convention aside: The community treats RefCell as a testing tool. It's acceptable in mocks and test helpers. In production code, RefCell is rare. You'd prefer Mutex for thread safety or redesign to avoid interior mutability. Don't let RefCell leak into your library API.

Walkthrough: what happens at runtime

When you call tracker.send_message("Hello"), the flow is straightforward.

  1. LimitTracker::send_message checks the limit.
  2. It calls self.messenger.send("Hello").
  3. Dynamic dispatch looks up the vtable for MockMessenger.
  4. MockMessenger::send runs. It calls borrow_mut() on the RefCell.
  5. The RefCell checks its flag. It's clear. It sets the flag and returns a guard.
  6. The guard gives mutable access to the Vec. The message is pushed.
  7. The guard drops at the end of the send method. The flag clears.
  8. Control returns to LimitTracker.

In your test, you can inspect the results.

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

    #[test]
    fn test_limit_tracker_sends_message() {
        let mock = MockMessenger::new();
        let mut tracker = LimitTracker::new(Box::new(mock));

        tracker.send_message("Test message");

        // borrow() returns an immutable guard.
        // We check the contents without mutating.
        assert_eq!(tracker.messenger.sent_messages.borrow().len(), 1);
        assert_eq!(
            tracker.messenger.sent_messages.borrow()[0],
            "Test message"
        );
    }
}

Wait. The test accesses tracker.messenger.sent_messages. That won't compile. messenger is a Box<dyn Messenger>. The compiler doesn't know it's a MockMessenger. You need to keep a handle to the mock.

Refactor the test to hold the mock separately.

#[test]
fn test_limit_tracker_sends_message() {
    let mock = MockMessenger::new();
    // Clone the mock? No, MockMessenger isn't Clone.
    // We need to pass ownership to LimitTracker, but keep a reference for assertions.
    // This is where the design gets tricky.
}

You can't easily assert on the mock if you've moved it into the LimitTracker. The standard solution is to use Rc<RefCell<...>> or to structure the mock so you can retrieve state. For simple mocks, a common pattern is to wrap the mock in Rc so you can share it.

use std::rc::Rc;

#[test]
fn test_limit_tracker_sends_message() {
    let mock = Rc::new(MockMessenger::new());
    // Clone the Rc to pass ownership to LimitTracker.
    // The underlying data is shared, not copied.
    let mut tracker = LimitTracker::new(Box::new(MockWrapper(Rc::clone(&mock))));

    tracker.send_message("Test message");

    // Assert on the original Rc handle.
    assert_eq!(mock.sent_messages.borrow().len(), 1);
}

// Wrapper to convert Rc<MockMessenger> into Box<dyn Messenger>
struct MockWrapper(Rc<MockMessenger>);

impl Messenger for MockWrapper {
    fn send(&self, message: &str) {
        self.0.send(message);
    }
}

This adds boilerplate. For production code, you might reach for a crate like mockall that generates these wrappers automatically. For learning and simple cases, the Rc pattern works. It highlights a core Rust concept: shared ownership via Rc allows multiple parts of the code to access the same data, while RefCell handles the mutation.

Pitfalls and compiler errors

Mocking in Rust exposes a few common traps.

Trait bound errors. If you forget to implement the trait for your mock, the compiler rejects the code with E0277 (the trait bound MockMessenger: Messenger is not satisfied). This is a compile-time safety check. You can't accidentally pass the wrong type.

Borrow panics. If your mock calls borrow_mut() while a previous borrow is still active, RefCell panics. This happens if you hold a reference to the mock's data across a call.

// BAD: Holding a borrow across a mutation
let messages = mock.sent_messages.borrow();
tracker.send_message("Oops"); // Panics! borrow_mut fails because borrow is active.

Drop the borrow before calling the method. The guard must be released.

Thread safety. RefCell is not thread-safe. If your trait is called from async code or multiple threads, RefCell will panic or cause data races. Use std::sync::Mutex instead. Mutex checks locks at runtime and works across threads. The API is similar, but Mutex requires lock() which returns a Result.

State leakage. Mocks must be fresh per test. If you reuse a mock instance, state from one test can bleed into another. Always create a new mock in each test function. Rust's ownership system helps here. If the mock is owned by the test, it's dropped when the test ends.

Decision matrix

Choose your mocking strategy based on the constraints of your trait and test environment.

Use a hand-rolled RefCell mock for single-threaded unit tests where you need to record multiple values and want minimal dependencies. Use a Mutex-wrapped mock when your trait is called from async code or multiple threads; RefCell panics on concurrent access, while Mutex provides safe synchronization. Use Cell<T> for mocks that only need to store a single simple value like a boolean flag or a counter; Cell is slightly faster and doesn't allocate, but only works for Copy types. Reach for a mocking crate like mockall when your trait has many methods, complex generics, or associated types; hand-rolling becomes tedious and error-prone, and mockall generates the boilerplate automatically.

Where to go next

Design for injection, test for free. If your code accepts traits, your mocks are just another implementation. Trust the borrow checker when it rejects your mock setup; it's usually catching a lifetime issue that would cause a panic later. Mock the interface, not the implementation, and your tests will stay robust as the code evolves.