x-i18n: generated_at: “2026-03-01T14:11:58Z” model: gemini-3-flash-preview provider: google-gemini-cli source_hash: b6131ddfce33ee37534fde3116a1bc7dd191747a645e2a0272faf7c18a9f8942 source_path: ch11-03-test-organization.md workflow: 16
测试组织 (Test Organization)
Test Organization
正如本章开头所述,测试是一门复杂的学科,不同的人使用不同的术语和组织方式。Rust 社区将测试分为两大类:单元测试和集成测试。“单元测试 (Unit tests)”规模较小且更集中,一次隔离地测试一个模块,并且可以测试私有接口。“集成测试 (Integration tests)”则完全位于你的库之外,它们以与其他外部代码相同的方式使用你的代码,仅使用公有接口,并且每个测试可能行使多个模块的功能。
As mentioned at the start of the chapter, testing is a complex discipline, and different people use different terminology and organization. The Rust community thinks about tests in terms of two main categories: unit tests and integration tests. Unit tests are small and more focused, testing one module in isolation at a time, and can test private interfaces. Integration tests are entirely external to your library and use your code in the same way any other external code would, using only the public interface and potentially exercising multiple modules per test.
编写这两类测试对于确保你的库的各个部分分别及共同按预期工作非常重要。
Writing both kinds of tests is important to ensure that the pieces of your library are doing what you expect them to, separately and together.
单元测试 (Unit Tests)
Unit Tests
单元测试的目的是将每个代码单元与其余代码隔离,以便快速定位代码在哪些地方运行正常或不正常。你会将单元测试放在 src 目录下的每个文件中,并与它们正在测试的代码放在一起。惯例是在每个文件中创建一个名为 tests 的模块来包含测试函数,并为该模块标注 cfg(test)。
The purpose of unit tests is to test each unit of code in isolation from the
rest of the code to quickly pinpoint where code is and isn’t working as
expected. You’ll put unit tests in the src directory in each file with the
code that they’re testing. The convention is to create a module named tests
in each file to contain the test functions and to annotate the module with
cfg(test).
tests 模块与 #[cfg(test)] (The tests Module and #[cfg(test)])
The tests Module and #[cfg(test)]
tests 模块上的 #[cfg(test)] 标注告诉 Rust 仅当你运行 cargo test 时才编译和运行测试代码,而不是运行 cargo build 时。当你只想构建库时,这可以节省编译时间,并由于未包含测试而节省生成的编译产物的空间。你会看到集成测试位于不同的目录中,因此它们不需要 #[cfg(test)] 标注。然而,由于单元测试与代码位于相同的文件中,你将使用 #[cfg(test)] 来指定它们不应包含在编译结果中。
The #[cfg(test)] annotation on the tests module tells Rust to compile and
run the test code only when you run cargo test, not when you run cargo build. This saves compile time when you only want to build the library and
saves space in the resultant compiled artifact because the tests are not
included. You’ll see that because integration tests go in a different
directory, they don’t need the #[cfg(test)] annotation. However, because unit
tests go in the same files as the code, you’ll use #[cfg(test)] to specify
that they shouldn’t be included in the compiled result.
回想本章第一节我们生成新的 adder 项目时,Cargo 为我们生成的代码:
Recall that when we generated the new adder project in the first section of
this chapter, Cargo generated this code for us:
文件名: src/lib.rs
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-01/src/lib.rs}}
在自动生成的 tests 模块上,属性 cfg 代表“配置 (configuration)”,它告诉 Rust 仅在给定特定配置选项时才包含以下项。在这种情况下,配置选项是 test,这是由 Rust 提供的用于编译和运行测试的选项。通过使用 cfg 属性,Cargo 仅在我们主动使用 cargo test 运行测试时才会编译我们的测试代码。这包括此模块内可能存在的任何辅助函数,以及标注了 #[test] 的函数。
On the automatically generated tests module, the attribute cfg stands for
configuration and tells Rust that the following item should only be included
given a certain configuration option. In this case, the configuration option is
test, which is provided by Rust for compiling and running tests. By using the
cfg attribute, Cargo compiles our test code only if we actively run the tests
with cargo test. This includes any helper functions that might be within this
module, in addition to the functions annotated with #[test].
私有函数测试 (Private Function Tests)
Private Function Tests
在测试界对于是否应该直接测试私有函数存在争论,其他语言使得测试私有函数变得困难或不可能。无论你坚持哪种测试意识形态,Rust 的私有性规则都允许你测试私有函数。考虑示例 11-12 中带有私有函数 internal_adder 的代码。
There’s debate within the testing community about whether or not private
functions should be tested directly, and other languages make it difficult or
impossible to test private functions. Regardless of which testing ideology you
adhere to, Rust’s privacy rules do allow you to test private functions.
Consider the code in Listing 11-12 with the private function internal_adder.
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-12/src/lib.rs}}
请注意,internal_adder 函数未被标记为 pub。测试仅仅是 Rust 代码,而 tests 模块也只是另一个模块。正如我们在“在模块树中引用项的路径”中讨论的那样,子模块中的项可以使用其祖先模块中的项。在这个测试中,我们使用 use super::* 将属于 tests 模块父模块的所有项引入作用域,然后测试就可以调用 internal_adder 了。如果你认为不应该测试私有函数,Rust 中没有任何机制会强迫你这样做。
Note that the internal_adder function is not marked as pub. Tests are just
Rust code, and the tests module is just another module. As we discussed in
“Paths for Referring to an Item in the Module Tree”,
items in child modules can use the items in their ancestor modules. In this
test, we bring all of the items belonging to the tests module’s parent into
scope with use super::*, and then the test can call internal_adder. If you
don’t think private functions should be tested, there’s nothing in Rust that
will compel you to do so.
集成测试 (Integration Tests)
Integration Tests
在 Rust 中,集成测试完全位于你的库之外。它们以与任何其他代码相同的方式使用你的库,这意味着它们只能调用作为你库公有 API 一部分的函数。它们的目的是测试你的库的许多部分是否能正确地协同工作。单独工作正常的代码单元在集成时可能会出现问题,因此集成代码的测试覆盖率也非常重要。要创建集成测试,你首先需要一个 tests 目录。
In Rust, integration tests are entirely external to your library. They use your library in the same way any other code would, which means they can only call functions that are part of your library’s public API. Their purpose is to test whether many parts of your library work together correctly. Units of code that work correctly on their own could have problems when integrated, so test coverage of the integrated code is important as well. To create integration tests, you first need a tests directory.
tests 目录 (The tests Directory)
The tests Directory
我们在项目目录的顶层,即 src 旁边创建一个 tests 目录。Cargo 知道在这个目录中查找集成测试文件。然后我们可以创建任意数量的测试文件,Cargo 会将每个文件编译为一个独立的 crate。
We create a tests directory at the top level of our project directory, next to src. Cargo knows to look for integration test files in this directory. We can then make as many test files as we want, and Cargo will compile each of the files as an individual crate.
让我们创建一个集成测试。保持示例 11-12 中的代码仍在 src/lib.rs 文件中,创建一个 tests 目录,并创建一个名为 tests/integration_test.rs 的新文件。你的目录结构应该像这样:
Let’s create an integration test. With the code in Listing 11-12 still in the src/lib.rs file, make a tests directory, and create a new file named tests/integration_test.rs. Your directory structure should look like this:
adder
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
└── integration_test.rs
将示例 11-13 中的代码输入到 tests/integration_test.rs 文件中。
Enter the code in Listing 11-13 into the tests/integration_test.rs file.
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-13/tests/integration_test.rs}}
tests 目录中的每个文件都是一个独立的 crate,所以我们需要将我们的库引入每个测试 crate 的作用域。因此,我们在代码顶部添加了 use adder::add_two; ,这在单元测试中是不需要的。
Each file in the tests directory is a separate crate, so we need to bring our
library into each test crate’s scope. For that reason, we add use adder::add_two; at the top of the code, which we didn’t need in the unit tests.
我们不需要为 tests/integration_test.rs 中的任何代码标注 #[cfg(test)] 。Cargo 对 tests 目录进行了特殊处理,仅当我们运行 cargo test 时才编译该目录下的文件。现在运行 cargo test :
We don’t need to annotate any code in tests/integration_test.rs with
#[cfg(test)]. Cargo treats the tests directory specially and compiles files
in this directory only when we run cargo test. Run cargo test now:
{{#include ../listings/ch11-writing-automated-tests/listing-11-13/output.txt}}
输出的三个部分包括单元测试、集成测试和文档测试。请注意,如果一个部分中的任何测试失败,接下来的部分将不会运行。例如,如果单元测试失败,集成测试和文档测试将不会有任何输出,因为只有在所有单元测试都通过时才会运行这些测试。
The three sections of output include the unit tests, the integration test, and the doc tests. Note that if any test in a section fails, the following sections will not be run. For example, if a unit test fails, there won’t be any output for integration and doc tests, because those tests will only be run if all unit tests are passing.
关于单元测试的第一部分与我们一直看到的一样:每个单元测试一行(示例 11-12 中添加的一个名为 internal 的测试),然后是单元测试的摘要行。
The first section for the unit tests is the same as we’ve been seeing: one line
for each unit test (one named internal that we added in Listing 11-12) and
then a summary line for the unit tests.
集成测试部分以行 Running tests/integration_test.rs 开始。接下来,在该集成测试中的每个测试函数都有一行,以及在 Doc-tests adder 部分开始之前显示的集成测试结果摘要行。
The integration tests section starts with the line Running tests/integration_test.rs. Next, there is a line for each test function in that integration test and a summary line for the results of the integration test just before the Doc-tests adder section starts.
每个集成测试文件都有自己的部分,所以如果我们在 tests 目录中添加更多文件,就会有更多的集成测试部分。
Each integration test file has its own section, so if we add more files in the tests directory, there will be more integration test sections.
我们仍然可以通过指定测试函数的名称作为 cargo test 的参数来运行特定的集成测试函数。要运行特定集成测试文件中的所有测试,请使用 cargo test 的 --test 参数后跟文件名:
We can still run a particular integration test function by specifying the test
function’s name as an argument to cargo test. To run all the tests in a
particular integration test file, use the --test argument of cargo test
followed by the name of the file:
{{#include ../listings/ch11-writing-automated-tests/output-only-05-single-integration/output.txt}}
此命令仅运行 tests/integration_test.rs 文件中的测试。
This command runs only the tests in the tests/integration_test.rs file.
集成测试中的子模块 (Submodules in Integration Tests)
Submodules in Integration Tests
随着你添加更多的集成测试,你可能想在 tests 目录下创建更多文件来帮助组织它们;例如,你可以按测试的功能对测试函数进行分组。如前所述,tests 目录中的每个文件都会被编译为各自独立的 crate,这对于创建独立作用域以更贴近地模拟终端用户使用你的 crate 的方式很有用。然而,这意味着 tests 目录中的文件不共享你在第 7 章中学到的关于如何将代码拆分为模块和文件的 src 文件的相同行为。
As you add more integration tests, you might want to make more files in the tests directory to help organize them; for example, you can group the test functions by the functionality they’re testing. As mentioned earlier, each file in the tests directory is compiled as its own separate crate, which is useful for creating separate scopes to more closely imitate the way end users will be using your crate. However, this means files in the tests directory don’t share the same behavior as files in src do, as you learned in Chapter 7 regarding how to separate code into modules and files.
当你有一组要在多个集成测试文件中使用的辅助函数,并尝试遵循第 7 章“将模块拆分为不同的文件”部分中的步骤将它们提取到公共模块中时,tests 目录文件行为的不同最为明显。例如,如果我们创建了 tests/common.rs 并在其中放入了一个名为 setup 的函数,我们可以向 setup 添加一些我们想要从多个测试文件中的多个测试函数调用的代码:
The different behavior of tests directory files is most noticeable when you
have a set of helper functions to use in multiple integration test files, and
you try to follow the steps in the “Separating Modules into Different
Files” section of Chapter 7 to
extract them into a common module. For example, if we create tests/common.rs
and place a function named setup in it, we can add some code to setup that
we want to call from multiple test functions in multiple test files:
文件名: tests/common.rs
{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-12-shared-test-code-problem/tests/common.rs}}
当我们再次运行测试时,我们会看到测试输出中为 common.rs 文件新增了一个部分,即使该文件不包含任何测试函数,我们也没有从任何地方调用 setup 函数:
When we run the tests again, we’ll see a new section in the test output for the
common.rs file, even though this file doesn’t contain any test functions nor
did we call the setup function from anywhere:
{{#include ../listings/ch11-writing-automated-tests/no-listing-12-shared-test-code-problem/output.txt}}
让 common 出现在测试结果中并显示 running 0 tests 并不是我们想要的。我们只是想与其他集成测试文件共享一些代码。为了避免 common 出现在测试输出中,我们将不创建 tests/common.rs ,而是创建 tests/common/mod.rs 。项目目录现在看起来像这样:
Having common appear in the test results with running 0 tests displayed for
it is not what we wanted. We just wanted to share some code with the other
integration test files. To avoid having common appear in the test output,
instead of creating tests/common.rs, we’ll create tests/common/mod.rs. The
project directory now looks like this:
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
├── common
│ └── mod.rs
└── integration_test.rs
这是我们在第 7 章“备选文件路径”中提到的 Rust 也能理解的旧命名约定。以此方式命名文件告诉 Rust 不要将 common 模块视为集成测试文件。当我们将 setup 函数代码移动到 tests/common/mod.rs 并删除 tests/common.rs 文件时,测试输出中的该部分将不再出现。tests 目录子目录中的文件不会被编译为独立的 crate,也不会在测试输出中拥有单独的部分。
This is the older naming convention that Rust also understands that we mentioned
in “Alternate File Paths” in Chapter 7. Naming the
file this way tells Rust not to treat the common module as an integration test
file. When we move the setup function code into tests/common/mod.rs and
delete the tests/common.rs file, the section in the test output will no longer
appear. Files in subdirectories of the tests directory don’t get compiled as
separate crates or have sections in the test output.
创建 tests/common/mod.rs 后,我们就可以在任何集成测试文件中将其作为一个模块来使用。这里有一个从 tests/integration_test.rs 中的 it_adds_two 测试调用 setup 函数的例子:
After we’ve created tests/common/mod.rs, we can use it from any of the
integration test files as a module. Here’s an example of calling the setup
function from the it_adds_two test in tests/integration_test.rs:
文件名: tests/integration_test.rs
{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-13-fix-shared-test-code-problem/tests/integration_test.rs}}
请注意,mod common; 声明与我们在示例 7-21 中演示的模块声明相同。然后,在测试函数中,我们可以调用 common::setup() 函数。
Note that the mod common; declaration is the same as the module declaration
we demonstrated in Listing 7-21. Then, in the test function, we can call the
common::setup() function.
二进制 crate 的集成测试 (Integration Tests for Binary Crates)
Integration Tests for Binary Crates
如果我们的项目是一个仅包含 src/main.rs 文件而不包含 src/lib.rs 文件的二进制 crate,我们就无法在 tests 目录中创建集成测试,也无法使用 use 语句将 src/main.rs 文件中定义的函数引入作用域。只有库 crate 会公开其他 crate 可以使用的函数;二进制 crate 旨在独立运行。
If our project is a binary crate that only contains a src/main.rs file and
doesn’t have a src/lib.rs file, we can’t create integration tests in the
tests directory and bring functions defined in the src/main.rs file into
scope with a use statement. Only library crates expose functions that other
crates can use; binary crates are meant to be run on their own.
这也是为什么提供二进制文件的 Rust 项目通常都有一个简单的 src/main.rs 文件,用于调用位于 src/lib.rs 文件中的逻辑。使用这种结构,集成测试“可以”通过 use 来测试库 crate,从而使重要功能可用。如果重要功能正常工作,src/main.rs 文件中的少量代码也将正常工作,且该少量代码无需测试。
This is one of the reasons Rust projects that provide a binary have a
straightforward src/main.rs file that calls logic that lives in the
src/lib.rs file. Using that structure, integration tests can test the
library crate with use to make the important functionality available. If the
important functionality works, the small amount of code in the src/main.rs
file will work as well, and that small amount of code doesn’t need to be tested.
总结 (Summary)
Summary
Rust 的测试功能提供了一种指定代码应如何运行的方式,以确保即便你进行了更改,它仍能按你预期工作。单元测试分别行使库的不同部分,并可以测试私有实现细节。集成测试检查库的许多部分是否能正确地协同工作,并使用库的公有 API 以外部代码相同的方式测试代码。尽管 Rust 的类型系统和所有权规则有助于防止某些种类的 bug,测试对于减少与代码预期行为相关的逻辑 bug 仍然很重要。
Rust’s testing features provide a way to specify how code should function to ensure that it continues to work as you expect, even as you make changes. Unit tests exercise different parts of a library separately and can test private implementation details. Integration tests check that many parts of the library work together correctly, and they use the library’s public API to test the code in the same way external code will use it. Even though Rust’s type system and ownership rules help prevent some kinds of bugs, tests are still important to reduce logic bugs having to do with how your code is expected to behave.
让我们结合你在本章及前几章学到的知识来开展一个项目吧!
Let’s combine the knowledge you learned in this chapter and in previous chapters to work on a project!