12 个版本
使用旧 Rust 2015
0.2.4 | 2019 年 9 月 22 日 |
---|---|
0.2.3 | 2019 年 2 月 3 日 |
0.2.2 | 2018 年 1 月 29 日 |
0.1.6 | 2017 年 12 月 31 日 |
0.1.2 | 2017 年 4 月 15 日 |
#186 in 测试
59 每月下载量
用于 ironoxide
120KB
1K SLoC
Double
功能齐全的 Rust 模拟库,具有丰富的参数匹配器。
Double 允许您模拟 trait
实现,以便您可以跟踪函数调用参数并在测试时设置返回值或覆盖函数。
以下是一个快速示例
#[macro_use]
extern crate double;
// Code under test
trait BalanceSheet {
fn profit(&self, revenue: u32, costs: u32) -> i32;
}
fn double_profit(revenue: u32, costs: u32, balance_sheet: &BalanceSheet) -> i32 {
balance_sheet.profit(revenue, costs) * 2
}
// Test which uses a mock BalanceSheet
mock_trait!(
MockBalanceSheet,
profit(u32, u32) -> i32);
impl BalanceSheet for MockBalanceSheet {
mock_method!(profit(&self, revenue: u32, costs: u32) -> i32);
}
fn test_doubling_a_sheets_profit() {
// GIVEN:
let sheet = MockBalanceSheet::default();
sheet.profit.return_value(250);
// WHEN:
let profit = double_profit(500, 250, &sheet);
// THEN:
// mock returned 250, which was doubled
assert_eq!(500, profit);
// assert that the revenue and costs were correctly passed to the mock
sheet.profit.has_calls_exactly_in_order(vec!((500, 250)));
}
// Executing test
fn main() {
test_doubling_a_sheets_profit();
}
更多示例可在 示例目录 中找到。
定义模拟
模拟 trait
需要两个步骤。一是生成将实现模拟的模拟 struct
,二是生成被模拟 trait
方法的主体。
对于第一步,我们使用 mock_trait
宏。它接受要生成的模拟 struct
的名称和指定所有 trait
方法的列表,包括它们的参数(省略 self
)和返回值(如果方法不返回值,则指定 -> ()
)。
以下是一个示例
trait BalanceSheet {
fn profit(&self, revenue: u32, costs: u32) -> i32;
fn clear(&mut self);
}
mock_trait!(
MockBalanceSheet,
profit(u32, u32) -> i32,
clear() -> ());
在这里,我们生成一个名为 MockBalanceSheet
的 struct
。这个结构体包含所有必要的数据库,用于存储每个方法被调用的次数、它们被调用的参数以及每个方法被调用时应返回的值。这些数据按方法存储,结构体为每个方法都有一个 double::Mock
字段。这就是为什么必须在生成结构体时声明所有 trait
的方法的原因。
对于第二步,我们生成模拟方法的主体。生成的主体包含用于通过 mock_method
将方法参数传递给底层 double::Mock
对象的样板代码。例如:
impl BalanceSheet for MockBalanceSheet {
mock_method!(profit(&self, revenue: u32, costs: u32) -> i32);
mock_method!(clear(&mut self));
}
请注意,可以指定不可变和可变方法。只需将
&self
或&mut self
传递给mock_method
,具体取决于被模拟的特质是否将方法指定为不可变或可变。
完成这两个步骤后,模拟对象就准备就绪了。
使用模拟
带有模拟的测试通常结构如下
- 前提:创建模拟对象并指定它们返回的值
- 当:运行待测试的代码,并将模拟对象传递给它
- 则:断言模拟被调用的次数和预期的参数
例如,假设我们希望测试一些使用 BalanceSheet
生成显示当前利润的 HTML 页面的代码
fn generate_profit_page<T: BalanceSheet>(revenue: u32, costs: u32, sheet: &T) {
let profit_str = sheet.profit(revenue, costs).to_string();
return "<html><body><p>Profit is: $" + profit_str + "</p></body></html>";
}
我们可以使用生成的 MockBalanceSheet
来测试此函数
fn test_balance {
// GIVEN:
// create instance of mock and configure its behaviour (will return 42)
let mock = MockBalanceSheet::default();
mock.profit.return_value(42);
// WHEN:
// run code under test
let page = generate_profit_page(30, 20);
// THEN:
// ensure mock affected output in the right away
assert_eq!("<html><body><p>Profit is: $42</p></body></html>")
// also assert that the mock's profit() method was called _exactly_ once,
// with the arguments 30 (for revenue) and 20 (for costs).
assert_true!(mock.profit.has_calls_exactly(
vec!((30, 20))
));
}
前提:设置模拟行为
模拟可以被配置为返回单个值、一系列值(每个调用一个值)或调用一个函数/闭包。此外,还可以在传递特定参数时使模拟返回特殊值/调用特殊函数。
这些行为通过在模拟对象上调用方法进行配置。这些方法在下面的表中列出。
方法 | 它做什么 |
---|---|
use_fn_for((args),dynFn(...) ->retval) |
在指定的 args 被传递时调用给定的函数并返回它返回的值 |
use_closure_for((args), &dynFn(...) ->retval) |
invoke given closure and return the value it returns when specified ( args) are passed in |
return_value_for((args),val) |
在指定的 args 被传递时返回 val |
use_fn(dynFn(...) ->retval) |
在默认情况下调用给定的函数并返回它返回的值 |
use_closure(&dynFn(...) ->retval) |
在默认情况下调用给定的闭包并返回它返回的值 |
return_values(vec<retval>) |
默认情况下返回给定向量中的值,对于模拟方法的每次调用返回一个值。如果没有更多的值在向量中,则返回由 return_value 指定的默认值 |
return_value(val) |
默认情况下返回 val |
如果没有指定行为,模拟将仅返回由 Default
特质指定的返回类型的默认值。
示例用法
// Configure mock to return 9001 profit when given args 42 and 10. Any other
// arguments will cause the mock to return a profit of 1.
let sheet = MockBalanceSheet::default();
sheet.profit.return_value_for((42, 10), 9001);
sheet.profit.return_value(1);
// Configure mock to call arbitrary function. The mock will return the
// result of the function back to the caller.
fn subtract(revenue: u32, costs: u32) -> i32 {
revenue - costs
}
let sheet2 = MockBalanceSheet::default();
sheet.use_fn(subtract);
有关如何使用这些功能的代码示例,请参阅 rustdocs。
可以同时使用其中许多功能。例如,可以告诉模拟为参数 (
42,10) 返回特定的值使用 return_value_for
,但对于其他所有内容使用 return_value
返回默认值 1。
当调用模拟方法时,它使用优先级顺序来确定是否应返回默认值、返回特定值、调用函数等。
这些方法的优先级顺序与上述表格中指定的顺序相同。例如,如果调用 use_fn
和 return_value
,那么模拟将调用传递给 use_fn
的函数,并且不返回值。
如果一个方法返回一个 Option<T>
或一个 Result<T, E>
,那么可以使用以下便利函数来指定默认返回值
方法 | 返回值 | 它做什么 |
---|---|---|
return_some |
Some(val) |
返回 Some(val) 枚举的 Option |
return_none |
None |
返回 None 枚举的 Option |
return_ok |
Ok(val) |
返回 Ok(val) 枚举的 Result |
return_err |
Err(val) |
返回 Err(val) 枚举的 Result |
然后:断言代码测试使用了模拟的预期方式
测试运行后,我们可以验证模拟是否被正确次数和正确的参数调用。
下表列出了可以用来验证模拟是否按预期调用的方法。
方法 | 返回值 | 它做什么 |
---|---|---|
calls() |
Vec<(Args)> |
返回每个模拟调用的参数,按调用时间排序。 |
called() |
bool |
如果方法至少被调用一次,则返回 true 。 |
num_calls() |
usize |
方法被调用的次数。 |
called_with((args)) |
bool |
如果方法至少被调用一次,并且带有给定的 args ,则返回 true 。 |
has_calls(vec!((args), ...)) |
bool |
如果方法至少被调用一次,并且对于给定的 args 元组,则返回 true 。 |
has_calls_in_order(vec!((args), ...)) |
bool |
如果对于给定的 args 集合,方法至少被调用一次,并且调用参数与输入 vec 中指定的顺序相同,则返回 true 。 |
has_calls_exactly(vec!((args), ...)) |
bool |
如果对于给定的 args 集合,方法恰好被调用一次,则返回 true 。 |
has_calls_exactly_in_order(vec!((args), ...)) |
bool |
如果对于给定的 args 集合,方法恰好被调用一次,并且调用参数与输入 vec 中指定的顺序相同,则返回 true 。 |
called_with_pattern(matcher_set) |
bool |
如果方法至少被调用一次,并且参数与给定的匹配器集匹配,则返回 true 。 |
has_patterns(vec!(matcher_set, ...)) |
bool |
如果所有给定的匹配器集都至少被模拟的调用匹配一次,则返回 true 。 |
has_patterns_in_order(vec!(matcher_set, ...)) |
bool |
如果模拟有匹配所有指定匹配器集的调用,则返回 true 。匹配器集必须按照输入 matcher_set 向量中指定的顺序匹配。 |
has_patterns_exactly(vec!(matcher_set, ...)) |
bool |
如果所有提供的匹配器集合都至少被模拟的调用匹配一次,则返回 true 。调用次数等于指定匹配器集合的数量。 |
has_patterns_exactly_in_order(vec!(matcher_set, ...)) |
bool |
如果模拟的调用匹配所有指定的匹配器集合,则返回 true 。匹配器集合必须按照输入 matcher_set 向量中指定的顺序匹配。调用次数等于指定匹配器集合的数量。 |
示例用法
let sheet = MockBalanceSheet::default();
// invoke mock method
sheet.profit(42, 10);
sheet.profit(5, 0);
// assert the invocation was recorded correctly
assert!(sheet.profit.called());
assert!(sheet.profit.called_with((42, 10)));
assert!(sheet.profit.has_calls((42, 10)));
assert!(sheet.profit.has_calls_in_order((42, 10), (5, 0)));
assert!(sheet.profit.has_calls_exactly((5, 0), (42, 10)));
assert!(sheet.profit.has_calls_exactly_in_order((42, 10), (5, 0)));
有关如何使用基于模式的断言的详细信息,请参阅 模式匹配 部分。
在多个测试中重用模拟
调用 reset_calls()
清除模拟方法的所有记录调用。
为了确保各个测试尽可能独立(因此,更少可能存在错误),建议为不同的测试用例构造不同的模拟对象。
尽管如此,在以下情况下重用相同的模拟及其返回值可能会使测试代码更容易阅读和维护:在这些情况下,可以使用 reset_calls()
清除之前测试的调用。
模式匹配
当一个模拟函数被用于测试时,我们通常想对模拟调用的内容进行断言。例如,假设我们正在测试一些逻辑,该逻辑确定机器人的下一步行动。我们可能想断言这个逻辑告诉机器人做什么。
let robot = MockRobot::default();
do_something_with_the_robot(&robot);
assert!(robot.move_forward.called_with(100);
上述代码检查 do_something_with_the_robot()
应该告诉机器人前进100个单位。然而,有时你可能不想这么具体。这会使测试变得过于严格。过度指定会导致脆弱的测试并掩盖测试的意图。因此,建议仅指定必要的部分——不多也不少。
如果你关心 moved_forward()
将被调用,但对其实际参数不感兴趣,你可以简单地断言调用次数
assert!(robot.move_forward.called())
assert!(robot.move_forward.num_calls() == 1u)
但如果我们要检查的行为稍微复杂一些怎么办?如果我们想检查机器人是否至少前进了100个单位,但机器人前进得更多也没关系呢?在这种情况下,我们的断言比“是否调用了 move_forward()
?”更具体,但约束条件不如“必须正好前进 100 个单位”严格。
如果我们知道当前实现将正好前进100个单位,那么直接使用精确相等性检查可能很有诱惑力。然而,正如之前提到的,这使得测试非常脆弱。如果实现技术上是自由的,可以开始前进超过100个单位,那么这个测试就会失败。开发人员必须转到测试并修复失败的测试。如果没有过度限制,这种更改是不必要的。这可能听起来很微不足道。然而,代码库会增长。这意味着测试的数量也会增长。如果所有测试都是脆弱的,那么在产品代码更改时维护/更新这些测试将变成一项巨大的负担。
double
允许开发人员通过使用模糊断言来避免这种情况。可以使用 模式匹配 对模拟参数值执行更宽松的断言。在机器人示例中,我们可以使用一行代码断言机器人前进了100 或更多 个单位。
use double::matcher::*;
assert!(robot.move_forward.called_with_pattern(p!(ge, 100)));
让我们来分解一下。首先,我们将 called_with
改为 called_with_pattern
。然后,我们按照如下方式传递我们想要使用的匹配器
p!(ge, 100)
p!
宏生成一个模拟对象接受的匹配器函数。 ge
是匹配器的名称(ge = 大于等于)。 100
是一个 匹配器参数,用于配置匹配器以匹配正确的值。将 100
传递给 ge
的意思是“构造一个匹配函数,该函数匹配大于等于 100
的值”。
对于接受多个参数的函数,也可以使用模式匹配。我们只需使用 matcher!
宏将单个参数匹配器包装起来
assert!(robot.move.called_with_pattern(
matcher!( p!(ge, 100), p!(eq, Direction::Left) )
));
上面的代码断言机器人的 move()
方法被调用,并且机器人向左方向移动至少 100 个单位。
还有其他一些检查函数,例如 called_with_pattern()
,它们使用了模式匹配器。有关这些函数的列表,请参阅 然后:断言测试代码以期望的方式使用模拟 部分。
正式定义
正式上讲,模式匹配器被定义为接收单个参数值的函数。它对值执行某些检查,如果参数的值“匹配”所需的模式,则返回 true
,否则返回 false
。例如,any
匹配器(它接受任何值)被定义为
pub fn any<T>(_: &T) -> bool {
true
}
其中 _
是参数值。大多数匹配器都是 参数化的。例如,eq
匹配器接受一个参数——期望的值。同样,匹配器 ge
也接受一个参数,该参数指定期望值应该大于或等于的数字。
匹配器可以接受任意数量的参数。额外的参数作为匹配器函数的额外参数指定。例如,以下是 eq
匹配器函数的定义。
pub fn eq<T: PartialEq>(arg: &T, target_val: T) -> bool {
*arg == target_val
}
要使用参数化匹配器,它必须绑定到参数集。 p!
宏就是这样做的。它接收一个匹配器函数以及绑定到它的参数集,然后返回一个新的闭包函数,该闭包函数绑定到这些参数。例如
let bound_matcher = p!(eq, 42);
assert!(bound_matcher(42) == true);
assert!(bound_matcher(10) == false);
注意绑定的匹配器只接受一个参数——正在匹配的参数值。匹配器函数的其他参数绑定在返回的闭包内。
当将匹配器传递给 Mock
的断言调用(例如 called_with_pattern
和 has_patterns
)时,它们需要作为 匹配器集 传递。 Mock
的断言检查模拟函数具有的完整参数集的操作,而不仅仅是单个参数。例如,如果一个模拟函数接受三个参数,那么 called_with_pattern
预期一个大小为 3 的匹配器集。该集包含每个模拟参数的一个匹配器。
匹配器集是通过使用 matcher!
宏构建的。此宏接受模拟函数中每个参数的绑定匹配器函数。匹配器函数的顺序对应于模拟函数中参数的顺序。
下面是 matcher!
的一个使用示例
let arg1_matcher = p!(eq, 42);
let arg2_matcher = p!(lt, 10);
let arg_set_matcher = matcher!(arg1_matcher, arg2_matcher);
assert!(matcher((42, 5)) == true);
assert!(matcher((42, -5)) == true);
assert!(matcher((42, 10)) == false);
assert!(matcher((100, 5)) == false);
实际上,大多数对 matcher!
和 p!
的调用都会在断言调用中进行。将 matcher!
和 p!
的调用内联组合允许开发者编写如下简洁且富有表现力的断言。
// This reads:
// * first arg should be >= 100
// * second arg should be `Direction::Left`
assert!(robot.move.called_with_pattern(
matcher!( p!(ge, 100), p!(eq, Direction::Left) )
));
嵌套断言器
可以嵌套断言器。例如,你可能想断言一个参数匹配 多个 模式。
回到机器人的例子,我们可能不关心机器人向前移动的确切量,但关心它在某个范围内。假设我们想断言它的移动在 100-200 个单位范围内。不多也不少。
我们使用两个断言器,ge
和 le
,用于一个参数。我们将它们包裹在组合断言器 all_of
中,如下所示。
assert!(robot.move_forward.called_with_pattern(
matcher!(
p!(all_of, vec!(
p!(ge, 100),
p!(le, 200))))
));
用户可以使用 p!
宏嵌套任意数量的断言器。但尽量不要过度使用此功能,因为这可能导致难以阅读的测试。
注意:上面的内容是为了说明。执行值范围检查的更简单方法是使用非组合宏
between_exc
和between_inc
。
内置断言器
本节列出了库中内置的所有标准断言器。如果这些断言器都不符合你的使用情况,请参阅 定义你自己的断言器 部分。
通配符
any() |
参数可以是正确类型的任何值 |
比较断言器
eq(值) |
参数==值 |
ne(值) |
参数!=值 |
lt(值) |
参数<值 |
le(值) |
参数<=值 |
gt(值) |
参数>值 |
ge(值) |
参数>=值 |
is_some(断言器) |
参数是一个 Option::Some ,其内容与 matcher 匹配 |
is_ok(断言器) |
参数是一个 Result::Ok ,其内容与 matcher 匹配 |
is_err(断言器) |
参数是一个 Result::er ,其内容与 matcher 匹配 |
浮点数断言器
f32_eq(值) |
参数是一个与 f32 value 大约相等的值,将两个 NaN 视为不等。 |
f64_eq(值) |
参数是一个与 f64 value 大约相等的值,将两个 NaN 视为不等。 |
nan_sensitive_f32_eq(值) |
参数是一个与 f32 value 大约相等的值,将两个 NaN 视为相等。 |
nan_sensitive_f64_eq(值) |
参数是一个与 f64 value 大约相等的值,将两个 NaN 视为相等。 |
字符串断言器
contains(字符串) |
参数包含作为子字符串的 string 。 |
starts_with(前缀) |
参数以字符串 prefix 开头。 |
starts_with(suffix) |
参数以字符串 suffix 结尾。 |
eq_nocase(字符串) |
参数等于 string ,忽略大小写。 |
ne_nocase(值) |
参数不等于 string ,忽略大小写。 |
容器断言器
目前没有用于检查容器内容的断言器。这些将在 double
的未来版本中添加。有一个 GitHub 问题 用于跟踪这项工作。
组合断言器
all_of(vec!(m1,m2, ...mn)) |
参数匹配所有匹配器 m1 到 mn 。 |
any_of(vec!(m1,m2, ...mn)) |
匹配匹配器 m1 到 mn 中的至少一个。 |
not(m) |
参数不匹配匹配器 m 。 |
定义自己的匹配器
如果内置的匹配器都不符合您的使用场景,您可以定义自己的。
假设我们正在测试一个RESTful服务。我们有一些请求处理逻辑。我们想测试处理逻辑正确地响应了请求。在这种情况下,“正确”意味着它返回了一个包含“time”键的JSON对象。
以下是测试的生产代码
trait ResponseSender {
fn send_response(&mut self, response: &str);
}
fn request_handler(response_sender: &mut ResponseSender) {
// business logic here
response_sender.send_response(
"{ \"current_time\": \"2017-06-10 20:30:00\" }");
}
让我们模拟响应发送者并断言JSON响应的内容
mock_trait!(
MockResponseSender,
send_response(&str) -> ());
impl ResponseSender for MockResponseSender {
mock_method!(send_response(&mut self, response: &str));
}
#[test]
fn ensure_current_time_field_is_returned() {
// GIVEN:
let mut mock_sender = MockResponseSender::default();
// WHEN:
request_handler(&mock_sender);
// THEN:
// check the sender received a response that contains a current_time field
}
这个检查很繁琐。必须手动提取传递给模拟器的文本字符串,将其解析为JSON(处理无效JSON)并手动检查
对于单个测试,这可能不是问题。然而,想象一下,如果我们有数十个API端点的多个测试用例。在所有测试中重复相同的JSON断言逻辑会导致代码重复。这种重复掩盖了测试的意图,并使它们更难更改。
自定义匹配器来拯救!我们可以使用匹配器来检查响应文本字符串是否是一个有效的JSON对象,它包含特定的键/字段。
匹配器定义为至少接受一个参数(正在匹配的 arg
)和零个或多个参数的函数。它返回一个 bool
,表示 arg
是否匹配。在这种情况下,我们有一个参数——我们断言存在于响应中的 key
。
extern crate json;
use self::json;
fn is_json_object_with_key(arg: &str, key: &str) -> bool {
match json::parse(str) {
Ok(json_value) => match json_value {
Object(object) => match object.get(key) {
Some(_) => true // JSON object that contains key
None => false // JSON object that does contain key
},
_ => false // not a object (must be another JSON type)
},
Err(_) => false // not valid JSON
}
}
使用匹配器需要将其绑定到参数(使用 p!
)并将它传递给模拟断言方法,如下所示
fn ensure_current_time_field_is_returned() {
// GIVEN:
let mut mock_sender = MockResponseSender::default();
// WHEN:
request_handler(&mock_sender);
// THEN:
// we expect a "time" field to be in the response JSON
assert(response_sender.send_response.called_with_pattern(
p!(is_json_object_with_key, "time")
));
// we DO NOT expect a "time" field to be in the response JSON
assert(!response_sender.send_response.called_with_pattern(
p!(is_json_object_with_key, "records")
));
}
其他用例
模拟没有返回值的方法
如果一个方法不返回任何内容,则在生成方法时可以使用双的宏省略返回值
trait Queue {
fn enqueue(&mut self, value: i32);
fn dequeue(&mut self) -> i32;
}
mock_trait!(
MockQueue,
enqueue(i32) -> (), // still have to specify return value here...
dequeue() -> i32);
impl Queue for MockQueue {
mock_method!(enqueue(&mut self, value: i32)); // ...but not here!
mock_method!(dequeue(&mut self) -> i32);
}
模拟返回不实现 Default
的类型的方法
mock_trait!
宏假定所有被模拟的 trait
中的方法的返回类型都实现了 Default
。这使得构建模拟对象变得方便。可以通过调用 MockTrait::default()
来构建模拟对象并自动配置它返回所有方法的默认值。
如果一个 trait
提供了一个返回不实现 Default
的类型的方法的,那么必须使用 mock_trait_no_default!
来生成模拟。这个宏生成一个不实现 Default
的模拟。客户端必须使用 MockTrait::new()
构造生成的模拟实例,并为每个方法手动指定默认返回值。
例如
// `Result` does not implement the `Default` trait. Trying to mock `UserStore`
// using the `mock_trait!` macro will fail. We use `mock_trait_no_default!`
// instead.
pub trait UserStore {
fn get_username(&self, id: i32) -> Result<String, String>;
}
mock_trait_no_default!(
MockUserStore,
get_username(i32) -> Result<String, String>);
impl UserStore for MockUserStore {
mock_method!(get_username(&self, id: i32) -> Result<String, String>);
}
fn test_manually_setting_default_retval() {
// GIVEN:
// Construct instance of the mock, manually specifying the default
// return value for `get_username()`.
let mock = MockUserStore::new(
Ok("default_user_name".to_owned())); // get_username() default retval
// WHEN:
let result = mock.get_username(10001);
// THEN:
assert_eq!(Ok("default_username".to_owned()), result);
}
模拟接受 &str
引用的方法
&str
是一个常见的参数类型。然而,double 不支持使用额外的样板代码模拟接受 &str
参数的方法。
这是因为在模拟中不能存储接收到的 &str
参数。模拟需要拥有给定的参数,而 &str
是一个非拥有引用。因此,模拟特徵必须像这样指定
trait TextStreamWriter {
fn write(&mut self, text: &str);
}
mock_trait!(
MockTextStreamWriter,
// have to use `String`, not `&str` here, since `&str` is a reference
write(String) -> ()
);
impl TextStreamWriter for MockTextStreamWriter {
mock_method!(write(&mut self, text: &str), self, {
// manually convert the reference to an owned `String` before passing
// it to the underlying mock object
self.write.call(text.to_owned())
});
}
上面使用的 mock_method
变体允许您手动指定生成的函数的主体。自定义主体只是将 &str
参数转换为拥有字符串,并手动传递到底层的 write
Mock
对象。(通常自动生成的主体会为您完成这项工作)。
注意:底层模拟对象的名字总是与模拟方法的名称相同。因此,在自定义
write
主体中,您应该将参数传递到self.write
。
&str
参数很常见。我们理解,每次它们出现时手动指定主体都很不方便。有计划添加一个宏来自动生成调用 to_owned()
的主体。当它发布时,本节将进行更新。
使用泛型类型参数模拟方法
使用泛型类型参数模拟方法需要额外的努力。例如,假设有一个负责在程序中比较任意两个值的 Comparator
特征。它可能看起来像这样
trait Comparator {
fn is_equal<T: Eq>(&self, a: &T, b: &T) -> bool;
}
T
可以是多种类型。目前,我们无法在底层的 Mock
对象中存储具有泛型的调用参数。因此,必须将泛型类型转换为不同的、通用的表示。一种绕过这种限制的方法是将每个泛型类型转换为 String
。例如,对于 Comparator
特征
# #[macro_use] extern crate double;
use std::string::ToString;
trait Comparator {
fn is_equal<T: Eq + ToString>(&self, a: &T, b: &T) -> bool;
}
mock_trait!(
MockComparator,
// store all passed in call args as strings
is_equal((String, String)) -> bool
);
impl Comparator for MockComparator {
mock_method!(is_equal<(T: Eq + ToString)>(&self, a: &T, b: &T) -> bool, self, {
// Convert both arguments to strings and manually pass to underlying
// mock object.
// Notice how the both arguments as passed as a single tuple. The
// underlying mock object always expects a single tuple.
self.is_equal.call((a.to_string(), b.to_string()))
});
}
如果所有 T
的 to_string
转换都不是有损的,那么我们的模拟期望可以非常精确。如果 to_string
转换 是 有损的,那么这个机制仍然可以用来捕获所有传递对象的属性,保存在结果 String
中。
这种方法要求编写者确保要测试的代码将 ToString
特征添加到特征的类型参数约束中。这种限制迫使测试编写者修改生产代码以使用 double
进行模拟。
尽管如此,使用 double
模拟具有类型参数的泛型方法仍然有价值。尽管它会给生产代码添加样板代码,手动实现模拟方法主体也很麻烦,但增值在于所有参数匹配、期望、调用测试函数等都是由 double
处理的。
double 的作者认为,重新实现上述功能比模拟具有类型参数的方法所需的小量样板代码更繁琐。
使用 double 模拟自由函数
double::Mock
对象也可以用于自由函数。考虑以下函数
fn generate_sequence(func: &dyn Fn(i32) -> i32, min: i32, max: i32) -> Vec<i32> {
// exclusive range
(min..max).map(func).collect()
}
此函数遍历一系列整数,使用提供的转换函数 func
将每个整数映射到另一个整数。
在测试 generate_sequence
时,无需自己生成模拟转换函数的样板代码,可以使用宏 mock_func!
。此宏为您生成一个 double::Mock
对象和一个封装它的闭包。例如
#[macro_use]
extern crate double;
fn generate_sequence(func: &dyn Fn(i32) -> i32, min: i32, max: i32) -> Vec<i32> {
// exclusive range
(min..max).map(func).collect()
}
fn test_function_used_correctly() {
// GIVEN:
mock_func!(
mock, // name of variable that stores mock object
mock_fn, // name of variable that stores closure wrapper
i32, // return value type
i32); // argument1 type
mock.use_closure(Box::new(|x| x * 2));
// WHEN:
let sequence = generate_sequence(&mock_fn, 1, 5);
// THEN:
assert_eq!(vec!(2, 4, 6, 8), sequence);
assert!(mock.has_calls_exactly(vec!(
1, 2, 3, 4
)));
}
fn main() {
test_function_used_correctly();
}
您可以在 mock_func!
宏的前两个参数中指定应存储生成的模拟对象和闭包的变量名。
如果函数的返回类型没有实现 Default
,则必须使用 mock_func_no_default!
宏,如下所示
// ...
fn test_function_with_custom_defaults() {
// GIVEN:
mock_func_no_default!(
mock,
mock_fn,
i32, // return value type
42, // default return value
i32); // argument1 type
mock.use_closure_for((3), Box::new(|x| x * 2));
// WHEN:
let sequence = generate_sequence(&mock_fn, 1, 5);
// THEN:
assert_eq!(vec!(42, 42, 6, 42), sequence);
assert!(mock.has_calls_exactly(vec!(
1, 2, 3, 4
)));
}
fn main() {
test_function_with_custom_defaults();
}
依赖项
~310KB