如何在Rust中Mock外部Crate的函数与结构体?
Rust中Mock外部Crate函数与结构体的可行方案
问题场景
以下代码直接在内部调用reqwest的API,未提供依赖注入入口,需要Mockreqwest逻辑以测试各类场景,且要求无需修改生产代码,排除虚拟服务器类方案(如mockito),尝试过mockall、faux但无法处理外部Crate:
// Libs use reqwest::blocking::{get, ClientBuilder}; // Functions fn get_body_foo() -> String { let client = ClientBuilder::new().build().unwrap(); let res = client.get("https://www.google.com").send().unwrap(); res.text().unwrap() } fn get_body_bar() -> String { let res = get("https://www.google.com").unwrap(); res.text().unwrap() }
可行解决方案
方案1:条件编译+全局Mock钩子(推荐)
核心是将对reqwest的调用封装为一层中间代理,通过条件编译让生产环境直接调用原API,测试环境则使用可动态配置的Mock逻辑,仅需一次封装,后续业务代码无需改动。
步骤1:封装reqwest调用代理
// 新增reqwest_proxy模块,隔离外部依赖调用 #[cfg(not(test))] mod reqwest_proxy { use reqwest::blocking::{Client, ClientBuilder, Response}; // 生产环境直接转发到reqwest原方法 pub fn build_client() -> Client { ClientBuilder::new().build().unwrap() } pub fn get(url: &str) -> Response { reqwest::blocking::get(url).unwrap() } pub fn send(client: &Client, url: &str) -> Response { client.get(url).send().unwrap() } } // 测试环境的Mock实现 #[cfg(test)] mod reqwest_proxy { use std::sync::Mutex; use reqwest::blocking::{Client, Response}; use once_cell::sync::Lazy; // 全局静态钩子,用于动态设置Mock行为 pub static BUILD_CLIENT_HOOK: Lazy<Mutex<Box<dyn Fn() -> Client + Send + Sync>>> = Lazy::new(|| { Mutex::new(Box::new(|| panic!("未初始化Mock"))) }); pub static GET_HOOK: Lazy<Mutex<Box<dyn Fn(&str) -> Response + Send + Sync>>> = Lazy::new(|| { Mutex::new(Box::new(|_| panic!("未初始化Mock"))) }); pub static SEND_HOOK: Lazy<Mutex<Box<dyn Fn(&Client, &str) -> Response + Send + Sync>>> = Lazy::new(|| { Mutex::new(Box::new(|_,_| panic!("未初始化Mock"))) }); // 测试环境调用钩子中的Mock逻辑 pub fn build_client() -> Client { let hook = BUILD_CLIENT_HOOK.lock().unwrap(); hook() } pub fn get(url: &str) -> Response { let hook = GET_HOOK.lock().unwrap(); hook(url) } pub fn send(client: &Client, url: &str) -> Response { let hook = SEND_HOOK.lock().unwrap(); hook(client, url) } } // 修改业务代码调用代理层(仅需一次修改) use reqwest_proxy::{build_client, get, send}; fn get_body_foo() -> String { let client = build_client(); let res = send(&client, "https://www.google.com"); res.text().unwrap() } fn get_body_bar() -> String { let res = get("https://www.google.com"); res.text().unwrap() }
步骤2:编写测试用例
#[cfg(test)] mod tests { use super::*; use reqwest::blocking::Response; use reqwest::StatusCode; // 构造自定义Mock响应 fn mock_response(body: &str) -> Response { Response::builder() .status(StatusCode::OK) .body(body.to_string()) .unwrap() } #[test] fn test_get_body_foo() { // 设置build_client的Mock行为 *reqwest_proxy::BUILD_CLIENT_HOOK.lock().unwrap() = Box::new(|| { reqwest::blocking::ClientBuilder::new().build().unwrap() }); // 设置send的Mock行为 *reqwest_proxy::SEND_HOOK.lock().unwrap() = Box::new(|_, _| { mock_response("Mocked Foo Body") }); assert_eq!(get_body_foo(), "Mocked Foo Body"); } #[test] fn test_get_body_bar() { // 设置get的Mock行为 *reqwest_proxy::GET_HOOK.lock().unwrap() = Box::new(|_| { mock_response("Mocked Bar Body") }); assert_eq!(get_body_bar(), "Mocked Bar Body"); } }
方案2:符号替换(进阶Hack)
利用Rust链接器特性,在测试环境替换外部Crate的函数符号,无需封装代理层,但兼容性差,依赖具体Crate的内部实现:
步骤1:添加依赖
[dev-dependencies] linkme = "0.3"
步骤2:编写测试替换逻辑
#[cfg(test)] use linkme::distributed_slice; // 匹配reqwest内部暴露的函数符号(需对应版本) #[distributed_slice] #[linkme::distributed_slice(reqwest::blocking::GET_FN)] static GET_FN: fn(&str) -> Result<reqwest::blocking::Response, reqwest::Error> = mock_get; // 自定义Mock实现 fn mock_get(_url: &str) -> Result<reqwest::blocking::Response, reqwest::Error> { Ok(Response::builder() .status(StatusCode::OK) .body("Mocked Body") .unwrap()) } #[test] fn test_get_body_bar() { assert_eq!(get_body_bar(), "Mocked Body"); }
方案对比
- 方案1:兼容性强、可维护性高,仅需一次封装,适合绝大多数场景。
- 方案2:属于hack手段,依赖Crate内部实现,版本兼容性差,仅适合简单函数Mock,不推荐生产级项目使用。
内容的提问来源于stack exchange,提问作者Gustavo Saiki
相关产品推荐
相关产品推荐

