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 开发工具
每月1,124次下载
在 4 个 crate 中使用(通过 mry)
83KB
2K SLoC
Mry
一个简单但功能强大的模拟库,用于 结构体、特质 和 函数。
功能
- 非常简单且易于使用的 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::make
与 async 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