#mocking #testing

mry_macros

为 mry 编写的宏 crate,mry 是一个简单但功能强大的模拟库,支持结构体、特质和函数

30 个版本 (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日

#1701 in 开发工具

Download history 252/week @ 2024-04-19 130/week @ 2024-04-26 180/week @ 2024-05-03 236/week @ 2024-05-10 245/week @ 2024-05-17 522/week @ 2024-05-24 299/week @ 2024-05-31 134/week @ 2024-06-07 231/week @ 2024-06-14 164/week @ 2024-06-21 61/week @ 2024-06-28 118/week @ 2024-07-05 154/week @ 2024-07-12 149/week @ 2024-07-19 560/week @ 2024-07-26 225/week @ 2024-08-02

每月1,124次下载
4 个 crate 中使用(通过 mry

MIT/Apache

83KB
2K SLoC

Mry

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

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

功能

  • 非常简单且易于使用的 API
  • 支持结构体、特质和函数的模拟。
  • 无需在模拟对象和真实对象之间切换。
  • 支持部分模拟。

mockall 的比较

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

Mry 是 cfg-free

在 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. 如果在结构体 Cat 中模拟名为 new 的相关函数,可以使用 Cat::mock_new 创建模式。

Cat::mock_new(mry::Any)

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

mock_hello(mry::Any)

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

步骤 2. 为模式设置预期行为

在模式之后,您可以链接以下任何一个以设置预期行为。

  • 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());

(可选) 步骤 3. 断言模式按预期次数调用

您可以使用 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);

基本用法

模拟结构体

我们需要在结构体定义和 impl 块前添加属性 #[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");
}

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

使用以下格式将相关函数包含到impl块中:#[mry::mry].

#[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")).

使用 trait_variant::makeasync fn (1.75.0或更高版本)

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

#[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 包,则必须在 [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")).

为结构体实现特性

API支持对实现特性的模拟。

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

您可以使用与 cat.mock_into() 相同的方式模拟特性。

使用手动创建的模拟结构体模拟特性

如果特性有泛型或关联类型,我们需要以这种方式进行模拟。

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

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

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

依赖

~0.7–1.1MB
~26K SLoC