< 返回版块

0x5F3759DF 发表于 2020-05-14 22:10

Tags:mock,unit test,inversion of control

上次介绍了一个实现依赖反转的容器库(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_streturn_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的简写predicatespredicates函数的参数与方法的参数相同(通过引用传入)。例如:

#[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(未完待续)

评论区

写评论

还没有评论

1 共 0 条评论, 1 页