4 个版本
使用旧的 Rust 2015
0.1.3 | 2017 年 12 月 27 日 |
---|---|
0.1.2 | 2017 年 8 月 20 日 |
0.1.1 | 2017 年 8 月 18 日 |
0.1.0 | 2017 年 8 月 13 日 |
#31 in #tdd
27 每月下载
用于 galvanic-test
96KB
1.5K SLoC
Galvanic-mock:泛型特质的行为驱动模拟
此包提供用于模拟特质行为的过程宏(#[mockable]
,#[use_mocks]
)。
- 根据模式定义模拟对象的给定行为
- 为与模拟的交互定义预期
- 同时模拟多个特质
- 模拟泛型特质和具有关联类型的特质
- 模拟泛型特质方法
- 将 #[derive(..)] 和其他属性应用于模拟
- 在行为中使用 galvanic-assert 匹配器,如
eq
、lt
等 - 与 galvanic-test 和 galvanic-assert 集成
- 与您最喜欢的测试框架一起使用
该包是 galvanic 的组成部分——一个完整的 Rust 测试框架。该框架分为三部分,因此您可以仅使用您需要的部分。
关于 galvanic-mock 的简要介绍
在一个设计良好的软件项目中,具有松散耦合和依赖注入,模拟可以简化软件测试的开发。它模仿了一个 真实 对象的行为,即生产代码中存在的一个对象,以解耦测试软件组件与其他系统部分。
电化学模拟 是一个用于Rust中特质的驱动模拟库。它允许用户创建一个模拟对象,模拟一个或多个特质的行为,根据给定的交互模式进行模拟。一个特质的方法的模式由其参数的布尔匹配器(可以是每个参数或一次性对所有参数进行匹配)、计算返回值的常量或函数,以及模式有效的重复次数组成。
// this crate requires a nightly version of rust
#![feature(proc_macro)]
extern crate galvanic_mock;
use galvanic_mock::{mockable, use_mocks};
#[mockable]
trait MyTrait {
fn foo(&self, x: i32, y: i32) -> i32;
}
#[test]
#[use_mocks]
fn simple_usage_of_mocks() {
// create a new object implementing `MyTrait`
let mock = new_mock!(MyTrait);
let some_calculation = 1 + 2*3;
// define behaviours how your mocks should react given some input
given! {
// make val available to your behaviours (must implement `Clone`), the type is **not** optional!
bind val: i32 = some_calculation;
// define input matchers per argument and return a constant value whenever it matches
<mock as MyTrait>::foo(|&x| x < 7, |&y| y % 2 == 0) then_return 12 always;
// or define a single input matcher for all arguments and return the result of a function
<mock as MyTrait>::foo |&(x, y)| x < y then_return_from |&(x,y)| y - x always;
// with the `bound` variable you can access variable declared with `bind VAR: TYPE = VALUE;`
<mock as MyTrait>::foo(|_| true) then_return_from |&(x,_)| x*bound.val always;
}
// only matches the last behaviour
assert_eq!(mock.foo(12, 4), 84);
// would match the first and the second behaviour, but the first matching behaviour is always used
assert_eq!(mock.foo(3, 4), 12);
// matches the second behaviour
assert_eq!(mock.foo(12, 14), 2);
}
除了模拟对象的行为外,还可以对对象交互的预期进行声明。预期行为的模式与给定行为的模式类似。以下示例说明了这些概念。
#![feature(proc_macro)]
extern crate galvanic_mock;
use galvanic_mock::{mockable, use_mocks};
// matchers from galvanic_assert can be used as argument matchers
extern crate galvanic_assert;
use galvanic_assert::matchers::{gt, leq, any_value};
#[mockable]
trait MyTrait {
fn foo(&self, x: i32, y: i32) -> i32;
}
#[mockable]
trait MyOtherTrait<T> {
fn bar(&self, x: T) -> T;
}
#[test]
#[use_mocks]
fn simple_use_of_mocks() {
// to mock multiple traits just separate them with a colon
// specify all types for generic traits as you would specify a type
let mock = new_mock!(MyTrait, MyOtherTrait<String>);
// expectations are matched top-down, but once the specified match count is reached it won't match again
given! {
// instead of repeating the trait over and over, you can open a block
<mock as MyTrait>::{
// this behaviour will match only twice
foo any_value() then_return_from |_| 7 times 2;
foo(gt(12), any_value()) then_return 2 always;
};
// for generic traits all generic types and associated types need to be given
<mock as MyOtherTrait<String>>::bar(|x| x == "hugo") then_return "got hugo".to_string() always;
}
// expectations are matched top-down, but will never be exhausted
expect_interactions! {
// `times` expects an exact number of matching interactions
<mock as MyOtherTrait<String>>::bar any_value() times 1;
// besides `times`, also `at_least`, `at_most`, `between`, and `never` are supported
// all limits are inclusive
<mock as MyTrait>::foo(any_value(), leq(2)) between 2,5;
}
assert_eq!(mock.foo(15, 1), 7);
assert_eq!(mock.bar("hugo".to_string()), "got hugo".to_string());
assert_eq!(mock.foo(15, 2), 7);
assert_eq!(mock.foo(15, 5), 2);
// the expected interactions are verified when the mock is dropped or when `mock.verify()` is called
}
文档
在阅读文档之前,请确保阅读了介绍中的示例,因为文档将使用它们作为解释的基础。
要使用模拟库,请确保使用Rust的 nightly 版本,因为该crate需要 proc_macro
功能。最好将其作为dev依赖项添加到您的 Cargo.toml
中。
[dev-dependencies]
galvanic-mock = "*" # galvanic uses `semver` versioning
在crate的根目录(无论是 main.rs
还是 lib.rs
)中添加以下内容以激活所需的功能并导入宏。
#![feature(proc_macro)]
extern crate galvanic_mock;
// The use statement should be placed where the #[mocakable] and #[use_mocks] attributes
// are actually used, or reimported.
use galvanic_mock::{mockable, use_mocks};
如果我们想在模拟中使用 galvanic-assert
匹配器,则必须启用 galvanic_assert_integration
功能,如下所示。
[dev-dependencies]
galvanic-mock = { version = "*", features = ["galvanic_assert_integration"] }
galvanic-assert = "*" # galvanic-assert uses semver versioning too. To find the version required by `galvanic-mock` check version of the optional dependency in the manifest `Cargo.toml`.
如果启用了集成功能,则必须在 extern crate galvanic_assert
和 extern crate galvanic_mock
之间指定,否则库将无法编译(即使没有使用 galvanic_assert
匹配器)。
使用 #[mockable]
定义可模拟的特质
在特质可以模拟之前,您必须告诉模拟框架其名称、泛型、关联类型和方法。如果特质是您自己的crate的一部分,只需将该 #[mockable]
属性应用于特质定义。
#[mockable]
trait MyTrait {
fn foo(&self, x: i32, y: i32) -> i32;
}
这会将 MyTrait
注册为可模拟的。进一步假定 MyTrait
在crate的顶层定义,或者当模拟时总是按名称导入,例如,使用 use crate::module::MyTrait
。
如果特质在子模块中定义,则应将路径提供给属性。
mod sub {
#[mockable(::sub)]
trait MyTrait {
fn foo(&self, x: i32, y: i32) -> i32;
}
}
无论如何注释特质,这将是以后引用它的唯一方式。没有内置的名称解析,例如,上面的特质必须始终作为 ::sub::MyTrait
使用。模拟特质的用户负责确保特质在提供的路径下对模拟位置可见。因此,建议使用如上例所示的 全局 路径。
模拟 外部 特质
可以通过在属性中的路径前添加 extern
关键字来模拟外部特质。必须重新声明完整的特质定义,尽管宏的展开将省略其定义。
#[mockable(extern some_crate::sub)]
trait MyTrait {
fn foo(&self, x: i32, y: i32) -> i32;
}
修复宏展开顺序问题
和其他宏一样,#[mockable]
也受宏展开顺序的影响。此外,必须在使用之前定义可模拟的特质。如果这是一个内部特质的难题,其定义可以类似于外部特质重新表述。
// this occurance of the trait declaration will be removed
#[mockable(intern ::sub)]
trait MyTrait {
fn foo(&self, x: i32, y: i32) -> i32;
}
// a mock is created somewhere here
...
// the true declaration is encountered later
mod sub {
trait MyTrait {
fn foo(&self, x: i32, y: i32) -> i32;
}
}
使用#[use_mocks]
声明模拟使用
任何需要使用模拟的位置(fn
,mod
)都必须用#[use_mocks]
进行注解。
#[test]
#[use_mocks]
fn some_test {
...
}
如果#[use_mocks]
应用于一个模块,那么模拟类型将在所有子模块和函数之间共享。
#[use_mocks]
mod test_module {
#[test]
fn some_test {
...
}
#[test]
fn some_other_test {
...
}
}
尽管不要将#[use_mocks]
应用于已经具有#[use_mocks]
属性的某些其他项中。
以下宏只能在用#[use_mocks]
注解的位置中使用。
使用new_mock!
创建新的模拟
要创建一个新的模拟对象,请使用new_mock!
宏后跟一个模拟特质的列表。对于泛型特质,指定所有它们的类型参数和相关类型。创建的对象满足声明的特质界限,还可以转换为boxed特质对象。
#[use_mocks]
fn some_test {
let mock = new_mock!(MyTrait, MyOtherTrait<i32, f64, Assoc=String>);
...
}
将为每个模拟对象创建一个新的模拟类型。如果需要将其他属性应用于该类型,请在其类型列表之后提供它们。
#[use_mocks]
fn some_test {
let mock = new_mock!(MyTrait #[derive(Clone)]#[other_attribute]);
...
}
当相同的模拟设置代码在多个测试中共享时,我们可以将模拟创建代码放在单独的工厂函数中,在相应的测试用例中调用它,并进一步修改它(例如,添加特定的行为)。为了能够这样做,我们需要知道创建的模拟类型的名称。到目前为止,这些类型一直是匿名的,名称由new_mock!
命令选择。可以提供显式的模拟类型名称。
#[use_mocks]
mod test_module {
fn create_mock() -> mock::MyMockType {
new_mock!(MyTrait #[some_attribute] for MyMockType)
... // define given/expec behaviours
}
#[test]
fn some_test {
let mock: mock::MyMockType = create_mock();
... // define further test=specific given/expec behaviours
}
}
创建的类型将放置在mock
模块中,该模块对注解了#[use_mocks]
的(子)模块和函数自动可见。
使用given!
块定义行为
创建模拟对象后,可以在其上调用模拟特质的函数。然而,因为它只是一个模拟,所以调用的函数会panic,因为它们不知道该怎么做。首先,需要根据方法参数的条件定义对象的行为。根据行为驱动开发(BDD)的术语,这是通过一个given!
块完成的。它设置了我们要测试的场景的先决条件。
given! {
<mock as MyTrait>::func |&(x, y)| x < y then_return 1 always;
...
}
given!
块由多个具有以下模式的给定语句组成。
given! {
<OBJECT as TRAIT>::METHOD ARGUMENT_MATCHERS THEN REPEAT;
...
}
该语句类似于通用函数调用语法,但增加了额外的组件。
OBJECT
... 我们定义模式的模拟对象TRAIT
...METHOD
所属的模拟特质。引用特质的规则与new_mock!
相同。UFC语法不是可选的,目前必须按照在创建OBJECT
的new_mock!
语句中提供的相同顺序提供泛型/关联类型参数。METHOD
... 属于行为的方法ARGUMENT_MATCHERS
... 方法参数的先决条件,行为必须满足这些条件才能被调用THEN
... 定义在行为被选择后发生的事情,例如,返回一个常量值REPEAT
... 定义行为在变得无效之前可以匹配的次数
当调用一个方法时,会从上到下检查其给定行为的先决条件,并选择第一个匹配的行为。给定的代码块不是全局定义,它表现得像任何其他代码块/语句:如果控制流程从未进入该代码块,则行为不会被添加到模拟对象中。如果代码块被多次进入或达到另一个代码块,则其行为会被追加到当前行为列表中。
由于为模拟对象定义许多行为时编写完整的UFC语法可能会变得繁琐,因此增加了一些语法糖。
given! {
<OBJECT as TRAIT>::{
METHOD ARGUMENT_MATCHERS THEN REPEAT;
METHOD ARGUMENT_MATCHERS THEN REPEAT;
...
};
...
}
这些行为块消除了不必要的重复。请注意,块末尾的分号不是可选的。
此外,目前不支持模拟静态方法!.
参数模式
方法参数的先决条件可以定义为两种形式:按参数和显式。
按参数模式
大多数情况下,按参数模式就足够了,而且被认为更易读。
given! {
<mock as MyTrait>::func(|&x| x == 2, |&y| y < 3.0) then_return 1 always;
}
参数匹配器遵循闭包语法,其参数通过不可变引用传递,并必须返回一个bool
或实现std::convert::Into<bool>
的类型。尽管我们使用闭包语法,但这并不是闭包,这意味着您不能从给定的代码块外部捕获变量。我们将在稍后了解如何将外部的值绑定到内部,使其可用。
如果启用了galvanic_assert_integration
功能,则可以使用来自galvanic-assert
的匹配器而不是闭包语法。有关示例,请参阅介绍。
特殊情况:空模式
为了匹配不带参数的方法,我们必须使用按参数模式,尽管不传递任何模式。我们将下面的形式称为空模式。
given! {
<mock as MyTrait>::func_without_args() then_return 1 always;
}
显式模式
第二种形式将所有参数一次以元组的形式接收。
given! {
<mock as MyTrait>::func |&(x, y)| x < y then_return 1 always;
}
同样,带参数的元组通过引用传递。注意,在分解包含不可复制对象的元组(如Rust中的任何其他模式)时,我们必须使用ref
。观察此形式中func
后面的括号缺失。括号用于区分两种形式。
请注意,显式模式不能用于不带参数的方法。
返回值
在THEN
部分定义行为的选择动作。我们可以使用then_return
返回常量表达式的值。
given! {
<mock as MyTrait>::func ... then_return (1+2).to_string() always;
}
或者,我们根据函数调用中的参数,使用then_return_from
计算一个值。这些参数再次作为curried参数元组的引用传递。请注意,我们使用了闭包语法,但我们无法捕获外部作用域的变量。
given! {
<mock as MyTrait>::func ... then_return_from |&(x,y)| (x + y)*2 always;
}
或者简单地崩溃
given! {
<mock as MyTrait>::func ... then_panic always;
}
重复
行为的最后一个元素是在行为耗尽且将不再匹配之前的匹配重复次数。这可以是always
(如之前所使用的)或者times
后跟一个整数表达式。
let x: i32 = func()
given! {
<mock as MyTrait>::func |&(x, y)| x < y then_return 1 times x+1;
}
与参数匹配器和then表达式相反,times
表达式是在给定块的上下文中评估的。
从外部作用域绑定值
到目前为止,参数匹配器和then表达式不能引用外部上下文。这主要是因为当实际的闭包传递给模拟对象时,与引用的生存期有关的问题。为了解决这些问题,可以在给定的块中绑定外部作用域的值。
let x = 1;
given! {
bind value1: f64 = 12.23;
bind value2: i32 = x*2;
<mock as MyTrait>::func |&(_, y)| y > bound.value2 then_return bound.value1 always;
<mock as MyTrait>::func |&(_, y)| y <= bound.value2 then_return_from |&(x, _)| x*bound.value1 always;
}
绑定语句必须出现在给定语句之前,一般形式如下
given! {
bind VARIABLE: TYPE = EXPRESSION:
...
}
请注意,这里类型不是可选的。所有使用bind
定义的变量都可以稍后通过bound
变量的成员访问。绑定表达式将在进入给定块时评估。这也意味着如果给定块被多次进入,则绑定语句将针对新的行为重新评估。
泛型特例方法的行为
当你尝试模拟以下泛型方法时要小心。
#[mockable]
trait MyTrait {
fn generic_func<T,F>(x: T, y: F) -> T;
}
...
given! {
<mock as MyTrait>::generic_func |&(ref x, ref y)| ... then_return 1 always;
}
无论实际使用什么类型,都会应用这种行为。这意味着除了在类型参数上定义的特质界限之外,你无法使用其他很多内容。我们无法假设,例如,x
始终是i32
,尽管我们可能根据上下文知道这一点。在这种情况下,我们必须自己检测类型或使用unsafe
转换。 这将在未来的版本中改变,并且会更容易/更有用。
静态特质方法的行为
目前不支持,但下一个版本中优先考虑。
期望与expect_interactions!
块交互
除了定义模拟应该如何行动之外,通常还需要知道一些交互,即方法调用,已经与模拟发生了。这可以通过一个类似给定块的expect块来完成。
expect_interactions! {
<mock as MyTrait>::func(|&x| x == 2, |&y| y < 12.23) times 2;
}
同样,该块由多个expect语句组成,其一般形式如下。
expect_interactions! {
<OBJECT as TRAIT>::METHOD ARGUMENT_MATCHERS REPEAT;
...
}
特质块、参数匹配器、绑定和评估顺序与给定块相同。重复表达式支持一些不同的选项。此外,expect行为永远不会耗尽。expect语句仅指定模式的测试顺序,而不是期望交互的顺序。在expect_interactions
块中交互的顺序被认为是任意的。此外,只有第一个匹配的expect表达式将被计算。稍后表达式,其参数匹配器可以用相同的参数满足,将不会进行评估。
目前不支持指定固定顺序。
在模拟对象被丢弃或调用mock.verify()
时,将验证期望。如果验证时期望的交互没有按指定的方式发生,则当前线程将崩溃。如果发生了不匹配任何expect行为的其他交互,则它们不会被视为错误。
重复
expect块中的重复表达式可以是以下之一。
times EXPRESSION
... 表示必须恰好发生EXPRESSION
次匹配。never
... 表示不应遇到该交互(等同于times 0
)。at_least EXPRESSION
... 表示至少发生EXPRESSION
次匹配(包含)。at_most EXPRESSION
... 表示最多发生EXPRESSION
次匹配(包含)。between EXPRESSION1, EXPRESSION2
... 表示在包含范围 [EXPRESSION1
,EXPRESSION2
] 内应发生匹配次数。
Mock 接口
所有 Mock 支持一些用于控制 Mock 的基本方法。
should_verify_on_drop(bool)
... 如果传入false
,则禁用验证,反之亦然。reset_given_behaviours()
... 从 Mock 中移除所有已提供的行为。reset_expected_behaviours()
... 从 Mock 中移除所有期望。are_expected_behaviours_satisfied()
... 如果所有期望当前都已满足,则返回true
,否则返回false
。verify()
... 如果某些期望当前未满足,则引发 panic。
依赖项
~2MB
~44K SLoC