Rust Mock 工具
Mock(模拟)是测试中不可或缺的工具,用来替代复杂或不可控的依赖,比如数据库、网络服务等,帮助我们写出高质量、健壮的测试代码。Rust 社区中,mockall
和 mockito
是两款主流且强大的 Mock 库,分别针对 trait 依赖和 HTTP 请求。
目录
-
Mock 基础理解与意义
-
mockall
:Rust Trait Mock 实战- 2.1 安装与基本用法
- 2.2 参数匹配器技巧
- 2.3 调用次数与顺序控制
- 2.4 返回值高级控制
- 2.5 异步函数 Mock
- 2.6 手写 Mock vs 自动生成 Mock
- 2.7 常见坑与调试技巧
-
mockito
:HTTP 请求 Mock 实战- 3.1 安装与基础用法
- 3.2 匹配请求头、请求体及路径参数
- 3.3 模拟网络延迟与错误
- 3.4 多个 Mock 交互与验证调用顺序
1. Mock 基础理解与意义
-
为什么要用 Mock?
测试真实依赖(数据库、网络)通常会慢、不稳定,且不易覆盖异常路径。Mock 允许我们模拟依赖行为,快速且精准地测试业务逻辑。
-
Mock 和 Stub 的区别?
Stub 只是返回预设结果的替身,Mock 除了返回结果,还能验证调用是否发生、参数是否正确、调用顺序等。
-
Rust Mock 工具特点
Rust 静态类型强且拥有 trait 机制,Mock 库多基于 trait 来实现替身行为,网络请求 Mock 则独立存在。
2. mockall:Trait Mock 实战
mockall
通过 #[automock]
宏自动生成 Mock 结构体,支持丰富的匹配和返回值设置。
2.1 安装与基础用法
Cargo.toml
:
toml
[dev-dependencies]
mockall = "0.11"
示例:
rust
use mockall::{automock, predicate::*};
#[automock]
pub trait UserRepo {
fn find_user(&self, user_id: u32) -> Option<String>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_mock() {
let mut mock = MockUserRepo::new();
mock.expect_find_user()
.with(predicate::eq(42))
.times(1)
.returning(|_| Some("Alice".to_string()));
assert_eq!(mock.find_user(42), Some("Alice".to_string()));
}
}
2.2 参数匹配器技巧
mockall 提供丰富参数匹配器 predicate
,常用:
predicate::eq(value)
- 等于predicate::always()
- 任意参数都匹配predicate::lt(value)
,gt()
,le()
,ge()
- 大小比较- 自定义闭包匹配
predicate::function(|arg| ...)
示例:
rust
mock.expect_find_user()
.with(predicate::function(|id| *id > 0))
.returning(|id| Some(format!("User{}", id)));
2.3 调用次数与顺序控制
.times(n)
指定调用次数.once()
等价.times(1)
.never()
期望不被调用.in_sequence(&mut seq)
用于控制调用顺序(配合Sequence
类型)
示例:
rust
use mockall::Sequence;
let mut seq = Sequence::new();
mock.expect_find_user()
.with(predicate::eq(1))
.times(1)
.in_sequence(&mut seq)
.returning(|_| Some("First".to_string()));
mock.expect_find_user()
.with(predicate::eq(2))
.times(1)
.in_sequence(&mut seq)
.returning(|_| Some("Second".to_string()));
这样会验证先调用参数1再调用参数2。
2.4 返回值高级控制
.returning()
支持闭包返回不同结果.return_once()
返回一次后失效,之后走.returning()
默认行为.panic()
用于模拟异常
示例:
rust
mock.expect_find_user()
.with(predicate::eq(1))
.return_once(|_| Some("One".to_string()))
.return_once(|_| Some("Two".to_string()))
.returning(|_| None);
2.5 异步函数 Mock
支持 async trait:
rust
use std::future::Future;
use std::pin::Pin;
#[automock]
trait AsyncTrait {
fn fetch(&self) -> Pin<Box<dyn Future<Output=String> + Send>>;
}
#[tokio::test]
async fn async_mock_test() {
let mut mock = MockAsyncTrait::new();
mock.expect_fetch()
.returning(|| Box::pin(async { "async result".to_string() }));
let res = mock.fetch().await;
assert_eq!(res, "async result");
}
2.6 手写 Mock vs 自动生成 Mock
- 自动生成方便快捷,推荐用
#[automock]
- 手写 Mock 更灵活,适合复杂场景或无 trait 的情况
手写示例:
rust
struct ManualMock;
impl UserRepo for ManualMock {
fn find_user(&self, user_id: u32) -> Option<String> {
if user_id == 42 { Some("Manual".to_string()) } else { None }
}
}
2.7 常见坑与调试技巧
times()
配置不当导致测试失败:检查调用次数是否和预期匹配- 调用顺序不对 :用
Sequence
管理调用顺序 - 参数匹配失败 :用
.with(predicate::always())
暂时放宽匹配,确认调用 - 调试信息 :Rust 测试输出可用
println!
配合 Mock 函数打印确认执行流程
3. mockito:HTTP 请求 Mock
3.1 安装
toml
[dev-dependencies]
mockito = "0.31"
3.2 简单示例
rust
#[cfg(test)]
mod tests {
use mockito::mock;
#[test]
fn http_mock() {
let _m = mock("GET", "/api/user/42")
.with_status(200)
.with_body(r#"{"name":"Alice"}"#)
.create();
let url = format!("{}/api/user/42", mockito::server_url());
let resp = reqwest::blocking::get(&url).unwrap();
let body = resp.text().unwrap();
assert_eq!(body, r#"{"name":"Alice"}"#);
}
}
3.3 复杂匹配
- 匹配请求头
rust
mock("POST", "/submit")
.match_header("content-type", "application/json")
.with_status(201)
.create();
- 匹配请求体
rust
use mockito::Matcher;
mock("POST", "/submit")
.match_body(Matcher::JsonString(r#"{"key":"value"}"#.to_string()))
.with_status(201)
.create();
3.4 模拟延时、失败
rust
mock("GET", "/slow")
.with_status(200)
.with_body("delayed")
.with_delay(std::time::Duration::from_secs(3))
.create();
模拟网络错误:
rust
mock("GET", "/fail")
.with_status(500)
.with_body("Internal Server Error")
.create();
3.5 多个 Mock 与调用顺序
rust
let _m1 = mock("GET", "/step1")
.with_status(200)
.with_body("first")
.create();
let _m2 = mock("GET", "/step2")
.with_status(200)
.with_body("second")
.create();
请求 /step1
和 /step2
会匹配对应 Mock。