29 个版本

0.8.0 2022年12月16日
0.7.11 2021年4月2日
0.7.10 2021年1月20日
0.7.9 2020年12月22日
0.1.1 2017年9月5日

#98测试 分类中

Download history 2991/week @ 2024-03-14 2528/week @ 2024-03-21 2497/week @ 2024-03-28 4604/week @ 2024-04-04 2872/week @ 2024-04-11 2699/week @ 2024-04-18 1389/week @ 2024-04-25 1540/week @ 2024-05-02 1598/week @ 2024-05-09 896/week @ 2024-05-16 1187/week @ 2024-05-23 1170/week @ 2024-05-30 1555/week @ 2024-06-06 844/week @ 2024-06-13 951/week @ 2024-06-20 993/week @ 2024-06-27

每月下载量 4,544 次
10 crates 中使用

MIT 许可证

29KB
302

logo

Rust 的模拟框架(目前仅限 nightly)。更多详情请参考 文档

#[mockable]
mod hello_world {
    pub fn world() -> &'static str {
        "world"
    }

    pub fn hello_world() -> String {
        format!("Hello {}!", world())
    }
}

#[test]
fn mock_test() {
    hello_world::world.mock_safe(|| MockResult::Return("mocking"));

    assert_eq!("Hello mocking!", hello_world::hello_world());
}

lib.rs:

Rust 的模拟框架(目前仅限 nightly)

#[mockable]
mod hello_world {
    pub fn world() -> &'static str {
        "world"
    }

    pub fn hello_world() -> String {
        format!("Hello {}!", world())
    }
}

#[test]
fn mock_test() {
    hello_world::world.mock_safe(|| MockResult::Return("mocking"));

    assert_eq!("Hello mocking!", hello_world::hello_world());
}

简介

这是一份用户指南,展示了如何为 Rust 项目设置使用模拟进行测试。

强烈建议仅在测试运行中使用模拟,**绝不要在发布构建中使用模拟**!Mocktopus 并非为了高性能设计,可能会降低代码执行速度。

注意:本指南仅展示测试构建中模拟的设置。

先决条件

将 Mocktopus 作为 dev-dependency 添加到项目的 Cargo.toml

[dev-dependencies]
mocktopus = "0.7.0"

在 crate 根目录中启用过程宏

#![cfg_attr(test, feature(proc_macro_hygiene))]

导入 Mocktopus(对于 Rust 2018 可跳过)

#[cfg(test)]
extern crate mocktopus;

使函数可模拟

为了使函数可模拟,它们必须使用提供的程序宏进行注解。有关所有可能性和规则的更多信息,请参阅 文档

将宏导入命名空间中以使用它们

#[cfg(test)]
use mocktopus::macros::*;

注解可模拟的代码,如独立函数或 impl 块

#[mockable]
fn my_fn() {}

#[mockable]
impl Struct {
    fn my_method() {}
}

在 impl 块中注解单个函数是不合法的

impl Struct {
    #[mockable] // WRONG, will break Mocktopus
    fn my_method() {}
}

可以注解模块,这将使其所有可能的可模拟内容都变得可模拟

#[cfg_attr(test, mockable)]
mod my_module {
    fn my_fn() {}
}

对于单独文件中的模块,此方法不适用

#[cfg_attr(test, mockable)] // WRONG, has no effect
mod my_module;

模拟

在测试模块中导入模拟工具

#[cfg(test)]
mod tests {
    use mocktopus::mocking::*;

其中之一导入了 trait Mockable。它为所有函数实现,并提供设置模拟的接口

#[test]
fn my_test() {
    my_function.mock_safe(|| MockResult::Return(1));

    assert_eq!(1, my_function());
}

也可以模拟结构体的方法,无论是从自己的 impl、traits 或 trait 默认中实现

// Mocking method
MyStruct::my_method.mock_safe(|| MockResult::Return(1));
// Mocking trait method
MyStruct::my_trait_method.mock_safe(|| MockResult::Return(2));
// Mocking default trait method
MyStruct::my_trait_default_method.mock_safe(|| MockResult::Return(3));

使用 mock_safe 进行模拟是最简单的,但 Mockable trait 提供了更多功能,请参阅 文档

模拟范围

每个模拟只在其被设置的线程中工作。所有 Rust 测试运行都是在独立的线程中执行的,因此模拟之间不会泄漏

#[cfg_attr(test, mockable)]
fn common_fn() -> u32 {
    0
}

#[test]
fn common_fn_test_1() {
    assert_eq!(0, common_fn());

    common_fn.mock_safe(|| MockResult::Return(1));

    assert_eq!(1, common_fn());
}

#[test]
fn common_fn_test_2() {
    assert_eq!(0, common_fn());

    common_fn.mock_safe(|| MockResult::Return(2));

    assert_eq!(2, common_fn());
}

模拟闭包

mock_safe 只有一个参数:一个闭包,它接收与被模拟函数相同的输入并返回一个 MockResult。每当被模拟函数被调用时,其输入都会传递给闭包

#[cfg_attr(test, mockable)]
fn my_function_1(_: u32) {
    return
}

#[test]
fn my_function_1_test() {
    my_function_1.mock_safe(|x| {
        assert_eq!(2, x);
        MockResult::Return(())
    });

    my_function_1(2); // Passes
    my_function_1(3); // Panics
}

如果闭包返回 MockResult::Return,则模拟函数不会执行。它将立即返回一个值,该值在 MockResult::Return 中传递

#[cfg_attr(test, mockable)]
fn my_function_2() -> u32 {
    unreachable!()
}

#[test]
fn my_function_2_test() {
    my_function_2.mock_safe(|| MockResult::Return(3));

    assert_eq!(3, my_function_2());
}

如果闭包返回 MockResult::Continue,则模拟函数将正常运行,但参数已改变。新参数作为 MockResult::Continue 中的元组从闭包返回

#[cfg_attr(test, mockable)]
fn my_function_3(x: u32, y: u32) -> u32 {
    x + y
}

#[test]
fn my_function_3_test() {
    my_function_3.mock_safe(|x, y| MockResult::Continue((x, y + 1)));

    assert_eq!(3, my_function_3(1, 1));
}

模拟泛型

在模拟泛型函数时,必须定义所有泛型,并且只会影响这个变体

#[cfg_attr(test, mockable)]
fn generic_fn<T: Display>(t: T) -> String {
    t.to_string()
}

#[test]
fn generic_fn_test() {
    generic_fn::<u32>.mock_safe(|_| MockResult::Return("mocked".to_string()));

    assert_eq!("1", generic_fn(1i32));
    assert_eq!("mocked", generic_fn(1u32));
}

唯一的例外是生命周期,它们将被忽略

#[cfg_attr(test, mockable)]
fn lifetime_generic_fn<'a>(string: &'a String) -> &'a str {
    string.as_ref()
}

#[test]
fn lifetime_generic_fn_test() {
    lifetime_generic_fn.mock_safe(|_| MockResult::Return("mocked"));

    assert_eq!("mocked", lifetime_generic_fn(&"not mocked".to_string()));
}

相同的规则适用于方法和结构

struct GenericStruct<'a, T: Display + 'a>(&'a T);

#[cfg_attr(test, mockable)]
impl<'a, T: Display + 'a> GenericStruct<'a, T> {
    fn to_string(&self) -> String {
        self.0.to_string()
    }
}

static VALUE: u32 = 1;

#[test]
fn lifetime_generic_fn_test() {
    GenericStruct::<u32>::to_string.mock_safe(|_| MockResult::Return("mocked".to_string()));

    assert_eq!("mocked", GenericStruct(&VALUE).to_string());
    assert_eq!("mocked", GenericStruct(&2u32).to_string());
    assert_eq!("2", GenericStruct(&2i32).to_string());
}

模拟异步

模拟异步函数几乎与非异步函数完全相同

#[cfg_attr(test, mockable)]
async fn sleep(ms: u64) {
    tokio::time::delay_for(std::time::Duration::from_millis(ms)).await;
}

#[tokio::test]
async fn sleep_test() {
    sleep.mock_safe(|_| MockResult::Return(Box::pin(async move { () })));

    sleep(10000).await;
}

模拟技巧

返回模拟内部创建的值的引用

#[mockable]
fn my_fn(my_string: &String) -> &String {
    my_string
}

#[test]
fn my_fn_test() {
    my_fn.mock_safe(|_| MockResult::Return(Box::leak(Box::new("mocked".to_string()))));

    assert_eq!("mocked", my_fn(&"not mocked 1"));
    assert_eq!("mocked", my_fn(&"not mocked 2"));
}

这个技巧是将引用值存储在 Box::new 中,然后使用 Box::leak 防止其释放。这使得结构永远存活,并返回对其的 &'static mut 引用。值直到进程终止才释放,因此这仅适用于测试中使用,并且结构不占用大量资源,如大量内存、打开的文件句柄、套接字等。

返回模拟外部创建的值

#[mockable]
fn my_fn() -> String {
    "not mocked".to_string()
}

#[test]
fn my_fn_test() {
    mock = Some("mocked".to_string());
    my_fn.mock_safe(move || MockResult::Return(mock.unwrap()));

    assert_eq!("mocked", my_fn());
    // assert_eq!("mocked", my_fn()); // WILL PANIC!
}

这使得函数在第一次调用时返回预定义的值,在第二次调用时崩溃。它可以返回 MockResult::Continue 而不是崩溃来模拟第一次调用。

如果模拟应该在不同调用中返回不同的值,则可以将返回值存储在向量中

#[test]
fn my_fn_test() {
    mut mock = vec!["mocked 1".to_string(), "mocked 2".to_string()];
    my_fn.mock_safe(move || MockResult::Return(mock.remove(0)));

    assert_eq!("mocked 1", my_fn());
    assert_eq!("mocked 2", my_fn());
    // assert_eq!("mocked 3", my_fn()); // WILL PANIC!
}

向量可以存储 MockResult 以进行更复杂的模拟。

依赖关系

~1.5MB
~35K SLoC