Rust mocking库faux的使用:轻量级对象模拟与测试辅助工具

Rust mocking库faux的使用:轻量级对象模拟与测试辅助工具

faux是一个用于从结构体创建mock的库,它允许你在测试中模拟结构体的方法,而不需要复杂化或污染你的生产代码。

快速开始

由于faux大量使用了不安全的Rust特性,因此建议仅在测试中使用。在你的Cargo.toml中将其设置为dev-dependency

[dev-dependencies]
faux = "^0.1"

faux提供两个属性:

  • #[create]: 将结构体转换为可mock的等效结构
  • #[methods]: 将impl块中的方法转换为可mock的等效方法

使用Rust的#[cfg_attr(...)]将这些属性限定为仅测试配置:

#[cfg_attr(test, faux::create)]
pub struct MyStructToMock { /* fields */ }

#[cfg_attr(test, faux::methods)]
impl MyStructToMock { /* methods to mock */ }

示例

以下是内容中提供的示例代码:

mod client {
    #[faux::create]
    pub struct UserClient { /* data of the client */ }

    #[derive(Clone)]
    pub struct User {
        pub name: String
    }

    #[faux::methods]
    impl UserClient {
        pub fn fetch(&self, id: usize) -> User {
            User { name: "".into() }
        }
    }
}

use crate::client::UserClient;

pub struct Service {
    client: UserClient,
}

#[derive(Debug, PartialEq)]
pub struct UserData {
    pub id: usize,
    pub name: String,
}

impl Service {
    fn user_data(&self) -> UserData {
        let id = 3;
        let user = self.client.fetch(id);
        UserData { id, name: user.name }
    }
}

fn main() {
    let mut client = client::UserClient::faux();

    faux::when!(client.fetch(3))
        .then_return(client::User { name: "my user name".into() });

    let subject = Service { client };

    let expected = UserData { id: 3, name: String::from("my user name") };
    assert_eq!(subject.user_data(), expected);
}

完整示例Demo

以下是一个更完整的示例,展示了如何使用faux进行测试:

// 假设这是我们要测试的生产代码模块
mod production {
    #[cfg_attr(test, faux::create)]
    pub struct DatabaseClient {
        connection_string: String,
    }

    #[cfg_attr(test, faux::methods)]
    impl DatabaseClient {
        pub fn new(connection_string: &str) -> Self {
            DatabaseClient {
                connection_string: connection_string.to_string(),
            }
        }

        pub fn query(&self, sql: &str) -> Result<Vec<String>, String> {
            // 实际实现会连接数据库并执行查询
            Ok(vec!["result1".to_string(), "result2".to_string()])
        }

        pub async fn async_query(&self, sql: &str) -> Result<Vec<String>, String> {
            // 异步查询实现
            Ok(vec!["async_result".to_string()])
        }
    }

    pub struct DataService {
        db: DatabaseClient,
    }

    impl DataService {
        pub fn new(db极抱歉,我的回复被截断了。以下是完整的回复:

```rust
    pub struct DataService {
        db: DatabaseClient,
    }

    impl DataService {
        pub fn new(db: DatabaseClient) -> Self {
            DataService { db }
        }

        pub fn get_data(&self) -> Vec<String> {
            self.db.query("SELECT * FROM data").unwrap()
        }

        pub async fn get_async_data(&self) -> Vec<String> {
            self.db.async_query("SELECT * FROM async_data").await.unwrap()
        }
    }
}

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

    #[test]
    fn test_sync_query() {
        // 创建mock客户端
        let mut mock_db = DatabaseClient::faux();
        
        // 设置mock行为
        faux::when!(mock_db.query("SELECT * FROM data"))
            .then_return(Ok(vec!["mock1".to_string(), "mock2".to_string()]));
        
        // 创建测试对象
        let service = DataService::new(mock_db);
        
        // 测试
        let result = service.get_data();
        assert_eq!(result, vec!["mock1", "mock2"]);
    }

    #[tokio::test]
    async fn test_async_query() {
        let mut mock_db = DatabaseClient::faux();
        
        faux::when!(mock_db.async_query("SELECT * FROM async_data"))
            .then_return(Ok(vec!["async_mock".to_string()]));
        
        let service = DataService::new(mock_db);
        let result = service.get_async_data().await;
        assert_eq!(result, vec!["async_mock"]);
    }

    #[test]
    fn test_argument_matchers() {
        let mut mock_db = DatabaseClient::faux();
        
        // 使用any!宏匹配任何参数
        faux::when!(mock_db.query(faux::any!()))
            .then_return(Ok(vec!["any_query".to_string()]));
        
        let service = DataService::new(mock_db);
        let result = service.get_data();
        assert_eq!(result, vec!["any_query"]);
    }
}

特性

faux可以模拟以下方法的返回值或实现:

  • 异步方法
  • 特质方法
  • 泛型结构方法
  • 具有指针自类型的方法(如self: Rc<Self>
  • 外部模块中的方法(但不包括外部crate)

faux还提供了易于使用的参数匹配器。

#[derive(...)]和自动特质的交互

faux mock会自动实现SendSync(如果真实实例也实现了这些特质)。使用#[derive(...)]实现CloneDebugDefault也会按预期工作。其他可派生特质(如EqHash)则不支持,因为它们与数据有关,而faux是关于模拟行为而非数据的。


1 回复

Rust Mocking库faux的使用:轻量级对象模拟与测试辅助工具

介绍

faux是一个轻量级的Rust mocking库,允许开发者创建模拟对象(mock objects)用于测试。与其他Rust mocking解决方案相比,faux具有以下特点:

  • 轻量级且简单易用
  • 不需要复杂的设置或宏
  • 专注于对象行为的模拟
  • 与Rust测试框架无缝集成

完整示例demo

下面是一个完整的示例,展示如何使用faux进行测试:

// 在Cargo.toml中添加依赖:
// [dev-dependencies]
// faux = "0.1"

// 定义需要模拟的结构体和方法
#[cfg(test)]
use faux::create;

#[cfg_attr(test, faux::create)]
pub struct DatabaseClient {
    connection_string: String,
}

#[cfg_attr(test, faux::methods)]
impl DatabaseClient {
    pub fn new(conn: &str) -> Self {
        DatabaseClient {
            connection_string: conn.to_string(),
        }
    }

    pub fn query(&self, sql: &str) -> Result<Vec<String>, String> {
        // 实际实现会连接数据库并执行查询
        Ok(vec!["result1".to_string(), "result2".to_string()])
    }

    pub fn execute(&self, sql: &str) -> Result<u64, String> {
        // 实际实现会执行更新操作并返回影响的行数
        Ok(1)
    }
}

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

    #[test]
    fn test_database_client() {
        // 创建模拟实例
        let mut mock = DatabaseClient::faux();
        
        // 设置模拟行为 - 查询
        when!(mock.query("SELECT * FROM users"))
            .then_return(Ok(vec!["user1".to_string(), "user2".to_string()]));
            
        // 设置模拟行为 - 带通配符的查询
        when!(mock.query(_))
            .then_return(Ok(vec!["default".to_string()]));
            
        // 设置模拟行为 - 执行
        when!(mock.execute("UPDATE users SET name = 'test' WHERE id = 1"))
            .then_return(Ok(1));
            
        // 测试查询
        let result = mock.query("SELECT * FROM users").unwrap();
        assert_eq!(result, vec!["user1", "user2"]);
        
        // 测试通配符查询
        let default_result = mock.query("SELECT * FROM products").unwrap();
        assert_eq!(default_result, vec!["default"]);
        
        // 测试执行
        let affected_rows = mock.execute("UPDATE users SET name = 'test' WHERE id = 1").unwrap();
        assert_eq!(affected_rows, 1);
        
        // 验证方法调用
        faux::verify!(mock.query("SELECT * FROM users")).once();
        faux::verify!(mock.query(_)).times(2); // 总共调用了两次query
        faux::verify!(mock.execute(_)).once();
    }

    #[test]
    fn test_multiple_calls_with_different_results() {
        let mut mock = DatabaseClient::faux();
        
        // 设置多次调用返回不同结果
        when!(mock.query("SELECT * FROM logs"))
            .then_return(Ok(vec!["log1".to_string()]))
            .then_return(Ok(vec!["log2".to_string(), "log3".to_string()]))
            .then_return(Err("connection failed".to_string()));
            
        assert_eq!(mock.query("SELECT * FROM logs").unwrap(), vec!["log1"]);
        assert_eq!(mock.query("SELECT * FROM logs").unwrap(), vec!["log2", "log3"]);
        assert!(mock.query("SELECT * FROM logs").is_err());
    }

    #[test]
    fn test_dynamic_return_based_on_input() {
        let mut mock = DatabaseClient::faux();
        
        // 根据输入参数动态返回结果
        when!(mock.execute).then(|sql| {
            if sql.contains("DELETE") {
                Ok(0)
            } else {
                Ok(1)
            }
        });
        
        assert_eq!(mock.execute("INSERT INTO table VALUES (1)").unwrap(), 1);
        assert_eq!(mock.execute("DELETE FROM table WHERE id = 1").unwrap(), 0);
    }
}

代码说明

  1. 模拟结构体创建

    • 使用#[cfg_attr(test, faux::create)]标记需要模拟的结构体
    • 使用#[cfg_attr(test, faux::methods)]标记需要模拟的实现块
  2. 模拟行为设置

    • when!(mock.method).then_return(value) - 设置固定返回值
    • when!(mock.method(arg)).then_return(value) - 设置特定参数时的返回值
    • when!(mock.method).then(|arg| {...}) - 根据输入参数动态计算返回值
  3. 验证调用

    • faux::verify!(mock.method(arg)).times(n) - 验证方法被调用特定次数
    • faux::verify!(mock.method(arg)).once() - 验证方法被调用一次

使用建议

  1. 将模拟代码放在#[cfg(test)]模块中,避免影响生产代码
  2. 每个测试用例应该创建新的模拟实例,避免状态污染
  3. 优先测试真实行为,只在必要时使用模拟
  4. 结合常规断言和调用验证,确保测试全面性
回到顶部