Mocking

Consider using:

  • mockall: A popular crate for creating mock objects for testing.
  • faux: Another mocking library.

Mock with mockall

mockall mockall-crates.io mockall-github mockall-lib.rs cat-development-tools::testing

A powerful mock object library for Rust.

use mockall::predicate::*;
use mockall::*;

// First, we need to add mockall to our Cargo.toml
// [dependencies]
// mockall = "0.11.3"

// Define our data model
#[derive(Debug, Clone, PartialEq)]
struct User {
    id: u64,
    name: String,
    email: String,
}

// Define our repository trait with the `#[automock]` attribute.
// It automatically generates a mock implementation of your trait, named
// Mock{TraitName}. In our case, it creates a `MockUserRepository`.
#[automock]
trait UserRepository {
    fn find_by_id(&self, id: u64) -> Option<User>;
    fn save(&self, user: User) -> Result<(), String>;
}

// Define our service that uses the repository.
struct UserService<T: UserRepository> {
    repository: T,
}

impl<T: UserRepository> UserService<T> {
    fn new(repository: T) -> Self {
        Self { repository }
    }

    fn get_user(&self, id: u64) -> Result<User, String> {
        self.repository
            .find_by_id(id)
            .ok_or_else(|| format!("User with id {} not found", id))
    }

    fn update_user_email(
        &self,
        id: u64,
        new_email: String,
    ) -> Result<User, String> {
        // Get the user
        let mut user = self.get_user(id)?;

        // Update the email
        user.email = new_email;

        // Save the user
        self.repository.save(user.clone())?;

        Ok(user)
    }
}

// Now, let's write our tests using mockall
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_get_user_success() {
        // Create a mock repository
        let mut mock_repo = MockUserRepository::new();

        // Set up expectations
        mock_repo
            .expect_find_by_id() // The method we're setting expectations for
            .with(eq(1)) // The argument we expect (using predicate). You can also use custom functions: .withf(|user| user.id == 1)
            .times(1)  // How many times we expect it to be called
            .returning(|_| { // What it should return
                Some(User {
                    id: 1,
                    name: "John Doe".to_string(),
                    email: "john@example.com".to_string(),
                })
            });

        // Create our service with the mock repository
        let service = UserService::new(mock_repo);

        // Call the method we want to test
        let result = service.get_user(1);

        // Assert the result
        assert!(result.is_ok());
        let user = result.unwrap();
        assert_eq!(user.id, 1);
        assert_eq!(user.name, "John Doe");
        assert_eq!(user.email, "john@example.com");
    }

    #[test]
    fn test_get_user_not_found() {
        // Create a mock repository
        let mut mock_repo = MockUserRepository::new();

        // Set up expectations - return None for any id
        mock_repo
            .expect_find_by_id()
            .with(eq(999))
            .times(1)
            .returning(|_| None);

        // Create our service with the mock repository
        let service = UserService::new(mock_repo);

        // Call the method we want to test
        let result = service.get_user(999);

        // Assert the result
        assert!(result.is_err());
        assert_eq!(result.unwrap_err(), "User with id 999 not found");
    }

    #[test]
    fn test_update_user_email() {
        // Create a mock repository
        let mut mock_repo = MockUserRepository::new();

        // Set up expectations for find_by_id
        mock_repo
            .expect_find_by_id()
            .with(eq(1))
            .times(1)
            .returning(|_| {
                Some(User {
                    id: 1,
                    name: "John Doe".to_string(),
                    email: "john@example.com".to_string(),
                })
            });

        // Set up expectations for save
        mock_repo
            .expect_save()
            .withf(|user: &User| {
                user.id == 1
                    && user.name == "John Doe"
                    && user.email == "newemail@example.com"
            })
            .times(1)
            .returning(|_| Ok(()));

        // Create our service with the mock repository
        let service = UserService::new(mock_repo);

        // Call the method we want to test
        let result =
            service.update_user_email(1, "newemail@example.com".to_string());

        // Assert the result
        assert!(result.is_ok());
        let updated_user = result.unwrap();
        assert_eq!(updated_user.id, 1);
        assert_eq!(updated_user.name, "John Doe");
        assert_eq!(updated_user.email, "newemail@example.com");
    }
}

Mock Structs with faux

faux faux-crates.io faux-github faux-lib.rs

A library to mock structs.

use faux::when;

// `faux` allows you to mock the methods of structs for testing without
// complicating or polluting your code.

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

// The trait we want to mock
trait Database {
    fn get_user(&self, id: u64) -> Option<User>;
    fn save_user(&self, user: &User) -> bool;
}

// A struct using the `Database` trait
struct UserService<D: Database> {
    database: D,
}

// Implementation using the trait
impl<D: Database> UserService<D> {
    fn new(database: D) -> Self {
        Self { database }
    }

    fn get_user_name(&self, id: u64) -> Option<String> {
        self.database.get_user(id).map(|user| user.name)
    }

    fn update_user_name(&self, id: u64, name: String) -> bool {
        if let Some(mut user) = self.database.get_user(id) {
            user.name = name;
            self.database.save_user(&user)
        } else {
            false
        }
    }
}

// Create a mockable implementation
#[faux::create]
struct MockDatabase {}

// Implement the trait for our mock
#[faux::methods]
impl Database for MockDatabase {
    fn get_user(&self, _id: u64) -> Option<User> {
        // This will be mocked
        unimplemented!()
    }

    fn save_user(&self, _user: &User) -> bool {
        // This will be mocked
        unimplemented!()
    }
}

// Test using the mock
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_get_user_name() {
        // Create a mock
        let mut mock_db = MockDatabase::faux();

        // Define behavior
        when!(mock_db.get_user).then(|id| {
            if id == 1 {
                Some(User {
                    id: 1,
                    name: "Alice".to_string(),
                })
            } else {
                None
            }
        });

        // Create service with mock
        let service = UserService::new(mock_db);

        // Test the service
        assert_eq!(service.get_user_name(1), Some("Alice".to_string()));
        assert_eq!(service.get_user_name(2), None);
    }

    #[test]
    fn test_update_user_name() {
        // Create a mock
        let mut mock_db = MockDatabase::faux();
        let user = User {
            id: 1,
            name: "Alice".to_string(),
        };

        // Configure get_user to return our test user
        when!(mock_db.get_user)
            .then(move |id| if id == 1 { Some(user.clone()) } else { None });

        // Configure save_user to succeed
        when!(mock_db.save_user).then(|_| true);

        // Create service with mock
        let service = UserService::new(mock_db);

        // Test update succeeds
        assert!(service.update_user_name(1, "Bob".to_string()));
        // Test update fails for non-existent user
        assert!(!service.update_user_name(2, "Charlie".to_string()));
    }
}