11-宏用例-编写测试
9.9 宏用例——编写测试
在为单元测试编写测试用例时,经常会用到宏。我们假设你正在编写HTTP客户端程序库,并且希望在各种HTTP谓词(如GET/POST)和不同的URL上测试该客户端程序。编写测试的通常做法是为每种类型的请求和URL创建函数。但是,有一种更好的方法可以做到这一点。通过宏,你可以构建一个小型DSL来执行测试以减少测试时间,这些测试是可读的,也可以在编译期进行类型检查。为了证明这一点,我们运行cargo new http_tester --lib命令创建一个新的程序库项目,并在其中包含我们的宏定义。该宏实现了一种小型语言,旨在描述对URL的简单HTTP GET/POST测试。以下是该语言的示例:
http://duckduckgo.com GET => 200
http://httpbin.org/post POST => 200, "key" => "value"
第1行代码表示向duckduckgo.com发送了一个GET请求,并期望返回代码为200(请求成功)。第2行代码表示向httpbin.org发送一个POST请求,以及带有自定义语法的表单参数"key"=>"value",它还期望返回代码为200。上述示例非常简单,但是足以满足我们演示上述宏的需要。
我们假定已经实现了该程序库,并将使用名为reqwest的HTTP请求库。我们将在Cargo.toml文件中添加对reqwest库的依赖:
# http_tester/Cargo.toml
[dependencies]
reqwest = "0.9.5"
以下是lib.rs的代码:
// http_tester/src/lib.rs
#[macro_export]
macro_rules! http_test {
($url:tt GET => $code:expr) => {
let request = reqwest::get($url).unwrap();
println!("Testing GET {} => {}", $url, $code);
assert_eq!(request.status().as_u16(), $code);
};
($url:tt POST => $code:expr, $($k:expr => $v:expr),*) => {
let params = [$(($k, $v),)*];
let client = reqwest::Client::new();
let res = client.post($url)
.form(¶ms)
.send().unwrap();
println!("Testing POST {} => {}", $url, $code);
assert_eq!(res.status().as_u16(), $code);
};
}
#[cfg(test)]
mod tests {
#[test]
fn test_http_verbs() {
http_test!("http://duckduckgo.com" GET => 200);
http_test!("http://httpbin.org/post" POST => 200, "hello" =>
"world", "foo" => "bar");
}
}
在宏定义中,我们只是匹配规则,GET和POST被视为字面标记。在匹配臂中,我们创建了请求客户端,并对输入返回的状态代码进行断言,该状态代码是提供给宏的。POST测试用例还有一个自定义语法,用于提供查询参数,例如"key"=>"value",它在params变量中作为数组进行收集。然后将其传递给reqwest::post构造器方法的form方法。我们将在第13章深入介绍Web请求程序库的详细内容。
让我们运行cargo test命令,得到以下输出结果:
running 1 test
test tests::test_http_verbs ... ok
请花一点时间思考一下在这里使用宏的好处是什么。这也可以作为带#[test]注释的函数调用来实现,但是宏有一些额外的优点,即使最基本的形式也是如此。优点之一是在编译期检查HTTP谓词,使我们的测试更具说明性。如果我们尝试使用不在预期范围之内的测试用例(例如HTTP DELETE)调用宏,那么将得到以下错误提示信息:
error: no rules expected the token `DELETE`
除了使用它们进行枚举测试用例之外,宏还用于根据某些外部环境状态(例如数据库表、时间及日期等)生成Rust代码。它们可以用于装饰具有自定义属性的结构体,在编译期为它们生成任意代码,或者创建新的linter插件以进行Rust编译器本身并不支持的其他静态分析。一个很好的例子是我们已经用过的clippy lint工具。宏还用于生成调用原生C程序库的代码。我们将会在第10章对这一主题进行详细介绍。