ExamplesBy LevelBy TopicLearning Paths
751 Fundamental

751-mock-trait-pattern — Mock Trait Pattern

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "751-mock-trait-pattern — Mock Trait Pattern" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Code that sends emails, makes HTTP requests, or writes to databases is hard to test: real calls are slow, cost money, and have side effects. Key difference from OCaml: 1. **Mechanism**: Rust uses traits + generics or `dyn Trait`; OCaml uses module types + functors or first

Tutorial

The Problem

Code that sends emails, makes HTTP requests, or writes to databases is hard to test: real calls are slow, cost money, and have side effects. The mock trait pattern defines a dependency as a trait, injects it through generics or dyn Trait, and provides a test implementation that records calls without performing real work. This is the foundation of dependency injection in Rust and is used in every major Rust web framework's testing guide.

🎯 Learning Outcomes

  • • Define EmailSender as a trait with a send method
  • • Inject the dependency generically: NotificationService<E: EmailSender>
  • • Implement MockEmailSender using RefCell<Vec<SentMessage>> to record calls
  • • Verify recorded calls in tests: count, recipients, subjects, bodies
  • • Understand when to use dyn Trait (runtime polymorphism) vs. generics (compile-time)
  • Code Example

    pub trait EmailSender {
        fn send(&self, to: &str, subject: &str, body: &str) -> Result<(), String>;
    }
    
    pub struct NotificationService<E: EmailSender> {
        sender: E,
    }

    Key Differences

  • Mechanism: Rust uses traits + generics or dyn Trait; OCaml uses module types + functors or first-class modules.
  • Interior mutability: Rust's RefCell is needed to mutate the call log through a &self reference; OCaml uses ref cells or Queue.t directly.
  • Ergonomics: Rust's mockall crate generates mock structs from trait definitions via derive macros; OCaml's mockmod is less mature.
  • Runtime cost: Rust generics generate monomorphized code with zero dyn overhead; OCaml's functor application is also zero-cost.
  • OCaml Approach

    OCaml modules serve as the primary abstraction for mocking. A functor Make(Sender: SENDER_SIG) creates a service that depends on the SENDER_SIG module type. Tests pass a mock module to the functor. OCaml's first-class modules and functors make this pattern very natural. Alternatively, mutable reference cells (ref) capture calls in a mock module implementation.

    Full Source

    #![allow(clippy::all)]
    //! # Mock Trait Pattern
    //!
    //! Test doubles without external crates using trait-based mocking.
    
    use std::cell::RefCell;
    
    /// The dependency trait that will be mocked
    pub trait EmailSender {
        fn send(&self, to: &str, subject: &str, body: &str) -> Result<(), String>;
    }
    
    /// Real implementation for production
    pub struct SmtpSender {
        pub host: String,
        pub port: u16,
    }
    
    impl EmailSender for SmtpSender {
        fn send(&self, to: &str, subject: &str, body: &str) -> Result<(), String> {
            // In production, this would send a real email
            println!(
                "[SMTP {}:{}] To={} Subject={} Body={}",
                self.host, self.port, to, subject, body
            );
            Ok(())
        }
    }
    
    /// A service that depends on EmailSender
    pub struct NotificationService<E: EmailSender> {
        sender: E,
    }
    
    impl<E: EmailSender> NotificationService<E> {
        pub fn new(sender: E) -> Self {
            NotificationService { sender }
        }
    
        pub fn notify_user(&self, email: &str, message: &str) -> Result<(), String> {
            self.sender.send(email, "Notification", message)
        }
    
        pub fn send_welcome(&self, email: &str, name: &str) -> Result<(), String> {
            let body = format!("Welcome, {}!", name);
            self.sender.send(email, "Welcome to our service", &body)
        }
    }
    
    /// Mock implementation for testing - records all calls
    pub struct MockEmailSender {
        pub calls: RefCell<Vec<(String, String, String)>>,
        pub should_fail: bool,
    }
    
    impl MockEmailSender {
        pub fn new() -> Self {
            MockEmailSender {
                calls: RefCell::new(Vec::new()),
                should_fail: false,
            }
        }
    
        pub fn failing() -> Self {
            MockEmailSender {
                calls: RefCell::new(Vec::new()),
                should_fail: true,
            }
        }
    
        pub fn call_count(&self) -> usize {
            self.calls.borrow().len()
        }
    
        pub fn last_call(&self) -> Option<(String, String, String)> {
            self.calls.borrow().last().cloned()
        }
    }
    
    impl Default for MockEmailSender {
        fn default() -> Self {
            Self::new()
        }
    }
    
    impl EmailSender for MockEmailSender {
        fn send(&self, to: &str, subject: &str, body: &str) -> Result<(), String> {
            self.calls
                .borrow_mut()
                .push((to.to_string(), subject.to_string(), body.to_string()));
    
            if self.should_fail {
                Err("Mock failure".to_string())
            } else {
                Ok(())
            }
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_notify_user_sends_email() {
            let mock = MockEmailSender::new();
            let service = NotificationService::new(mock);
    
            service.notify_user("user@example.com", "Hello!").unwrap();
    
            let calls = service.sender.calls.borrow();
            assert_eq!(calls.len(), 1);
            assert_eq!(calls[0].0, "user@example.com");
            assert_eq!(calls[0].1, "Notification");
            assert_eq!(calls[0].2, "Hello!");
        }
    
        #[test]
        fn test_send_welcome_formats_name() {
            let mock = MockEmailSender::new();
            let service = NotificationService::new(mock);
    
            service.send_welcome("alice@example.com", "Alice").unwrap();
    
            let (_, _, body) = service.sender.last_call().unwrap();
            assert!(body.contains("Alice"));
        }
    
        #[test]
        fn test_failing_sender() {
            let mock = MockEmailSender::failing();
            let service = NotificationService::new(mock);
    
            let result = service.notify_user("user@example.com", "test");
            assert!(result.is_err());
        }
    
        #[test]
        fn test_multiple_calls_recorded() {
            let mock = MockEmailSender::new();
            let service = NotificationService::new(mock);
    
            service.notify_user("a@example.com", "msg1").unwrap();
            service.notify_user("b@example.com", "msg2").unwrap();
            service.notify_user("c@example.com", "msg3").unwrap();
    
            assert_eq!(service.sender.call_count(), 3);
        }
    
        #[test]
        fn test_mock_default() {
            let mock = MockEmailSender::default();
            assert_eq!(mock.call_count(), 0);
            assert!(!mock.should_fail);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_notify_user_sends_email() {
            let mock = MockEmailSender::new();
            let service = NotificationService::new(mock);
    
            service.notify_user("user@example.com", "Hello!").unwrap();
    
            let calls = service.sender.calls.borrow();
            assert_eq!(calls.len(), 1);
            assert_eq!(calls[0].0, "user@example.com");
            assert_eq!(calls[0].1, "Notification");
            assert_eq!(calls[0].2, "Hello!");
        }
    
        #[test]
        fn test_send_welcome_formats_name() {
            let mock = MockEmailSender::new();
            let service = NotificationService::new(mock);
    
            service.send_welcome("alice@example.com", "Alice").unwrap();
    
            let (_, _, body) = service.sender.last_call().unwrap();
            assert!(body.contains("Alice"));
        }
    
        #[test]
        fn test_failing_sender() {
            let mock = MockEmailSender::failing();
            let service = NotificationService::new(mock);
    
            let result = service.notify_user("user@example.com", "test");
            assert!(result.is_err());
        }
    
        #[test]
        fn test_multiple_calls_recorded() {
            let mock = MockEmailSender::new();
            let service = NotificationService::new(mock);
    
            service.notify_user("a@example.com", "msg1").unwrap();
            service.notify_user("b@example.com", "msg2").unwrap();
            service.notify_user("c@example.com", "msg3").unwrap();
    
            assert_eq!(service.sender.call_count(), 3);
        }
    
        #[test]
        fn test_mock_default() {
            let mock = MockEmailSender::default();
            assert_eq!(mock.call_count(), 0);
            assert!(!mock.should_fail);
        }
    }

    Deep Comparison

    OCaml vs Rust: Mock Trait Pattern

    Dependency Injection via Traits/Modules

    Rust

    pub trait EmailSender {
        fn send(&self, to: &str, subject: &str, body: &str) -> Result<(), String>;
    }
    
    pub struct NotificationService<E: EmailSender> {
        sender: E,
    }
    

    OCaml (Modules)

    module type EMAIL_SENDER = sig
      val send : to_:string -> subject:string -> body:string -> (unit, string) result
    end
    
    module NotificationService (E : EMAIL_SENDER) = struct
      let notify_user email message =
        E.send ~to_:email ~subject:"Notification" ~body:message
    end
    

    Mock Implementation

    Rust

    pub struct MockEmailSender {
        pub calls: RefCell<Vec<(String, String, String)>>,
        pub should_fail: bool,
    }
    
    impl EmailSender for MockEmailSender {
        fn send(&self, to: &str, subject: &str, body: &str) -> Result<(), String> {
            self.calls.borrow_mut().push((to.into(), subject.into(), body.into()));
            if self.should_fail { Err("Mock failure".into()) } else { Ok(()) }
        }
    }
    

    OCaml

    module MockEmail : EMAIL_SENDER = struct
      let calls = ref []
      let should_fail = ref false
      
      let send ~to_ ~subject ~body =
        calls := (to_, subject, body) :: !calls;
        if !should_fail then Error "Mock failure" else Ok ()
    end
    

    Key Differences

    AspectOCamlRust
    AbstractionFirst-class modulesTraits + generics
    Interior mutabilityrefRefCell
    Mocking librariesLess commonmockall, mockito
    Type inferenceFullMay need turbofish
    Runtime polymorphismFunctorsdyn Trait

    Exercises

  • Add a FailingEmailSender that always returns Err("SMTP connection refused") and write tests for how NotificationService handles delivery failures.
  • Implement a RecordingEmailSender that records calls AND delegates to another sender — useful for integration tests that log without blocking real sends.
  • Use dyn EmailSender instead of a generic parameter in NotificationService and compare the ergonomics and performance implications.
  • Open Source Repos