使用测试驱动开发添加功能
Adding Functionality with Test-Driven Development
既然我们将 src/lib.rs 中的搜索逻辑与 main 函数分离开了,那么为代码的核心功能编写测试就变得容易得多。我们可以直接使用各种参数调用函数并检查返回值,而不必从命令行调用二进制文件。
Now that we have the search logic in src/lib.rs separate from the main
function, it’s much easier to write tests for the core functionality of our
code. We can call functions directly with various arguments and check return
values without having to call our binary from the command line.
在本节中,我们将使用测试驱动开发 (TDD) 过程将搜索逻辑添加到 minigrep 程序中,该过程包含以下步骤:
In this section, we’ll add the searching logic to the minigrep program using
the test-driven development (TDD) process with the following steps:
-
编写一个失败的测试,并运行它以确保它因为你预期的原因而失败。
-
Write a test that fails and run it to make sure it fails for the reason you expect.
-
编写或修改刚好足够的代码来使新测试通过。
-
Write or modify just enough code to make the new test pass.
-
重构你刚才添加或更改的代码,并确保测试继续通过。
-
Refactor the code you just added or changed and make sure the tests continue to pass.
-
从第 1 步开始重复!
-
Repeat from step 1!
虽然这只是编写软件的众多方法之一,但 TDD 可以帮助驱动代码设计。在编写使测试通过的代码之前编写测试,有助于在整个过程中保持高测试覆盖率。
Though it’s just one of many ways to write software, TDD can help drive code design. Writing the test before you write the code that makes the test pass |helps maintain high test coverage throughout the process.
我们将通过测试驱动来实现实际在文件内容中搜索查询字符串并生成匹配行列表的功能。我们将在一个名为 search 的函数中添加此功能。
We’ll test-drive the implementation of the functionality that will actually do
the searching for the query string in the file contents and produce a list of
lines that match the query. We’ll add this functionality in a function called
search.
编写一个失败的测试
Writing a Failing Test
在 src/lib.rs 中,我们将添加一个带有测试函数的 tests 模块,就像我们在第 11 章中所做的那样。该测试函数指定了我们希望 search 函数具有的行为:它将接收一个查询字符串和要搜索的文本,并仅返回文本中包含查询字符串的行。示例 12-15 展示了这个测试。
In src/lib.rs, we’ll add a tests module with a test function, as we did in
Chapter 11. The test function specifies the
behavior we want the search function to have: It will take a query and the
text to search, and it will return only the lines from the text that contain
the query. Listing 12-15 shows this test.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
unimplemented!();
}
// --snip--
#[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));
}
}
此测试搜索字符串 "duct"。我们正在搜索的文本有三行,其中只有一行包含 "duct"(请注意,起始双引号后的反斜杠告诉 Rust 不要在该字符串字面量的开头放置换行符)。我们断言 search 函数返回的值仅包含我们预期的行。
This test searches for the string "duct". The text we’re searching is three
lines, only one of which contains "duct" (note that the backslash after the
opening double quote tells Rust not to put a newline character at the beginning
of the contents of this string literal). We assert that the value returned from
the search function contains only the line we expect.
如果我们现在运行此测试,它将失败,因为 unimplemented! 宏会以“not implemented”消息引发 panic。根据 TDD 原则,我们将迈出一小步,通过将 search 函数定义为始终返回空 vector,添加刚好足够的代码,使调用该函数时不会引发 panic,如示例 12-16 所示。然后,测试应该能够编译并失败,因为空 vector 不匹配包含 "safe, fast, productive." 行的 vector。
If we run this test, it will currently fail because the unimplemented! macro
panics with the message “not implemented”. In accordance with TDD principles,
we’ll take a small step of adding just enough code to get the test to not panic
when calling the function by defining the search function to always return an
empty vector, as shown in Listing 12-16. Then, the test should compile and fail
because an empty vector doesn’t match a vector containing the line "safe, fast, productive.".
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 的签名中定义一个显式的生命周期 'a,并对 contents 参数和返回值使用该生命周期。回顾第 10 章,生命周期参数指定了哪个参数生命周期与返回值的生命周期相关联。在这种情况下,我们指明返回的 vector 应该包含引用参数 contents(而不是参数 query)切片的字符串切片。
Now let’s discuss why we need to define an explicit lifetime 'a in the
signature of search and use that lifetime with the contents argument and
the return value. Recall in Chapter 10 that
the lifetime parameters specify which argument lifetime is connected to the
lifetime of the return value. In this case, we indicate that the returned
vector should contain string slices that reference slices of the argument
contents (rather than the argument query).
换句话说,我们告诉 Rust,search 函数返回的数据将与通过 contents 参数传递到 search 函数的数据存活时间一样长。这很重要!切片引用的数据需要有效,引用才有效;如果编译器假设我们正在创建 query 而不是 contents 的字符串切片,它将进行错误的安全性检查。
In other words, we tell Rust that the data returned by the search function
will live as long as the data passed into the search function in the
contents argument. This is important! The data referenced by a slice needs
to be valid for the reference to be valid; if the compiler assumes we’re making
string slices of query rather than contents, it will do its safety checking
incorrectly.
如果我们忘记了生命周期标注并尝试编译此函数,我们将得到此错误:
If we forget the lifetime annotations and try to compile this function, we’ll get this error:
$ cargo build
Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
--> src/lib.rs:1:51
|
1 | 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
|
1 | 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` (lib) due to 1 previous error
Rust 无法知道输出需要两个参数中的哪一个,因此我们需要明确地告诉它。请注意,帮助文本建议为所有参数和输出类型指定相同的生命周期参数,这是不正确的!因为 contents 是包含我们所有文本的参数,并且我们希望返回该文本中匹配的部分,所以我们知道 contents 是唯一应该使用生命周期语法与返回值连接的参数。
Rust can’t know which of the two parameters we need for the output, so we need
to tell it explicitly. Note that the help text suggests specifying the same
lifetime parameter for all the parameters and the output type, which is
incorrect! Because contents is the parameter that contains all of our text
and we want to return the parts of that text that match, we know contents is
the only parameter that should be connected to the return value using the
lifetime syntax.
其他编程语言不要求你在签名中将参数与返回值连接起来,但随着时间的推移,这种做法会变得越来越容易。你可能希望将此示例与第 10 章中的“使用生命周期验证引用”一节中的示例进行比较。
Other programming languages don’t require you to connect arguments to return values in the signature, but this practice will get easier over time. You might want to compare this example with the examples in the “Validating References with Lifetimes” section in Chapter 10.
编写代码使测试通过
Writing Code to Pass the Test
目前,我们的测试失败了,因为 we 总是返回一个空 vector。为了修复该问题并实现 search,我们的程序需要遵循以下步骤:
Currently, our test is failing because we always return an empty vector. To fix
that and implement search, our program needs to follow these steps:
-
遍历内容的每一行。
-
Iterate through each line of the contents.
-
检查该行是否包含我们的查询字符串。
-
Check whether the line contains our query string.
-
如果包含,将其添加到我们要返回的值列表中。
-
If it does, add it to the list of values we’re returning.
-
如果不包含,什么都不做。
-
If it doesn’t, do nothing.
-
返回匹配的结果列表。
-
Return the list of results that match.
让我们逐步完成每个步骤,从遍历行开始。
Let’s work through each step, starting with iterating through lines.
使用 lines 方法遍历行
Iterating Through Lines with the lines Method
Rust 有一个方便的方法来处理字符串的逐行遍历,它的名字很贴切,叫作 lines,它的工作方式如示例 12-17 所示。请注意,这还不能编译。
Rust has a helpful method to handle line-by-line iteration of strings,
conveniently named lines, that works as shown in Listing 12-17. Note that
this won’t compile yet.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
// do something with line
}
}
#[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));
}
}
lines 方法返回一个迭代器。我们将在第 13 章中深入讨论迭代器。但回想一下,你在示例 3-5中看到过这种使用迭代器的方法,我们在那里使用 for 循环和迭代器对集合中的每个项目运行一些代码。
The lines method returns an iterator. We’ll talk about iterators in depth in
Chapter 13. But recall that you saw this way
of using an iterator in Listing 3-5, where we used a
for loop with an iterator to run some code on each item in a collection.
在每一行中搜索查询字符串
Searching Each Line for the Query
接下来,我们将检查当前行是否包含我们的查询字符串。幸运的是,字符串有一个名为 contains 的好方法可以为我们完成这项工作!在 search 函数中添加对 contains 方法的调用,如示例 12-18 所示。请注意,这仍然无法编译。
Next, we’ll check whether the current line contains our query string.
Fortunately, strings have a helpful method named contains that does this for
us! Add a call to the contains method in the search function, as shown in
Listing 12-18. Note that this still won’t compile yet.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
if line.contains(query) {
// do something with line
}
}
}
#[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));
}
}
目前,我们正在构建功能。为了使代码能够编译,我们需要从函数体中返回一个值,正如我们在函数签名中所指出的那样。
At the moment, we’re building up functionality. To get the code to compile, we need to return a value from the body as we indicated we would in the function signature.
存储匹配行
Storing Matching Lines
为了完成这个函数,我们需要一种方法来存储我们想要返回的匹配行。为此,我们可以在 for 循环之前创建一个可变 vector,并调用 push 方法将 line 存储在 vector 中。在 for 循环之后,我们返回该 vector,如示例 12-19 所示。
To finish this function, we need a way to store the matching lines that we want
to return. For that, we can make a mutable vector before the for loop and
call the push method to store a line in the vector. After the for loop,
we return the vector, as shown in Listing 12-19.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
#[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 函数应该只返回包含 query 的行,并且我们的测试应该通过。让我们运行测试:
Now the search function should return only the lines that contain query,
and our test should pass. Let’s run the test:
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.22s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 1 test
test tests::one_result ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
我们的测试通过了,所以我们知道它有效!
Our test passed, so we know it works!
在这一点上,我们可以考虑重构搜索函数的实现机会,同时保持测试通过以维持相同的功能。搜索函数中的代码还算不错,但它没有利用迭代器的一些有用特性。我们将在第 13 章中回到这个例子,届时我们将详细探索迭代器,并看看如何改进它。
At this point, we could consider opportunities for refactoring the implementation of the search function while keeping the tests passing to maintain the same functionality. The code in the search function isn’t too bad, but it doesn’t take advantage of some useful features of iterators. We’ll return to this example in Chapter 13, where we’ll explore iterators in detail, and look at how to improve it.
现在整个程序应该可以工作了!让我们试一试,首先使用一个应该从艾米莉·狄金森的诗中准确返回一行的单词:frog。
Now the entire program should work! Let’s try it out, first with a word that should return exactly one line from the Emily Dickinson poem: frog.
$ cargo run -- frog poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
Running `target/debug/minigrep frog poem.txt`
How public, like a frog
酷!现在让我们尝试一个会匹配多行的单词,比如 body:
Cool! Now let’s try a word that will match multiple lines, like body:
$ cargo run -- body poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [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)时,不会得到任何行:
And finally, let’s make sure that we don’t get any lines when we search for a word that isn’t anywhere in the poem, such as monomorphization:
$ cargo run -- monomorphization poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep monomorphization poem.txt`
太棒了!我们构建了自己迷你版本的经典工具,并学到了很多关于如何构建应用程序的知识。我们还了解了一些关于文件输入和输出、生命周期、测试和命令行解析的知识。
Excellent! We’ve built our own mini version of a classic tool and learned a lot about how to structure applications. We’ve also learned a bit about file input and output, lifetimes, testing, and command line parsing.
为了完善这个项目,我们将简要演示如何处理环境变量以及如何打印到标准错误,这两者在编写命令行程序时都很有用。
To round out this project, we’ll briefly demonstrate how to work with environment variables and how to print to standard error, both of which are useful when you’re writing command line programs.