You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 14:03:21