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会自动实现Send
和Sync
(如果真实实例也实现了这些特质)。使用#[derive(...)]
实现Clone
、Debug
和Default
也会按预期工作。其他可派生特质(如Eq
或Hash
)则不支持,因为它们与数据有关,而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);
}
}
代码说明
-
模拟结构体创建:
- 使用
#[cfg_attr(test, faux::create)]
标记需要模拟的结构体 - 使用
#[cfg_attr(test, faux::methods)]
标记需要模拟的实现块
- 使用
-
模拟行为设置:
when!(mock.method).then_return(value)
- 设置固定返回值when!(mock.method(arg)).then_return(value)
- 设置特定参数时的返回值when!(mock.method).then(|arg| {...})
- 根据输入参数动态计算返回值
-
验证调用:
faux::verify!(mock.method(arg)).times(n)
- 验证方法被调用特定次数faux::verify!(mock.method(arg)).once()
- 验证方法被调用一次
使用建议
- 将模拟代码放在
#[cfg(test)]
模块中,避免影响生产代码 - 每个测试用例应该创建新的模拟实例,避免状态污染
- 优先测试真实行为,只在必要时使用模拟
- 结合常规断言和调用验证,确保测试全面性