You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trpl-zh-cn/src/ch12-04-testing-the-library...

265 lines
11 KiB

## 采用测试驱动开发完善库的功能
8 years ago
6 years ago
> [ch12-04-testing-the-librarys-functionality.md](https://github.com/rust-lang/book/blob/master/src/ch12-04-testing-the-librarys-functionality.md)
8 years ago
> <br>
5 years ago
> commit 0ca4b88f75f8579de87adc2ad36d340709f5ccad
8 years ago
7 years ago
现在我们将逻辑提取到了 *src/lib.rs* 并将所有的参数解析和错误处理留在了 *src/main.rs* 中,为代码的核心功能编写测试将更加容易。我们可以直接使用多种参数调用函数并检查返回值而无需从命令行运行二进制文件了。如果你愿意的话,请自行为 `Config::new``run` 函数的功能编写一些测试。
8 years ago
7 years ago
在这一部分我们将遵循测试驱动开发Test Driven Development, TDD的模式来逐步增加 `minigrep` 的搜索逻辑。这是一个软件开发技术,它遵循如下步骤:
8 years ago
8 years ago
1. 编写一个会失败的测试,并运行它以确保其因为你期望的原因失败。
2. 编写或修改刚好足够的代码来使得新的测试通过。
3. 重构刚刚增加或修改的代码,并确保测试仍然能通过。
7 years ago
4. 从步骤 1 开始重复!
8 years ago
7 years ago
这只是众多编写软件的方法之一,不过 TDD 有助于驱动代码的设计。在编写能使测试通过的代码之前编写测试有助于在开发过程中保持高测试覆盖率。
8 years ago
我们将测试驱动实现实际在文件内容中搜索查询字符串并返回匹配的行示例的功能。我们将在一个叫做 `search` 的函数中增加这些功能。
8 years ago
8 years ago
### 编写失败测试
8 years ago
5 years ago
去掉 *src/lib.rs**src/main.rs* 中用于检查程序行为的 `println!` 语句,因为不再真正需要他们了。接着我们会像 [第十一章][ch11-anatomy] 那样增加一个 `test` 模块和一个测试函数。测试函数指定了 `search` 函数期望拥有的行为:它会获取一个需要查询的字符串和用来查询的文本,并只会返回包含请求的文本行。示例 12-15 展示了这个测试,它还不能编译:
8 years ago
7 years ago
<span class="filename">文件名: src/lib.rs</span>
8 years ago
8 years ago
```rust
5 years ago
# pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
8 years ago
# vec![]
# }
#
8 years ago
#[cfg(test)]
6 years ago
mod tests {
8 years ago
use super::*;
8 years ago
#[test]
fn one_result() {
8 years ago
let query = "duct";
8 years ago
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(
vec!["safe, fast, productive."],
8 years ago
search(query, contents)
8 years ago
);
}
}
```
<span class="caption">示例 12-15创建一个我们期望的 `search` 函数的失败测试</span>
8 years ago
6 years ago
这里选择使用 `"duct"` 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含 `"duct"`。我们断言 `search` 函数的返回值只包含期望的那一行。
8 years ago
6 years ago
我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译:`search` 函数还不存在呢!我们将增加足够的代码来使其能够编译:一个总是会返回空 vector 的 `search` 函数定义,如示例 12-16 所示。然后这个测试应该能够编译并因为空 vector 并不匹配一个包含一行 `"safe, fast, productive."` 的 vector 而失败。
8 years ago
7 years ago
<span class="filename">文件名: src/lib.rs</span>
8 years ago
7 years ago
```rust
5 years ago
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
7 years ago
vec![]
8 years ago
}
```
<span class="caption">示例 12-16刚好足够使测试通过编译的 `search` 函数定义</span>
8 years ago
5 years ago
注意需要在 `search` 的签名中定义一个显式生命周期 `'a` 并用于 `contents` 参数和返回值。回忆一下 [第十章][ch10-lifetimes] 中讲到生命周期参数指定哪个参数的生命周期与返回值的生命周期相关联。在这个例子中,我们表明返回的 vector 中应该包含引用参数 `contents`(而不是参数`query` slice 的字符串 slice。
8 years ago
7 years ago
换句话说,我们告诉 Rust 函数 `search` 返回的数据将与 `search` 函数中的参数 `contents` 的数据存在的一样久。这是非常重要的!为了使这个引用有效那么 **被** slice 引用的数据也需要保持有效;如果编译器认为我们是在创建 `query` 而不是 `contents` 的字符串 slice那么安全检查将是不正确的。
8 years ago
如果尝试不用生命周期编译的话,我们将得到如下错误:
8 years ago
7 years ago
```text
8 years ago
error[E0106]: missing lifetime specifier
7 years ago
--> src/lib.rs:5:51
8 years ago
|
5 years ago
5 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
7 years ago
| ^ expected lifetime
parameter
8 years ago
|
= help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `query` or `contents`
8 years ago
```
7 years ago
Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数 `contents` 包含了所有的文本而且我们希望返回匹配的那部分文本,所以我们知道 `contents` 是应该要使用生命周期语法来与返回值相关联的参数。
8 years ago
5 years ago
其他语言中并不需要你在函数签名中将参数与返回值相关联。所以这么做可能仍然感觉有些陌生,随着时间的推移这将会变得越来越容易。你可能想要将这个例子与第十章中生命 [“生命周期与引用有效性”][validating-references-with-lifetimes] 部分做对比。
8 years ago
7 years ago
现在运行测试:
8 years ago
7 years ago
```text
8 years ago
$ cargo test
7 years ago
Compiling minigrep v0.1.0 (file:///projects/minigrep)
--warnings--
7 years ago
Finished dev [unoptimized + debuginfo] target(s) in 0.43 secs
Running target/debug/deps/minigrep-abcabcabc
8 years ago
running 1 test
6 years ago
test tests::one_result ... FAILED
8 years ago
failures:
6 years ago
---- tests::one_result stdout ----
thread 'tests::one_result' panicked at 'assertion failed: `(left ==
7 years ago
right)`
left: `["safe, fast, productive."]`,
right: `[]`)', src/lib.rs:48:8
8 years ago
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
6 years ago
tests::one_result
8 years ago
7 years ago
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
8 years ago
7 years ago
error: test failed, to rerun pass '--lib'
8 years ago
```
8 years ago
好的,测试失败了,这正是我们所期望的。修改代码来让测试通过吧!
### 编写使测试通过的代码
7 years ago
目前测试之所以会失败是因为我们总是返回一个空的 vector。为了修复并实现 `search`,我们的程序需要遵循如下步骤:
8 years ago
7 years ago
* 遍历内容的每一行文本。
7 years ago
* 查看这一行是否包含要搜索的字符串。
7 years ago
* 如果有,将这一行加入列表返回值中。
7 years ago
* 如果没有,什么也不做。
7 years ago
* 返回匹配到的结果列表
8 years ago
8 years ago
让我们一步一步的来,从遍历每行开始。
7 years ago
#### 使用 `lines` 方法遍历每一行
8 years ago
7 years ago
Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被命名为 `lines`,它如示例 12-17 这样工作。注意这还不能编译:
8 years ago
7 years ago
<span class="filename">文件名: src/lib.rs</span>
8 years ago
```rust,ignore
5 years ago
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
8 years ago
for line in contents.lines() {
// do something with line
}
}
```
<span class="caption">示例 12-17遍历 `contents` 的每一行</span>
8 years ago
5 years ago
`lines` 方法返回一个迭代器。[第十三章][ch13-iterators] 会深入了解迭代器,不过我们已经在 [示例 3-5][ch3-iter] 中见过使用迭代器的方法了,在那里使用了一个 `for` 循环和迭代器在一个集合的每一项上运行了一些代码。
8 years ago
#### 用查询字符串搜索每一行
7 years ago
接下来将会增加检查当前行是否包含查询字符串的功能。幸运的是,字符串类型为此也有一个叫做 `contains` 的实用方法!如示例 12-18 所示在 `search` 函数中加入 `contains` 方法调用。注意这仍然不能编译:
8 years ago
7 years ago
<span class="filename">文件名: src/lib.rs</span>
8 years ago
```rust,ignore
5 years ago
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
8 years ago
for line in contents.lines() {
8 years ago
if line.contains(query) {
8 years ago
// do something with line
}
}
}
```
<span class="caption">示例 12-18增加检查文本行是否包含 `query` 中字符串的功能</span>
8 years ago
8 years ago
#### 存储匹配的行
7 years ago
我们还需要一个方法来存储包含查询字符串的行。为此可以在 `for` 循环之前创建一个可变的 vector 并调用 `push` 方法在 vector 中存放一个 `line`。在 `for` 循环之后,返回这个 vector如示例 12-19 所示:
8 years ago
7 years ago
<span class="filename">文件名: src/lib.rs</span>
8 years ago
8 years ago
```rust,ignore
5 years ago
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
8 years ago
let mut results = Vec::new();
for line in contents.lines() {
8 years ago
if line.contains(query) {
8 years ago
results.push(line);
}
}
results
}
```
<span class="caption">示例 12-19储存匹配的行以便可以返回他们</span>
8 years ago
7 years ago
现在 `search` 函数应该返回只包含 `query` 的那些行,而测试应该会通过。让我们运行测试:
8 years ago
7 years ago
```text
8 years ago
$ cargo test
7 years ago
--snip--
8 years ago
running 1 test
6 years ago
test tests::one_result ... ok
8 years ago
7 years ago
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
8 years ago
```
7 years ago
测试通过了,它可以工作了!
8 years ago
7 years ago
到此为止,我们可以考虑一下重构 `search` 的实现并时刻保持测试通过来保持其功能不变的机会了。`search` 函数中的代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并深入探索迭代器并看看如何改进代码。
8 years ago
7 years ago
#### 在 `run` 函数中使用 `search` 函数
8 years ago
7 years ago
现在 `search` 函数是可以工作并测试通过了的,我们需要实际在 `run` 函数中调用 `search`。需要将 `config.query` 值和 `run` 从文件中读取的 `contents` 传递给 `search` 函数。接着 `run` 会打印出 `search` 返回的每一行:
8 years ago
7 years ago
<span class="filename">文件名: src/lib.rs</span>
8 years ago
```rust,ignore
6 years ago
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.filename)?;
8 years ago
8 years ago
for line in search(&config.query, &contents) {
8 years ago
println!("{}", line);
}
Ok(())
}
```
7 years ago
这里仍然使用了 `for` 循环获取了 `search` 返回的每一行并打印出来。
8 years ago
7 years ago
现在整个程序应该可以工作了!让我们试一试,首先使用一个只会在艾米莉·狄金森的诗中返回一行的单词 “frog”
8 years ago
7 years ago
```text
8 years ago
$ cargo run frog poem.txt
7 years ago
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.38 secs
Running `target/debug/minigrep frog poem.txt`
8 years ago
How public, like a frog
```
8 years ago
7 years ago
好的!现在试试一个会匹配多行的单词,比如 “body”
8 years ago
7 years ago
```text
7 years ago
$ cargo run body poem.txt
7 years ago
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
7 years ago
Running `target/debug/minigrep body poem.txt`
Im nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!
8 years ago
```
8 years ago
最后,让我们确保搜索一个在诗中哪里都没有的单词时不会得到任何行,比如 "monomorphization"
7 years ago
```text
8 years ago
$ cargo run monomorphization poem.txt
7 years ago
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep monomorphization poem.txt`
8 years ago
```
7 years ago
非常好!我们创建了一个属于自己的迷你版经典工具,并学习了很多如何组织程序的知识。我们还学习了一些文件输入输出、生命周期、测试和命令行解析的内容。
8 years ago
7 years ago
为了使这个项目更丰满,我们将简要的展示如何处理环境变量和打印到标准错误,这两者在编写命令行程序时都很有用。
5 years ago
[validating-references-with-lifetimes]:
ch10-03-lifetime-syntax.html#validating-references-with-lifetimes
[ch11-anatomy]: ch11-01-writing-tests.html#the-anatomy-of-a-test-function
[ch10-lifetimes]: ch10-03-lifetime-syntax.html
[ch3-iter]: ch03-05-control-flow.html#looping-through-a-collection-with-for
[ch13-iterators]: ch13-02-iterators.html