ch12-04-testing-the-librarys-functionality.md
commit 04170d1feee2a47525b39f1edce77ba615ca9cdf
现在我们将逻辑提取到了 src∕lib.rs 并将所有的参数解析和错误处理留在了 src∕main.rs 中,为代码的核
心功能编写测试将更加容易。我们可以直接使用多种参数调用函数并检查返回值而无需从命令行运行二
进制文件了。
在这一部分,我们将遵循测试驱动开发(Test Driven Development, TDD)的模式来逐步增加 minigrep
的搜索逻辑。这是一个软件开发技术,它遵循如下步骤:
- 编写一个失败的测试,并运行它以确保它失败的原因是你所期望的。
- 编写或修改足够的代码来使新的测试通过。
- 重构刚刚增加或修改的代码,并确保测试仍然能通过。
- 从步骤 1 开始重复!
这只是众多编写软件的方法之一,不过 TDD 有助于驱动代码的设计。在编写能使测试通过的代码之前编
写测试有助于在开发过程中保持高测试覆盖率。
我们将测试驱动实现实际在文件内容中搜索查询字符串并返回匹配的行示例的功能。我们将在一个叫做
search 的函数中增加这些功能。
编写失败测试
去掉 src∕lib.rs 和 src∕main.rs 中用于检查程序行为的 println! 语句,因为不再真正需要他们了。接着我
们会像 第十一章 那样增加一个 test 模块和一个测试函数。测试函数指定了 search 函数期望拥有的行
为:它会获取一个需要查询的字符串和用来查询的文本,并只会返回包含请求的文本行。
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub filename: String,
}
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err(“not enough arguments”);
}
let query = args[1].clone();
let filename = args[2].clone();
Ok(Config { query, filename })
}
}
pub fn run(config: Config) -> Result<(), Box> {
let contents = fs::read_to_string(config.filename)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super:😗;
#[test]
fn one_result() {
let query = “duct”;
let contents = "
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec![“safe, fast, productive.”], search(query, contents));
}
}
创建一个我们期望的 search 函数的失败测试
这里选择使用 ”duct” 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含
”duct”。(注意双引号之后的反斜杠,这告诉 Rust 不要在字符串字面值内容的开头加入换行符)我们断
言 search 函数的返回值只包含期望的那一行。
我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译:search 函数还不存在呢!我们将增加
足够的代码来使其能够编译:一个总是会返回空 vector 的 search 函数定义,如示例 12-16 所示。然后这个
测试应该能够编译并因为空 vector 并不匹配一个包含一行 ”safe, fast, productive.” 的 vector 而失败。
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub filename: String,
}
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err(“not enough arguments”);
}
let query = args[1].clone();
let filename = args[2].clone();
Ok(Config { query, filename })
}
}
pub fn run(config: Config) -> Result<(), Box> {
let contents = fs::read_to_string(config.filename)?;
Ok(())
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
vec![]
}
#[cfg(test)]
mod tests {
use super:😗;
#[test]
fn one_result() {
let query = “duct”;
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec![“safe, fast, productive.”], search(query, contents));
}
}
刚好足够使测试通过编译的 search 函数定义
注意需要在 search 的签名中定义一个显式生命周期 ’ a 并用于 contents 参数和返回值。回忆一下 第十
章 中讲到生命周期参数指定哪个参数的生命周期与返回值的生命周期相关联。在这个例子中,我们表明
返回的 vector 中应该包含引用参数 contents(而不是参数query)slice 的字符串 slice。
换句话说,我们告诉 Rust 函数 search 返回的数据将与 search 函数中的参数 contents 的数据存在的一
样久。这是非常重要的!为了使这个引用有效那么 被 slice 引用的数据也需要保持有效;如果编译器认
为我们是在创建 query 而不是 contents 的字符串 slice,那么安全检查将是不正确的。
如果尝试不用生命周期编译的话,我们将得到如下错误:
$ cargo build
Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
–> src/lib.rs:28:51
|
28 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function’s return type contains a borrowed value, but the signature does not say whether it is borrowed from query
or contents
help: consider introducing a named lifetime parameter
|
28 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> {
| ++++ ++ ++ ++
For more information about this error, try rustc --explain E0106
.
error: could not compile minigrep
due to previous error
Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数 contents 包含了所有的文本而
且我们希望返回匹配的那部分文本,所以我们知道 contents 是应该要使用生命周期语法来与返回值相
关联的参数。
其他语言中并不需要你在函数签名中将参数与返回值相关联。所以这么做可能仍然感觉有些陌生,随着
时间的推移这将会变得越来越容易。你可能想要将这个例子与第十章中 ” 生命周期与引用有效性” 部分做
对比。
现在运行测试:
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished test [unoptimized + debuginfo] target(s) in 0.97s
Running unittests (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 1 test
test tests::one_result … FAILED
failures:
---- tests::one_result stdout ----
thread ‘main’ panicked at ‘assertion failed: (left == right)
left: ["safe, fast, productive."]
,
right: []
’, src/lib.rs:44:9
note: run with RUST_BACKTRACE=1
environment variable to display a backtrace
failures:
tests::one_result
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass ‘–lib’
好的,测试失败了,这正是我们所期望的。修改代码来让测试通过吧!