Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

使用测试驱动开发添加功能

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:

  1. 编写一个失败的测试,并运行它以确保它因为你预期的原因而失败。

  2. Write a test that fails and run it to make sure it fails for the reason you expect.

  3. 编写或修改刚好足够的代码来使新测试通过。

  4. Write or modify just enough code to make the new test pass.

  5. 重构你刚才添加或更改的代码,并确保测试继续通过。

  6. Refactor the code you just added or changed and make sure the tests continue to pass.

  7. 从第 1 步开始重复!

  8. 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:

  1. 遍历内容的每一行。

  2. Iterate through each line of the contents.

  3. 检查该行是否包含我们的查询字符串。

  4. Check whether the line contains our query string.

  5. 如果包含,将其添加到我们要返回的值列表中。

  6. If it does, add it to the list of values we’re returning.

  7. 如果不包含,什么都不做。

  8. If it doesn’t, do nothing.

  9. 返回匹配的结果列表。

  10. 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.