现在,我们将逻辑提取到src / lib.rs中,并将参数收集和错误处理留在src / main.rs中,为代码的核心功能编写测试要容易得多。我们可以直接使用各种参数调用函数,并检查返回值,而不必从命令行调用二进制文件。您可以自己编写Config::new和run功能的一些测试。
在本节中,我们将minigrep使用测试驱动的开发(TDD)流程将搜索逻辑添加到程序中。此软件开发技术遵循以下步骤:
- 编写一个失败的测试并运行它,以确保由于您期望的原因而失败。
- 编写或修改足够的代码以使新的测试通过。
- 重构刚刚添加或更改的代码,并确保测试继续通过。
- 从步骤1开始重复!
此过程只是编写软件的许多方法之一,但是TDD也可以帮助驱动代码设计。在编写使测试通过的代码之前编写测试,有助于在整个过程中保持较高的测试覆盖率。
我们将测试驱动该功能的实现,该功能实际上将在文件内容中搜索查询字符串并生成与查询匹配的行列表。我们将在名为的函数中添加此功能 search。
编写失败的测试
因为我们不需要他们了,让我们删除println!从报表 的src / lib.rs和SRC / main.rs我们用来检查程序的行为。然后,在src / lib.rs中,我们将添加一个tests带有测试功能的模块,就像在第11章中所做的那样。测试函数指定我们希望search函数具有的行为:它将接受一个查询,并在其中搜索查询的文本,并且它将仅返回包含查询的文本中的行。清单12-15显示了该测试,该测试尚未编译。
文件名:src / lib.rs
#[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)); }}
清单12-15:为search 我们希望拥有的功能创建失败的测试
此测试搜索字符串"duct"。我们正在搜索的文本为三行,其中只有一行包含"duct"。我们断言从search函数返回的值仅包含我们期望的行。
我们无法运行该测试并看到它失败,因为该测试甚至无法编译:该search功能尚不存在!因此,现在我们通过添加search 始终返回一个空向量的函数的定义,来添加足够的代码以使测试得以编译并运行,如清单12-16所示。然后测试应该编译并失败,因为空向量与包含该行的向量不匹配"safe, fast, productive."
文件名:src / lib.rs
pub fn search(query: &str, contents: &'a str) -> Vec { vec![]}
清单12-16:定义了足够的search 功能,以便我们的测试可以编译
注意,我们需要'a在签名中定义的显式生存期, search并与contents参数和返回值一起使用。在 第10章中,生命周期参数指定了将哪个参数生命周期连接到返回值的生命周期。在这种情况下,我们指示返回的向量应包含引用参数切片contents(而不是参数query)的字符串切片。
换句话说,我们告诉Rust,search只要search在contents参数中传递到函数中的 数据,函数返回的数据就会存在。这个很重要!引用的数据由一个切片需要是有效的基准是有效的; 如果编译器假定我们正在制作query而不是的字符串切片contents,它将错误地进行安全检查。
如果我们忘记了生命周期批注并尝试编译此函数,则会收到此错误:
$ 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 { | ^ expected 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`error: aborting due to previous errorFor more information about this error, try `rustc --explain E0106`.error: could not compile `minigrep`.To learn more, run the command again with --verbose.
Rust无法知道我们需要两个参数中的哪一个,因此我们需要告诉它。因为contents是包含所有文本的参数,并且我们想返回该文本中匹配的部分,所以我们知道contents应该使用生命周期语法将其连接到返回值。
其他编程语言不需要您连接参数以在签名中返回值。尽管这看起来很奇怪,但随着时间的推移它将变得更加容易。您可能需要将此示例与第10章中的“使用生命周期验证引用”部分进行比较。
现在让我们运行测试:
$ cargo test Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished test [unoptimized + debuginfo] target(s) in 0.97s Running target/debug/deps/minigrep-4672b652f7794785running 1 testtest tests::one_result ... FAILEDfailures:---- tests::one_result stdout ----thread 'main' panicked at 'assertion failed: `(left == right)` left: `["safe, fast, productive."]`, right: `[]`', src/lib.rs:44:9note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.failures: tests::one_resulttest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered outerror: test failed, to rerun pass '--lib'
太好了,测试失败了,完全符合我们的预期。让测试通过!
编写代码通过测试
目前,我们的测试失败了,因为我们总是返回一个空向量。要解决此问题并实施search,我们的程序需要遵循以下步骤:
- 遍历内容的每一行。
- 检查该行是否包含我们的查询字符串。
- 如果是这样,请将其添加到我们要返回的值列表中。
- 如果没有,则什么也不做。
- 返回匹配的结果列表。
让我们完成每个步骤,从遍历行开始。
通过该lines方法迭代线
Rust有一个有用的方法来处理字符串的逐行迭代,方便地命名为lines,如清单12-17所示。请注意,这尚未编译。
文件名:src / lib.rs
pub fn search(query: &str, contents: &'a str) -> Vec { for line in contents.lines() { // do something with line }}
清单12-17:遍历其中的每一行 contents
该lines方法返回一个迭代器。我们将在第13章中深入讨论迭代器 ,但回想一下您在清单3-5中看到了使用迭代器的这种方式,其中我们使用了for带有迭代器的循环来对集合中的每个项目运行一些代码。
在每一行中搜索查询
接下来,我们将检查当前行是否包含查询字符串。幸运的是,字符串有一个有用的名为的方法contains,可以为我们做到这一点!contains在search函数中添加对方法的调用,如清单12-18所示。请注意,这仍然不会编译。
文件名:src / lib.rs
pub fn search(query: &str, contents: &'a str) -> Vec { for line in contents.lines() { if line.contains(query) { // do something with line } }}
清单12-18:添加功能以查看行中是否包含字符串 query
存储匹配线
我们还需要一种方法来存储包含查询字符串的行。为此,我们可以在for循环之前创建一个可变的向量,并调用push将a存储line在向量中的方法。在后for循环,我们回到了载体,如清单12-19英寸
文件名:src / lib.rs
pub fn search(query: &str, contents: &'a str) -> Vec { let mut results = Vec::new(); for line in contents.lines() { if line.contains(query) { results.push(line); } } results}
清单12-19:存储匹配的行,以便我们可以返回它们
现在,该search函数应仅返回包含的行query,并且测试应通过。让我们运行测试:
$ cargo test Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished test [unoptimized + debuginfo] target(s) in 1.22s Running target/debug/deps/minigrep-4672b652f7794785running 1 testtest tests::one_result ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Running target/debug/deps/minigrep-caf9dbee196c78b9running 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests minigreprunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
我们的测试通过了,所以我们知道它有效!
在这一点上,我们可以考虑重构搜索功能实现的机会,同时保持测试通过以保持相同的功能。搜索功能中的代码还不错,但是它没有利用迭代器的一些有用功能。我们将在第13章返回该示例,在此我们将详细探讨迭代器,并研究如何改进它。
![eff37102db157f7854a0030898af4454.png](https://i-blog.csdnimg.cn/blog_migrate/66f6f0a021ea479d99aad4f4479557a3.jpeg)
在search函数中使用run函数
现在该search函数正在运行并经过测试,我们需要search 从run函数中调用。我们需要将config.query值和从文件contents中run读取的值 传递给search函数。然后run 将打印从返回的每一行search:
文件名:src / lib.rs
pub fn run(config: Config) -> Result> { let contents = fs::read_to_string(config.filename)?; for line in search(&config.query, &contents) { println!("{}", line); } Ok(())}
我们仍在使用for循环从中返回每一行search并进行打印。
现在整个程序应该可以工作了!让我们尝试一下,首先使用一个单词,该单词应该与艾米莉·狄金森的诗“青蛙”恰好返回一行:
$ cargo run frog poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.38s Running `target/debug/minigrep frog poem.txt`How public, like a frog
凉!现在,让我们尝试一个可以匹配多行的单词,例如“ body”:
![28acfa6c3de124a85c40d7203420f483.png](https://i-blog.csdnimg.cn/blog_migrate/d51edb90a286a26e2a8b344b36fa4b4e.jpeg)
$ cargo run body poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep body poem.txt`I’m nobody! Who are you?Are you nobody, too?How dreary to be somebody!
最后,让我们确保在搜索诗歌中没有出现的单词时不会出现任何行,例如“ monomorphization”:
$ cargo run monomorphization poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep monomorphization poem.txt`
优秀的!我们构建了自己的经典工具迷你版,并学到了很多有关如何构建应用程序的知识。我们还学到了一些有关文件输入和输出,生存期,测试和命令行解析的知识。
为了完善该项目,我们将简要演示如何使用环境变量以及如何将其打印为标准错误,这在编写命令行程序时都非常有用。