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


x-i18n: generated_at: “2026-03-01T14:16:54Z” model: gemini-3-flash-preview provider: google-gemini-cli source_hash: 82cdffa0023c7c3a3a8a8f5658cf305e09f123c1e5b08713d1a19ebda80a3582 source_path: ch12-04-testing-the-librarys-functionality.md workflow: 16

使用测试驱动开发添加功能 (Adding Functionality with Test-Driven Development)

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. 编写或修改刚好足够让新测试通过的代码。

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

  4. 从第 1 步重复!

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

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

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

  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)

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.

{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-15/src/lib.rs:here}}

此测试搜索字符串 "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” 消息的恐慌。根据 TDD 原则,我们将采取一小步,通过将 search 函数定义为始终返回一个空向量,来添加刚好足够的代码使调用该函数时测试不引发恐慌,如示例 12-16 所示。然后,测试应该能够编译,并且由于空向量不匹配包含行 "safe, fast, productive." 的向量而失败。

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

{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-16/src/lib.rs:here}}

现在让我们讨论为什么我们需要在 search 的签名中定义显式生命周期 'a ,并将该生命周期用于 contents 参数和返回值。回想第 10 章,生命周期参数指定哪个参数的生命周期与返回值的生命周期相连。在这种情况下,我们指出返回的向量应包含引用参数 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:

{{#include ../listings/ch12-an-io-project/output-only-02-missing-lifetimes/output.txt}}

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)

Writing Code to Pass the Test

目前,我们的测试失败是因为我们始终返回一个空向量。为了修复该问题并实现 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. 检查该行是否包含我们的查询字符串。

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

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

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

  6. Iterate through each line of the contents.

  7. Check whether the line contains our query string.

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

  9. If it doesn’t, do nothing.

  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)

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.

{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-17/src/lib.rs:here}}

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)

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.

{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-18/src/lib.rs:here}}

目前,我们正在构建功能。为了让代码通过编译,我们需要像函数签名中所指示的那样,从函数体返回一个值。

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)

Storing Matching Lines

为了完成此函数,我们需要一种方法来存储我们要返回的匹配行。为此,我们可以在 for 循环之前创建一个可变向量,并调用 push 方法将 line 存储在向量中。在 for 循环之后,我们返回该向量,如示例 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.

{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-19/src/lib.rs:here}}

现在 search 函数应该只返回包含 query 的行,并且我们的测试应该通过。让我们运行测试:

Now the search function should return only the lines that contain query, and our test should pass. Let’s run the test:

{{#include ../listings/ch12-an-io-project/listing-12-19/output.txt}}

我们的测试通过了,所以我们知道它有效!

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.

现在整个程序应该可以工作了!让我们尝试一下,首先用一个应该从 Emily Dickinson 的诗中返回恰好一行的单词: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.

{{#include ../listings/ch12-an-io-project/no-listing-02-using-search-in-run/output.txt}}

酷!现在让我们尝试一个匹配多行的单词,比如 body

Cool! Now let’s try a word that will match multiple lines, like body:

{{#include ../listings/ch12-an-io-project/output-only-03-multiple-matches/output.txt}}

最后,让我们确保在搜索诗中任何地方都没有的单词(例如 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:

{{#include ../listings/ch12-an-io-project/output-only-04-no-matches/output.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.