Home/Testing Strategies/Mocking & Test Doubles

Mocking & Test Doubles

Isolating dependencies in tests

intermediate
mockingmockalltest-doubles
🎮 Interactive Playground

What is Mocking?

Mocking replaces real dependencies with controlled test doubles. This isolates the code under test, making tests faster, more reliable, and easier to write.

The Problem

Real dependencies cause testing challenges:

  • External services: APIs, databases are slow and unreliable
  • Side effects: Sending emails, charging payments
  • State: Hard to set up specific scenarios
  • Determinism: Time, randomness vary between runs

Example Code

use std::collections::HashMap;
use std::sync::Arc;

/// Trait for dependencies we want to mock
pub trait UserRepository: Send + Sync {
    fn find_by_id(&self, id: u64) -> Option<User>;
    fn find_by_email(&self, email: &str) -> Option<User>;
    fn save(&mut self, user: &User) -> Result<(), RepositoryError>;
    fn delete(&mut self, id: u64) -> Result<(), RepositoryError>;
}

pub trait EmailService: Send + Sync {
    fn send_email(&self, to: &str, subject: &str, body: &str) -> Result<(), EmailError>;
}

pub trait TimeProvider: Send + Sync {
    fn now(&self) -> u64; // Unix timestamp
}

#[derive(Debug, Clone, PartialEq)]
pub struct User {
    pub id: u64,
    pub email: String,
    pub name: String,
    pub created_at: u64,
}

#[derive(Debug, PartialEq)]
pub enum RepositoryError {
    NotFound,
    DuplicateEmail,
    ConnectionError,
}

#[derive(Debug, PartialEq)]
pub enum EmailError {
    InvalidAddress,
    SendFailed,
}

/// Service that uses the dependencies
pub struct UserService<R: UserRepository, E: EmailService, T: TimeProvider> {
    repo: R,
    email: E,
    time: T,
}

impl<R: UserRepository, E: EmailService, T: TimeProvider> UserService<R, E, T> {
    pub fn new(repo: R, email: E, time: T) -> Self {
        UserService { repo, email, time }
    }

    pub fn register_user(&mut self, email: &str, name: &str) -> Result<User, String> {
        // Check if email already exists
        if self.repo.find_by_email(email).is_some() {
            return Err("Email already registered".to_string());
        }

        let user = User {
            id: self.time.now(), // Use timestamp as ID (simplified)
            email: email.to_string(),
            name: name.to_string(),
            created_at: self.time.now(),
        };

        self.repo.save(&user)
            .map_err(|e| format!("Failed to save: {:?}", e))?;

        // Send welcome email
        self.email.send_email(
            email,
            "Welcome!",
            &format!("Hello {}, welcome to our service!", name)
        ).map_err(|e| format!("Failed to send email: {:?}", e))?;

        Ok(user)
    }

    pub fn get_user(&self, id: u64) -> Option<User> {
        self.repo.find_by_id(id)
    }
}

/// Manual mock implementation
pub struct MockUserRepository {
    users: HashMap<u64, User>,
    pub find_by_id_calls: std::cell::RefCell<Vec<u64>>,
    pub save_calls: std::cell::RefCell<Vec<User>>,
    pub should_fail_save: bool,
}

impl MockUserRepository {
    pub fn new() -> Self {
        MockUserRepository {
            users: HashMap::new(),
            find_by_id_calls: std::cell::RefCell::new(Vec::new()),
            save_calls: std::cell::RefCell::new(Vec::new()),
            should_fail_save: false,
        }
    }

    pub fn with_user(mut self, user: User) -> Self {
        self.users.insert(user.id, user);
        self
    }

    pub fn fail_on_save(mut self) -> Self {
        self.should_fail_save = true;
        self
    }
}

impl Default for MockUserRepository {
    fn default() -> Self {
        Self::new()
    }
}

impl UserRepository for MockUserRepository {
    fn find_by_id(&self, id: u64) -> Option<User> {
        self.find_by_id_calls.borrow_mut().push(id);
        self.users.get(&id).cloned()
    }

    fn find_by_email(&self, email: &str) -> Option<User> {
        self.users.values().find(|u| u.email == email).cloned()
    }

    fn save(&mut self, user: &User) -> Result<(), RepositoryError> {
        self.save_calls.borrow_mut().push(user.clone());

        if self.should_fail_save {
            return Err(RepositoryError::ConnectionError);
        }

        self.users.insert(user.id, user.clone());
        Ok(())
    }

    fn delete(&mut self, id: u64) -> Result<(), RepositoryError> {
        self.users.remove(&id)
            .map(|_| ())
            .ok_or(RepositoryError::NotFound)
    }
}

/// Mock email service that records calls
pub struct MockEmailService {
    pub sent_emails: std::cell::RefCell<Vec<(String, String, String)>>,
    pub should_fail: bool,
}

impl MockEmailService {
    pub fn new() -> Self {
        MockEmailService {
            sent_emails: std::cell::RefCell::new(Vec::new()),
            should_fail: false,
        }
    }

    pub fn fail_on_send(mut self) -> Self {
        self.should_fail = true;
        self
    }
}

impl Default for MockEmailService {
    fn default() -> Self {
        Self::new()
    }
}

impl EmailService for MockEmailService {
    fn send_email(&self, to: &str, subject: &str, body: &str) -> Result<(), EmailError> {
        if self.should_fail {
            return Err(EmailError::SendFailed);
        }

        self.sent_emails.borrow_mut().push((
            to.to_string(),
            subject.to_string(),
            body.to_string(),
        ));
        Ok(())
    }
}

/// Mock time provider for deterministic testing
pub struct MockTimeProvider {
    pub current_time: u64,
}

impl MockTimeProvider {
    pub fn new(time: u64) -> Self {
        MockTimeProvider { current_time: time }
    }
}

impl TimeProvider for MockTimeProvider {
    fn now(&self) -> u64 {
        self.current_time
    }
}

/// Real implementations (for production)
pub struct PostgresUserRepository {
    // connection pool, etc.
}

pub struct SmtpEmailService {
    // SMTP client, etc.
}

pub struct SystemTimeProvider;

impl TimeProvider for SystemTimeProvider {
    fn now(&self) -> u64 {
        use std::time::{SystemTime, UNIX_EPOCH};
        SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs()
    }
}

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

    fn create_test_service() -> (
        UserService<MockUserRepository, MockEmailService, MockTimeProvider>,
        MockUserRepository,
        MockEmailService,
    ) {
        let repo = MockUserRepository::new();
        let email = MockEmailService::new();
        let time = MockTimeProvider::new(1000);

        // Note: We need to clone/share state or restructure for verification
        // This is simplified for demonstration
        (UserService::new(
            MockUserRepository::new(),
            MockEmailService::new(),
            MockTimeProvider::new(1000),
        ), repo, email)
    }

    #[test]
    fn test_register_user_success() {
        let repo = MockUserRepository::new();
        let email = MockEmailService::new();
        let time = MockTimeProvider::new(1000);

        let mut service = UserService::new(repo, email, time);

        let result = service.register_user("test@example.com", "Test User");

        assert!(result.is_ok());
        let user = result.unwrap();
        assert_eq!(user.email, "test@example.com");
        assert_eq!(user.name, "Test User");
        assert_eq!(user.created_at, 1000);
    }

    #[test]
    fn test_register_duplicate_email() {
        let existing_user = User {
            id: 1,
            email: "taken@example.com".to_string(),
            name: "Existing".to_string(),
            created_at: 500,
        };

        let repo = MockUserRepository::new().with_user(existing_user);
        let email = MockEmailService::new();
        let time = MockTimeProvider::new(1000);

        let mut service = UserService::new(repo, email, time);

        let result = service.register_user("taken@example.com", "New User");

        assert!(result.is_err());
        assert_eq!(result.unwrap_err(), "Email already registered");
    }

    #[test]
    fn test_register_handles_save_failure() {
        let repo = MockUserRepository::new().fail_on_save();
        let email = MockEmailService::new();
        let time = MockTimeProvider::new(1000);

        let mut service = UserService::new(repo, email, time);

        let result = service.register_user("test@example.com", "Test");

        assert!(result.is_err());
        assert!(result.unwrap_err().contains("Failed to save"));
    }

    #[test]
    fn test_register_handles_email_failure() {
        let repo = MockUserRepository::new();
        let email = MockEmailService::new().fail_on_send();
        let time = MockTimeProvider::new(1000);

        let mut service = UserService::new(repo, email, time);

        let result = service.register_user("test@example.com", "Test");

        assert!(result.is_err());
        assert!(result.unwrap_err().contains("Failed to send email"));
    }

    #[test]
    fn test_get_user() {
        let user = User {
            id: 42,
            email: "alice@example.com".to_string(),
            name: "Alice".to_string(),
            created_at: 1000,
        };

        let repo = MockUserRepository::new().with_user(user.clone());
        let email = MockEmailService::new();
        let time = MockTimeProvider::new(1000);

        let service = UserService::new(repo, email, time);

        let found = service.get_user(42);
        assert_eq!(found, Some(user));

        let not_found = service.get_user(999);
        assert_eq!(not_found, None);
    }
}

// Example using mockall crate (when available)
#[cfg(feature = "mockall")]
mod mockall_example {
    use mockall::{automock, predicate::*};

    #[automock]
    pub trait Database {
        fn get(&self, key: &str) -> Option<String>;
        fn set(&mut self, key: &str, value: &str) -> Result<(), String>;
    }

    #[test]
    fn test_with_mockall() {
        let mut mock = MockDatabase::new();

        mock.expect_get()
            .with(eq("key1"))
            .times(1)
            .returning(|_| Some("value1".to_string()));

        mock.expect_set()
            .with(eq("key2"), eq("value2"))
            .times(1)
            .returning(|_, _| Ok(()));

        assert_eq!(mock.get("key1"), Some("value1".to_string()));
        assert!(mock.set("key2", "value2").is_ok());
    }
}

fn main() {
    println!("Run tests with: cargo test");
    println!("With mockall: cargo test --features mockall");
}

Why This Works

  1. Trait-based injection: Dependencies are traits, easily swapped
  2. Call recording: Mocks track what was called and with what arguments
  3. Configurable behavior: Set up success, failure, specific return values
  4. Isolation: Test one component without its real dependencies

Mock Types

| Type | Purpose | Example |

|------|---------|---------|

| Dummy | Placeholder, not used | MockLogger that does nothing |

| Stub | Returns preset values | MockTime returning fixed timestamp |

| Spy | Records calls for verification | MockEmail storing sent messages |

| Mock | Verifies expectations | mockall's expect_* methods |

| Fake | Simplified working implementation | In-memory database |

When to Use

  • External services: APIs, databases, file systems
  • Non-determinism: Time, random numbers
  • Slow operations: Network calls, heavy computation
  • Side effects: Emails, payments, notifications

⚠️ Anti-patterns

// DON'T: Mock everything
fn test_add() {
    let mock_adder = MockAdder::new();
    mock_adder.expect_add().returning(|a, b| a + b);
    // Why mock pure functions?
}

// DON'T: Test the mock, not the code
#[test]
fn test_mock_works() {
    let mock = MockRepo::new();
    mock.save(&user);
    assert!(mock.was_called()); // This tests the mock!
}

// DO: Test behavior through the mock
#[test]
fn test_service_saves_user() {
    let mut service = create_service_with_mocks();
    service.register_user("email", "name").unwrap();
    // Verify the service behaved correctly
}

Exercises

  1. Add verification that email was sent with correct content
  2. Create a mock that returns different values on sequential calls
  3. Implement a fake in-memory database
  4. Use mockall to test an async service

🎮 Try it Yourself

🎮

Mocking & Test Doubles - Playground

Run this code in the official Rust Playground