#mocking #traits #structs #real #function #object #partial

mry

一个简单但功能强大的模拟库,支持结构体、特性和函数

28 个版本 (7 个破坏性更新)

0.8.0 2024年5月28日
0.7.1 2024年2月20日
0.6.0 2024年2月13日
0.2.6 2022年11月4日
0.1.7 2021年9月22日

#60 in 开发工具

Download history 119/week @ 2024-04-27 205/week @ 2024-05-04 240/week @ 2024-05-11 327/week @ 2024-05-18 488/week @ 2024-05-25 219/week @ 2024-06-01 158/week @ 2024-06-08 240/week @ 2024-06-15 117/week @ 2024-06-22 78/week @ 2024-06-29 148/week @ 2024-07-06 138/week @ 2024-07-13 168/week @ 2024-07-20 228/week @ 2024-07-27 191/week @ 2024-08-03 135/week @ 2024-08-10

每月740次下载
3 个包 中使用

MIT/Apache

62KB
1.5K SLoC

Mry

GitHub MIT/Apache 2.0 Crates.io docs.rs GitHub Repo stars

一个简单但功能强大的模拟库,用于 结构体特性函数

功能

  • 一个真正简单易用的 API
  • 支持结构体、特性和函数的模拟。
  • 无需在模拟对象和真实对象之间切换。
  • 支持部分模拟。

mockall 比较

Mry 的明显区别在于 API 简单且小巧,而且因为它仍在开发中,您会发现一些尚不支持的行为。此外,基于最小惊讶原则,mry 以最简单的方式解决了 mockall 的几个问题。

Mry 无 cfg

在 mockall 中,使用 #[double] 来切换真实和模拟的结构体。问题是 #[double] 使得模拟结构体适用于所有测试用例,因此当某些测试用例需要真实结构体时,尤其是测试结构体本身时,会变得很复杂。

在 mry 中,不需要 #[double] 或复杂的 use 策略。

Mry 不会引起数据竞争

在 mockall 中,您需要与静态函数和方法的模拟进行手动同步。问题是,如果您忘记加锁,结果将是不可预测的,并且难以调试。

在 mry 中,有一个管理的同步机制,如果您忘记它,您会得到一个错误,告诉您需要它。

示例

#[mry::mry]
struct Cat {
    name: String,
}

#[mry::mry]
impl Cat {
    fn meow(&self, count: usize) -> String {
        format!("{}: {}", self.name, "meow".repeat(count))
    }
}

#[test]
fn meow_returns() {
    let mut cat = mry::new!(Cat { name: "Tama".into() });

    let mock_meow = cat.mock_meow(mry::Any).returns("Called".to_string());

    assert_eq!(cat.meow(2), "Called".to_string());

    mock_meow.assert_called(1);
}

如何模拟方法或函数

步骤1. 创建方法或函数的模式

a. 如果你有一个模拟对象 cat:你可以通过调用 cat.mock_meow 并为每个参数提供一个匹配器来为名为 meow 的方法创建一个模式。

// If you mock a struct called `Cat`
let mut cat = mry::new!(Cat { name: "Tama".into() });
// If you mock a trait called `Cat`
let mut cat = MockCat::default();

cat.mock_meow(mry::Any) // Any value can be matched
cat.mock_meow(3) // matched with 3

b. 如果你模拟一个名为 new 的关联函数,该函数属于结构体 Cat,你可以使用 Cat::mock_new 来创建一个模式。

Cat::mock_new(mry::Any)

c. 如果你模拟一个名为 hello 的函数,你可以使用 mock_hello 来创建一个模式。

mock_hello(mry::Any)

[!NOTE] 你可以为同一个方法或函数创建多个模式,并且它们会按照创建的顺序进行匹配。

第二步。为模式设置预期行为

在模式之后,你可以链接着以下之一来设置预期行为。

  • returns(value) - 总是返回一个值。该值必须实现 Clone 以便多次返回。
  • returns_once(value) - 只返回一次值。无需实现 Clone
  • returns_with(closure) - 通过一个接收参数的闭包返回一个动态值。输出不需要实现 Clone
  • calls_real_impl() - 调用方法或函数的真实实现。用于部分模拟。
cat.mock_meow(3).returns("Called with 3".into());
Cat::mock_new(mry::Any).returns(cat);
mock_hello(mry::Any).returns("World".into());

(可选) 第三步。断言模式按预期次数调用

你可以调用 assert_called 来断言模式按预期次数调用。

cat.mock_meow(3).returns("Returns this string when called with 3".into());

assert_eq!(cat.meow(3), "Returns this string when called with 3".to_string());

cat.mock_meow(3).assert_called(1);

此外,你还可以对特定的模式进行计数。

cat.mock_meow(mry::Any).returns("Called".into());

assert_eq!(cat.meow(1), "Called".to_string());
assert_eq!(cat.meow(2), "Called".to_string());
assert_eq!(cat.meow(3), "Called".to_string());

cat.mock_meow(mry::Any)..assert_called(3);
// specific pattern
cat.mock_meow(2).assert_called(1);

如果你想断言与行为设置相同的模式,你可以绑定设置的结果并对其调用 assert_called

let mock_meow = cat.mock_meow(3).returns("Called".to_string());

assert_eq!(cat.meow(3), "Called".to_string());

mock_meow.assert_called(1);

基本用法

模拟结构体

我们需要在结构体定义和实现块前面添加一个属性 #[mry::mry] 来模拟它们。

#[mry::mry] // This
struct Cat {
    name: &'static str,
}

#[mry::mry] // And this
impl Cat {
    fn meow(&self, count: usize) -> String {
        format!("{}: {}", self.name, "meow".repeat(count))
    }
}

#[mry::mry] 向你的结构体添加了一个可见但虚幻的字段 mry,因此你的结构体必须通过以下方式构造。

// An easy way
mry::new!(Cat { name: "Tama" })

// is equivalent to:
Cat {
    name: "Tama",
    mry: Default::default(),
};

// If you derive or impl Default trait.
Cat::default();
// or
Cat { name: "Tama", ..Default::default() };

[!IMPORTANT] 在发布构建时,你的结构体的 mry 字段将为零大小,并且 mock_* 函数将不可用。

部分模拟

你可以通过使用 calls_real_impl() 来进行部分模拟。

#[mry::mry]
impl Cat {
    fn meow(&self, count: usize) -> String {
        self.meow_single().repeat(count)
    }

    fn meow_single(&self) -> String {
        "meow".into()
    }
}

#[test]
fn partial_mock() {
    let mut cat: Cat = Cat {
        name: "Tama".into(),
        ..Default::default()
    };

    cat.mock_meow_single().returns("hello".to_string());

    cat.mock_meow(mry::Any).calls_real_impl();

    // not "meowmeow"
    assert_eq!(cat.meow(2), "hellohello".to_string());
}

模拟特质

只需在特质定义中添加 #[mry::mry]

#[mry::mry]
pub trait Cat {
    fn meow(&self, count: usize) -> String;
}

现在我们可以使用 MockCat 作为模拟对象。

// You can construct it by Default trait
let mut cat = MockCat::default();

// API's are the same as the struct mocks.
cat.mock_meow(2).returns("Called with 2".into());

assert_eq!(cat.meow(2), "Called with 2".to_string());

模拟函数

#[mry::mry] 添加到函数定义中。

#[mry::mry]
fn hello(count: usize) -> String {
    "hello".repeat(count)
}

我们需要使用 #[mry::lock(hello)] 来锁定该函数,因为在模拟静态函数时使用了全局状态。

#[test]
#[mry::lock(hello)] // This is required!
fn function_keeps_original_function() {
    // Usage is the same as the struct mocks.
    mock_hello(Any).calls_real_impl();

    assert_eq!(hello(3), "hellohellohello");
}

模拟相关函数(静态函数)

使用 #[mry::mry] 将相关函数包含到 impl 块中。

#[mry::mry]
struct Cat {}

#[mry::mry]
impl Cat {
    fn meow(count: usize) -> String {
        "meow".repeat(count)
    }
}

在上述模拟函数中,我们也需要出于同样的原因来获取锁。

#[test]
#[mry::lock(Cat::meow)] // This is required!
fn meow_returns() {
    // Usage is the same as the struct mocks.
    Cat::mock_meow(Any).returns("Called".to_string());

    assert_eq!(Cat::meow(2), "Called".to_string());
}

要同时锁定多个静态函数,请以逗号分隔的格式列出函数:#[mry::lock(function_a, function_b, function_c)]。这种方法在锁定前对函数进行排序,从而自动防止死锁。

高级用法

特质中的 async fn(1.75.0 或更高版本)

#[mry::mry] 添加到特质定义中。

#[mry::mry]
pub trait Cat {
    async fn meow(&self, count: usize) -> String;
}

您可以将 cat.mock_meow().returns("Called".to_string()) 与同步方法一样使用。

使用 async fntrait_variant::make 属性(1.75.0 或更高版本)

如果您使用 trait_variant::make 属性,必须在 #[trait_variant::make(Cat: Send)] 下放置 #[mry::mry]

#[trait_variant::make(Cat: Send)]
#[mry::mry]
pub trait LocalCat {
    async fn meow(&self) -> String;
}

let mut cat = MockLocalCat::default();
cat.mock_meow().returns("Called".to_string());

let mut cat = MockCat::default();
cat.mock_meow().returns("Called".to_string());

async_trait

如果您使用 async_trait crate,必须在 #[mry::mry] 下放置 #[async_trait]

#[mry::mry]
#[async_trait::async_trait]
pub trait Cat {
    async fn meow(&self, count: usize) -> String;
}

您可以将 cat.mock_meow().returns("Called".to_string()) 与同步方法一样使用。

为结构实现特质

在相同的 API 中支持 impl 特质的模拟。

#[mry::mry]
impl Into<&'static str> for Cat {
    fn into(self) -> &'static str {
        self.name
    }
}

您还可以像 cat.mock_into() 一样使用 cat.mock_meow()

模拟泛型或关联类型的特质

我们还可以通过手动创建模拟结构来模拟特质。如果特质有泛型或关联类型,我们现在需要使用这种方式。

#[mry::mry]
#[derive(Default)]
struct MockIterator {}

#[mry::mry]
impl Iterator for MockIterator {
    type Item = u8;

    fn next(&mut self) -> Option<Self::Item> {
        todo!()
    }
}

依赖

~1–6.5MB
~37K SLoC