751-mock-trait-pattern — Mock Trait Pattern
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
EmailSender as a trait with a send methodNotificationService<E: EmailSender>MockEmailSender using RefCell<Vec<SentMessage>> to record callsdyn 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
dyn Trait; OCaml uses module types + functors or first-class modules.RefCell is needed to mutate the call log through a &self reference; OCaml uses ref cells or Queue.t directly.mockall crate generates mock structs from trait definitions via derive macros; OCaml's mockmod is less mature.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);
}
}#[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
| Aspect | OCaml | Rust |
|---|---|---|
| Abstraction | First-class modules | Traits + generics |
| Interior mutability | ref | RefCell |
| Mocking libraries | Less common | mockall, mockito |
| Type inference | Full | May need turbofish |
| Runtime polymorphism | Functors | dyn Trait |
Exercises
FailingEmailSender that always returns Err("SMTP connection refused") and write tests for how NotificationService handles delivery failures.RecordingEmailSender that records calls AND delegates to another sender — useful for integration tests that log without blocking real sends.dyn EmailSender instead of a generic parameter in NotificationService and compare the ergonomics and performance implications.