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 在 测试 分类中
每月下载量 4,544 次
在 10 crates 中使用
29KB
302 行
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