上次介绍了一个实现依赖反转的容器库(shaku),依赖反转的一大优势就是可以让代码实现真正意义的单元测试。当我们在进行单元测试某个结构体A时,依赖模拟器可以方便快捷的模拟出结构体的依赖B,从而在单元测试时避开测试B中的逻辑,以实现纯单元测试。这次我们就来介绍一个依赖模拟库。
mockall (第一部分)
一个强大的Rust对象模拟库
Mockall 可以模拟几乎所有的结构体和特征。模拟出的对象可在单元测试中作为替代实际的依赖对象使用。
使用方法
Mockall有两种使用方式。最简便的是直接使用 #[automock\]
。这种方式可以模拟大多数的特征,或只有一个impl
代码块的结构体。其他的情况则可以使用 mock!
。
无论使用以上哪种方式,基本的概念大致相同:
- 创建一个模拟的结构体。命名在原有结构体之前加上“Mock”字样。
- 在你的测试中,通过模拟出的结构体自带的
new
或者default
进行实例化。 - 为模拟出的机构体设置期望。每一个期望可以提供所需的参数匹配器、预期的被调用次数、以及在被调用的序列中固定的位置。每一个期望还必需有一个期待的返回值。
- 在测试时,为被测试的代码提供模拟的结构体。这个模拟结构体会返回事先设置好的返回值。任何违反预期的访问都将会引起panic。
入门
use mockall::*;
use mockall::predicate::*;
#[automock]
trait MyTrait {
fn foo(&self, x: u32) -> u32;
}
fn call_with_four(x: &MyTrait) -> u32 {
x.foo(4)
}
let mut mock = MockMyTrait::new();
mock.expect_foo()
.with(predicate::eq(4))
.times(1)
.returning(|x| x + 1);
assert_eq!(5, call_with_four(&mock));
静态返回值
每个期望值都必须具有关联的返回值(尽管启用nightly功能时,如果期望值返回类型实现Default
,则期望值将自动返回其返回类型的默认值)对于返回静态值的方法,宏将生成类似这样
的“期望”结构。设置这种期望值的返回值有两种方法:使用常量(return_const
)或闭包(returning
)。闭包将通过值来获取方法的参数。
#[automock]
trait MyTrait {
fn foo(&self) -> u32;
fn bar(&self, x: u32, y: u32) -> u32;
}
let mut mock = MockMyTrait::new();
mock.expect_foo()
.return_const(42u32);
mock.expect_bar()
.returning(|x, y| x + y);
此外,可以使用return_once
方法返回非克隆常量:
struct NonClone();
#[automock]
trait Foo {
fn foo(&self) -> NonClone;
}
let mut mock = MockFoo::new();
let r = NonClone{};
mock.expect_foo()
.return_once(move || r);
还可以将return_once
用于通过FnOnce
闭包来计算返回值。这对于返回非克隆值并同时触发副作用非常有用。
fn do_something() {}
struct NonClone();
#[automock]
trait Foo {
fn foo(&self) -> NonClone;
}
let mut mock = MockFoo::new();
let r = NonClone{};
mock.expect_foo()
.return_once(move || {
do_something();
r
});
模拟对象始终为Send
。如果您需要使用非Send
的返回类型,可以使用returning_st
或return_once_st
方法。如果需要匹配非Send
参数,则可以使用withf_st
它们采用非Send
对象并添加运行时访问检查。包装的对象将是Send
,但是从多个线程访问它会导致运行时出现panic。
#[automock]
trait Foo {
fn foo(&self, x: Rc<u32>) -> Rc<u32>; // Rc<u32> isn't Send
}
let mut mock = MockFoo::new();
let x = Rc::new(5);
let argument = x.clone();
mock.expect_foo()
.withf_st(move |x| *x == argument)
.returning_st(move |_| Rc::new(42u32));
assert_eq!(42, *mock.foo(x));
匹配参数
可选地,期望可以设置参数匹配器。匹配器将验证预期在被调用时是否参数也符合预期,否则将造成panic。匹配器的本质就是Predicate
这个特征的实现。例如:
#[automock]
trait Foo {
fn foo(&self, x: u32);
}
let mut mock = MockFoo::new();
mock.expect_foo()
.with(eq(42))
.return_const(());
mock.foo(0); // Panics!
有关Mockall内置predicates
功能的列表,请参见predicates
。为方便起见,withf
是设置常用function
的简写predicates
。predicates
函数的参数与方法的参数相同(通过引用传入)。例如:
#[automock]
trait Foo {
fn foo(&self, x: u32, y: u32);
}
let mut mock = MockFoo::new();
mock.expect_foo()
.withf(|x: &u32, y: &u32| x == y)
.return_const(());
mock.foo(2 + 2, 5); // Panics!
匹配多次调用
匹配器还可用于区分同一功能的不同调用。通过这种方式,它们可以为不同的参数提供不同的返回值。原理是在方法调用中,以FIFO顺序评估给定方法上设置的所有期望。第一个匹配的期望会被用到。只有当所有期望都不符合时,Mockall才会panic。例如:
#[automock]
trait Foo {
fn foo(&self, x: u32) -> u32;
}
let mut mock = MockFoo::new();
mock.expect_foo()
.with(eq(5))
.return_const(50u32);
mock.expect_foo()
.with(eq(6))
.return_const(60u32);
一种常见的模式是使用多个期望,以降低特异性。最后的期望值可以提供默认值或后备值,而更早的期望值可以更具体。例如:
#[automock]
trait Foo {
fn open(&self, path: String) -> Option<u32>;
}
let mut mock = MockFoo::new();
mock.expect_open()
.with(eq(String::from("something.txt")))
.returning(|_| Some(5));
mock.expect_open()
.return_const(None);
下次我们将继续从参数匹配的其他功能介绍mockall(未完待续)
评论区
写评论还没有评论