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.

//! This example demonstrates how to use the `mockall` crate for mocking.
//!
//! First, add `mockall` to your `Cargo.toml` dependencies:
//! ```toml
//! [dependencies]
//! mockall = "0.11.3"
//! ```
//!
//! Then define a `UserRepository` trait and a `UserService` struct that uses
//! it. We then use `mockall` to create a mock implementation of
//! `UserRepository` for testing `UserService`.
use mockall::predicate::*;
use mockall::*;

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

/// The `UserRepository` trait defines the interface for interacting with user
/// data. The `#[automock]` attribute 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>;
}

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

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

    /// Retrieves a user by their ID.
    ///
    /// # Arguments
    ///
    /// * `id` - The ID of the user to retrieve.
    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))
    }

    /// Updates a user's email.
    ///
    /// # Arguments
    ///
    /// * `id` - The ID of the user to update.
    /// * `new_email` - The new email address.
    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)
    }
}

/// The `tests` module contains unit tests for `UserService` using the mock
/// repository.
#[cfg(test)]
mod tests {
    use super::*;

    /// Tests the `get_user` method when a user is found.
    ///
    /// This test verifies that `get_user` returns the correct user when the
    /// repository successfully finds a user by ID.
    #[test]
    fn test_get_user_success() {
        let mut mock_repo = MockUserRepository::new();

        // Set up expectations for find_by_id
        // We expect find_by_id to be called once with id 1
        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");
    }

    /// Tests the `get_user` method when a user is not found.
    ///
    /// This test verifies that `get_user` returns an error when the
    /// repository does not find a user with the given ID.
    #[test]
    fn test_get_user_not_found() {
        let mut mock_repo = MockUserRepository::new();

        // Set up expectations:
        // We expect `find_by_id` to be called once with id 999 and return None
        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");
    }

    /// Tests the `update_user_email` method.
    ///
    /// This test verifies that `update_user_email` correctly updates a
    /// user's email and saves the updated user to the repository.
    #[test]
    fn test_update_user_email() {
        let mut mock_repo = MockUserRepository::new();

        // Set up expectations for find_by_id, we expect it to be called once
        // with id 1.
        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, we expect it to be called once with a
        // user with id 1, name "John Doe" and email
        // "newemail@example.com"
        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.

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

/// Represents a user with an ID and a name.
#[derive(Debug, Clone, PartialEq)]
struct User {
    id: u64,
    name: String,
}

/// A trait representing a database that can retrieve and save user data.
trait Database {
    /// Retrieves a user by their ID.
    ///
    /// Returns `Some(User)` if a user with the given ID exists, otherwise
    /// `None`.
    fn get_user(&self, id: u64) -> Option<User>;
    /// Saves a user to the database.
    ///
    /// Returns `true` if the save operation was successful, `false` otherwise.
    fn save_user(&self, user: &User) -> bool;
}

/// A service that interacts with a `Database` to manage user data.
struct UserService<D: Database> {
    /// The database implementation used by this service.
    database: D,
}

/// Implementation of `UserService` that uses a generic `Database`
/// implementation.
impl<D: Database> UserService<D> {
    /// Creates a new `UserService` with the given database.
    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
        }
    }
}

/// A mock implementation of the `Database` trait.
#[faux::create]
struct MockDatabase {}

/// Implementation of the `Database` trait for `MockDatabase`.
#[faux::methods]
impl Database for MockDatabase {
    /// Mock implementation of `get_user`.
    /// This method will be mocked during testing.
    fn get_user(&self, _id: u64) -> Option<User> {
        unimplemented!()
    }

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

/// Test module for `UserService` using `MockDatabase`.
#[cfg(test)]
mod tests {
    use super::*;

    /// Tests the `get_user_name` method of `UserService`.
    #[test]
    fn test_get_user_name() {
        // Create a mock.
        let mut mock_db = MockDatabase::faux();

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

        // Create a service with the mock DB.
        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);
    }

    /// Tests the `update_user_name` method of `UserService`.
    #[test]
    fn test_update_user_name() {
        // Create a mock DB.
        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 always succeed.
        when!(mock_db.save_user).then(|_| true);

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

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