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 开发工具
每月740次下载
在 3 个包 中使用
62KB
1.5K SLoC
Mry
一个简单但功能强大的模拟库,用于 结构体、特性 和 函数。
功能
- 一个真正简单易用的 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 fn
的 trait_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