x-i18n: generated_at: “2026-03-01T14:10:26Z” model: gemini-3-flash-preview provider: google-gemini-cli source_hash: 03cec10df505d25d37717447f906d392506c18f4af1ba7ef9cc32f05cfc15359 source_path: ch11-01-writing-tests.md workflow: 16
如何编写测试 (How to Write Tests)
How to Write Tests
“测试 (Tests)”是 Rust 函数,用于验证非测试代码是否按预期方式运行。测试函数的主体通常执行以下三个操作:
Tests are Rust functions that verify that the non-test code is functioning in the expected manner. The bodies of test functions typically perform these three actions:
-
设置所需的任何数据或状态。
-
运行你想要测试的代码。
-
断言结果是你所期望的。
-
Set up any needed data or state.
-
Run the code you want to test.
-
Assert that the results are what you expect.
让我们看看 Rust 专门为执行这些操作的测试提供的功能,包括 test 属性、几个宏以及 should_panic 属性。
Let’s look at the features Rust provides specifically for writing tests that
take these actions, which include the test attribute, a few macros, and the
should_panic attribute.
测试函数的结构 (Structuring Test Functions)
Structuring Test Functions
最简单的情况下,Rust 中的测试是一个标注了 test 属性的函数。属性(Attributes)是关于 Rust 代码片段的元数据;第 5 章中我们在结构体上使用的 derive 属性就是一个例子。要将函数更改为测试函数,请在 fn 之前的行中添加 #[test]。当你使用 cargo test 命令运行测试时,Rust 会构建一个测试运行器二进制文件,该文件会运行被标注的函数,并报告每个测试函数是成功还是失败。
At its simplest, a test in Rust is a function that’s annotated with the test
attribute. Attributes are metadata about pieces of Rust code; one example is
the derive attribute we used with structs in Chapter 5. To change a function
into a test function, add #[test] on the line before fn. When you run your
tests with the cargo test command, Rust builds a test runner binary that runs
the annotated functions and reports on whether each test function passes or
fails.
每当我们使用 Cargo 创建一个新的库项目时,都会自动为我们生成一个包含测试函数的测试模块。这个模块为你编写测试提供了一个模板,这样你就不用每次开始新项目时都要去查找确切的结构和语法。你可以根据需要添加任意数量的附加测试函数和测试模块!
Whenever we make a new library project with Cargo, a test module with a test function in it is automatically generated for us. This module gives you a template for writing your tests so that you don’t have to look up the exact structure and syntax every time you start a new project. You can add as many additional test functions and as many test modules as you want!
在实际测试任何代码之前,我们将通过试验模板测试来探索测试工作原理的某些方面。然后,我们将编写一些实际测试,调用我们编写的代码并断言其行为是否正确。
We’ll explore some aspects of how tests work by experimenting with the template test before we actually test any code. Then, we’ll write some real-world tests that call some code that we’ve written and assert that its behavior is correct.
让我们创建一个名为 adder 的新库项目,它将把两个数字相加:
Let’s create a new library project called adder that will add two numbers:
$ cargo new adder --lib
Created library `adder` project
$ cd adder
你的 adder 库中的 src/lib.rs 文件内容应该如示例 11-1 所示。
The contents of the src/lib.rs file in your adder library should look like
Listing 11-1.
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-01/src/lib.rs}}
文件以一个示例 add 函数开始,这样我们就有了可以测试的东西。
The file starts with an example add function so that we have something to
test.
现在,让我们仅关注 it_works 函数。请注意 #[test] 标注:此属性表明这是一个测试函数,因此测试运行器知道将此函数视为测试。在 tests 模块中,我们也可能有非测试函数来帮助设置常见场景或执行通用操作,因此我们总是需要指明哪些函数是测试。
For now, let’s focus solely on the it_works function. Note the #[test]
annotation: This attribute indicates this is a test function, so the test
runner knows to treat this function as a test. We might also have non-test
functions in the tests module to help set up common scenarios or perform
common operations, so we always need to indicate which functions are tests.
示例函数体使用 assert_eq! 宏来断言 result(包含调用 add 传入 2 和 2 的结果)等于 4。这个断言是典型测试格式的一个示例。让我们运行它,看看这个测试是否通过。
The example function body uses the assert_eq! macro to assert that result,
which contains the result of calling add with 2 and 2, equals 4. This
assertion serves as an example of the format for a typical test. Let’s run it
to see that this test passes.
cargo test 命令运行项目中所有的测试,如示例 11-2 所示。
The cargo test command runs all tests in our project, as shown in Listing
11-2.
{{#include ../listings/ch11-writing-automated-tests/listing-11-01/output.txt}}
Cargo 编译并运行了测试。我们看到行 running 1 test。下一行显示生成的测试函数的名称,称为 tests::it_works,以及运行该测试的结果为 ok。总体摘要 test result: ok. 意味着所有测试都通过了,而 1 passed; 0 failed 部分统计了通过或失败的测试数量。
Cargo compiled and ran the test. We see the line running 1 test. The next
line shows the name of the generated test function, called tests::it_works,
and that the result of running that test is ok. The overall summary test result: ok. means that all the tests passed, and the portion that reads 1 passed; 0 failed totals the number of tests that passed or failed.
可以将测试标记为被忽略,这样它在特定实例中就不会运行;我们将在本章稍后的“除非特别请求,否则忽略测试”部分进行介绍。因为我们在这里没有这样做,所以摘要显示 0 ignored。我们还可以向 cargo test 命令传递一个参数,以仅运行名称与字符串匹配的测试;这被称为“过滤 (filtering)”,我们将在“按名称运行测试子集”部分进行介绍。在这里,我们没有过滤正在运行的测试,因此摘要末尾显示 0 filtered out。
It’s possible to mark a test as ignored so that it doesn’t run in a particular
instance; we’ll cover that in the “Ignoring Tests Unless Specifically
Requested” section later in this chapter. Because we
haven’t done that here, the summary shows 0 ignored. We can also pass an
argument to the cargo test command to run only tests whose name matches a
string; this is called filtering, and we’ll cover it in the “Running a
Subset of Tests by Name” section. Here, we haven’t
filtered the tests being run, so the end of the summary shows 0 filtered out.
0 measured 统计数据用于衡量性能的基准测试(benchmark tests)。截至撰写本文时,基准测试仅在 nightly 版 Rust 中可用。请参阅关于基准测试的文档以了解更多信息。
The 0 measured statistic is for benchmark tests that measure performance.
Benchmark tests are, as of this writing, only available in nightly Rust. See
the documentation about benchmark tests to learn more.
测试输出中以 Doc-tests adder 开始的下一部分是针对任何文档测试(documentation tests)的结果。我们目前还没有任何文档测试,但 Rust 可以编译 API 文档中出现的任何代码示例。此功能有助于保持你的文档与代码同步!我们将在第 14 章的“作为测试的文档注释”部分讨论如何编写文档测试。现在,我们将忽略 Doc-tests 输出。
The next part of the test output starting at Doc-tests adder is for the
results of any documentation tests. We don’t have any documentation tests yet,
but Rust can compile any code examples that appear in our API documentation.
This feature helps keep your docs and your code in sync! We’ll discuss how to
write documentation tests in the “Documentation Comments as
Tests” section of Chapter 14. For now, we’ll
ignore the Doc-tests output.
让我们开始根据自己的需求定制测试。首先,将 it_works 函数的名称更改为不同的名称,例如 exploration,如下所示:
Let’s start to customize the test to our own needs. First, change the name of
the it_works function to a different name, such as exploration, like so:
文件名: src/lib.rs
{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-01-changing-test-name/src/lib.rs}}
然后,再次运行 cargo test。输出现在显示 exploration 而不是 it_works:
Then, run cargo test again. The output now shows exploration instead of
it_works:
{{#include ../listings/ch11-writing-automated-tests/no-listing-01-changing-test-name/output.txt}}
现在我们将添加另一个测试,但这次我们要创建一个会失败的测试!当测试函数中的某些内容引发恐慌时,测试就会失败。每个测试都在一个新线程中运行,当主线程看到测试线程已死亡时,该测试就会被标记为失败。在第 9 章中,我们讨论了引发恐慌最简单的方法是调用 panic! 宏。输入名为 another 的新测试函数,使你的 src/lib.rs 文件如示例 11-3 所示。
Now we’ll add another test, but this time we’ll make a test that fails! Tests
fail when something in the test function panics. Each test is run in a new
thread, and when the main thread sees that a test thread has died, the test is
marked as failed. In Chapter 9, we talked about how the simplest way to panic
is to call the panic! macro. Enter the new test as a function named
another, so your src/lib.rs file looks like Listing 11-3.
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-03/src/lib.rs}}
再次使用 cargo test 运行测试。输出应如示例 11-4 所示,它显示我们的 exploration 测试通过了,而 another 失败了。
Run the tests again using cargo test. The output should look like Listing
11-4, which shows that our exploration test passed and another failed.
{{#include ../listings/ch11-writing-automated-tests/listing-11-03/output.txt}}
行 test tests::another 显示的是 FAILED 而不是 ok。在单独的结果和摘要之间出现了两个新部分:第一个部分显示了每个测试失败的详细原因。在这种情况下,我们得到的细节是 tests::another 失败了,因为它在 src/lib.rs 文件的第 17 行发生了恐慌,消息为 Make this test fail。下一部分仅列出所有失败测试的名称,这在有很多测试和很多详细的失败测试输出时非常有用。我们可以使用失败测试的名称来仅运行该测试,以便更轻松地对其进行调试;我们将在“控制测试如何运行”部分讨论更多运行测试的方法。
Instead of ok, the line test tests::another shows FAILED. Two new
sections appear between the individual results and the summary: The first
displays the detailed reason for each test failure. In this case, we get the
details that tests::another failed because it panicked with the message Make this test fail on line 17 in the src/lib.rs file. The next section lists
just the names of all the failing tests, which is useful when there are lots of
tests and lots of detailed failing test output. We can use the name of a
failing test to run just that test to debug it more easily; we’ll talk more
about ways to run tests in the “Controlling How Tests Are
Run” section.
摘要行显示在最后:总体而言,我们的测试结果是 FAILED。我们有一个测试通过,一个测试失败。
The summary line displays at the end: Overall, our test result is FAILED. We
had one test pass and one test fail.
既然你已经看到了不同情况下测试结果的样子,让我们看看除了 panic! 之外,在测试中还有哪些有用的宏。
Now that you’ve seen what the test results look like in different scenarios,
let’s look at some macros other than panic! that are useful in tests.
使用 assert! 宏检查结果 (Checking Results with assert!)
Checking Results with assert!
标准库提供的 assert! 宏非常有用,它可以确保测试中的某些条件求值为 true。我们给 assert! 宏提供一个求值为布尔值的参数。如果值为 true,则什么也不会发生,测试通过。如果值为 false,则 assert! 宏会调用 panic! 导致测试失败。使用 assert! 宏有助于我们检查代码是否按照我们的意图运行。
The assert! macro, provided by the standard library, is useful when you want
to ensure that some condition in a test evaluates to true. We give the
assert! macro an argument that evaluates to a Boolean. If the value is
true, nothing happens and the test passes. If the value is false, the
assert! macro calls panic! to cause the test to fail. Using the assert!
macro helps us check that our code is functioning in the way we intend.
在第 5 章示例 5-15 中,我们使用了一个 Rectangle 结构体和一个 can_hold 方法,它们在示例 11-5 中再次列出。让我们将这些代码放入 src/lib.rs 文件,然后使用 assert! 宏为其编写一些测试。
In Chapter 5, Listing 5-15, we used a Rectangle struct and a can_hold
method, which are repeated here in Listing 11-5. Let’s put this code in the
src/lib.rs file, then write some tests for it using the assert! macro.
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-05/src/lib.rs}}
can_hold 方法返回一个布尔值,这意味着它是 assert! 宏的一个完美用例。在示例 11-6 中,我们编写了一个测试来练习 can_hold 方法,方法是创建一个宽度为 8、高度为 7 的 Rectangle 实例,并断言它可以持有另一个宽度为 5、高度为 1 的 Rectangle 实例。
The can_hold method returns a Boolean, which means it’s a perfect use case
for the assert! macro. In Listing 11-6, we write a test that exercises the
can_hold method by creating a Rectangle instance that has a width of 8 and
a height of 7 and asserting that it can hold another Rectangle instance that
has a width of 5 and a height of 1.
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-06/src/lib.rs:here}}
请注意 tests 模块内部的 use super::*; 行。tests 模块是一个常规模块,遵循我们在第 7 章“在模块树中引用项的路径”部分介绍的通常可见性规则。因为 tests 模块是一个内部模块,我们需要将被测代码从外部模块引入到内部模块的作用域。我们在这里使用了 glob 通配符,因此我们在外部模块中定义的任何内容都对这个 tests 模块可用。
Note the use super::*; line inside the tests module. The tests module is
a regular module that follows the usual visibility rules we covered in Chapter
7 in the “Paths for Referring to an Item in the Module
Tree”
section. Because the tests module is an inner module, we need to bring the
code under test in the outer module into the scope of the inner module. We use
a glob here, so anything we define in the outer module is available to this
tests module.
我们将测试命名为 larger_can_hold_smaller,并创建了所需的两个 Rectangle 实例。然后,我们调用了 assert! 宏并传递给它调用 larger.can_hold(&smaller) 的结果。该表达式应该返回 true,所以我们的测试应该通过。让我们拭目以待!
We’ve named our test larger_can_hold_smaller, and we’ve created the two
Rectangle instances that we need. Then, we called the assert! macro and
passed it the result of calling larger.can_hold(&smaller). This expression is
supposed to return true, so our test should pass. Let’s find out!
{{#include ../listings/ch11-writing-automated-tests/listing-11-06/output.txt}}
它确实通过了!让我们再添加一个测试,这次断言较小的矩形无法持有较大的矩形:
It does pass! Let’s add another test, this time asserting that a smaller rectangle cannot hold a larger rectangle:
文件名: src/lib.rs
{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-02-adding-another-rectangle-test/src/lib.rs:here}}
因为在这种情况下 can_hold 函数的正确结果是 false ,我们需要在将其传递给 assert! 宏之前对该结果取反。结果是,如果 can_hold 返回 false ,我们的测试将通过:
Because the correct result of the can_hold function in this case is false,
we need to negate that result before we pass it to the assert! macro. As a
result, our test will pass if can_hold returns false:
{{#include ../listings/ch11-writing-automated-tests/no-listing-02-adding-another-rectangle-test/output.txt}}
两个测试都通过了!现在让我们看看当我们代码中引入一个 bug 时,我们的测试结果会发生什么。我们将更改 can_hold 方法的实现,在比较宽度时将大于号 (>) 替换为小于号 (<):
Two tests that pass! Now let’s see what happens to our test results when we
introduce a bug in our code. We’ll change the implementation of the can_hold
method by replacing the greater-than sign (>) with a less-than sign (<)
when it compares the widths:
{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-03-introducing-a-bug/src/lib.rs:here}}
现在运行测试会产生如下结果:
Running the tests now produces the following:
{{#include ../listings/ch11-writing-automated-tests/no-listing-03-introducing-a-bug/output.txt}}
我们的测试抓住了 bug!因为 larger.width 是 8 且 smaller.width 是 5,现在 can_hold 中宽度的比较返回 false:8 不小于 5。
Our tests caught the bug! Because larger.width is 8 and smaller.width is
5, the comparison of the widths in can_hold now returns false: 8 is not
less than 5.
使用 assert_eq! 和 assert_ne! 宏测试相等性 (Testing Equality with assert_eq! and assert_ne!)
Testing Equality with assert_eq! and assert_ne!
验证功能的一种常见方法是测试被测代码的结果与你期望代码返回的值之间的相等性。你可以通过使用 assert! 宏并传递一个使用 == 运算符的表达式来实现这一点。然而,由于这是一种非常常见的测试,标准库提供了一对宏——assert_eq! 和 assert_ne!——来更方便地执行此测试。这些宏分别比较两个参数的相等或不等。如果断言失败,它们还会打印这两个值,这使得更容易看到测试失败的“原因”;相反,assert! 宏仅指示对于 == 表达式它得到了一个 false 值,而不会打印导致 false 值的值。
A common way to verify functionality is to test for equality between the result
of the code under test and the value you expect the code to return. You could
do this by using the assert! macro and passing it an expression using the
== operator. However, this is such a common test that the standard library
provides a pair of macros—assert_eq! and assert_ne!—to perform this test
more conveniently. These macros compare two arguments for equality or
inequality, respectively. They’ll also print the two values if the assertion
fails, which makes it easier to see why the test failed; conversely, the
assert! macro only indicates that it got a false value for the ==
expression, without printing the values that led to the false value.
在示例 11-7 中,我们编写了一个名为 add_two 的函数,它在其参数上加 2,然后使用 assert_eq! 宏测试此函数。
In Listing 11-7, we write a function named add_two that adds 2 to its
parameter, and then we test this function using the assert_eq! macro.
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-07/src/lib.rs}}
让我们检查它是否通过!
Let’s check that it passes!
{{#include ../listings/ch11-writing-automated-tests/listing-11-07/output.txt}}
我们创建了一个名为 result 的变量,它持有调用 add_two(2) 的结果。然后,我们将 result 和 4 作为参数传递给 assert_eq! 宏。此测试的输出行为 test tests::it_adds_two ... ok,ok 文本表明我们的测试通过了!
We create a variable named result that holds the result of calling
add_two(2). Then, we pass result and 4 as the arguments to the
assert_eq! macro. The output line for this test is test tests::it_adds_two ... ok, and the ok text indicates that our test passed!
让我们在代码中引入一个 bug,看看 assert_eq! 失败时的样子。将 add_two 函数的实现改为加 3:
Let’s introduce a bug into our code to see what assert_eq! looks like when it
fails. Change the implementation of the add_two function to instead add 3:
{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-04-bug-in-add-two/src/lib.rs:here}}
再次运行测试:
Run the tests again:
{{#include ../listings/ch11-writing-automated-tests/no-listing-04-bug-in-add-two/output.txt}}
我们的测试抓住了 bug!tests::it_adds_two 测试失败了,消息告诉我们失败的断言是 left == right,以及 left 和 right 的值分别是什么。此消息帮助我们开始调试:left 参数(即我们调用 add_two(2) 的结果)是 5 ,但 right 参数是 4 。你可以想象,当我们有很多测试在运行时,这会特别有帮助。
Our test caught the bug! The tests::it_adds_two test failed, and the message
tells us that the assertion that failed was left == right and what the left
and right values are. This message helps us start debugging: The left
argument, where we had the result of calling add_two(2), was 5, but the
right argument was 4. You can imagine that this would be especially helpful
when we have a lot of tests going on.
请注意,在某些语言和测试框架中,相等断言函数的参数被称为 expected 和 actual ,并且我们指定参数的顺序很重要。然而,在 Rust 中,它们被称为 left 和 right ,我们指定期望值和代码产生值的顺序并不重要。我们可以将此测试中的断言写成 assert_eq!(4, result) ,这会导致相同的失败消息显示 assertion `left == right` failed。
Note that in some languages and test frameworks, the parameters to equality
assertion functions are called expected and actual, and the order in which
we specify the arguments matters. However, in Rust, they’re called left and
right, and the order in which we specify the value we expect and the value
the code produces doesn’t matter. We could write the assertion in this test as
assert_eq!(4, result), which would result in the same failure message that
displays assertion `left == right` failed.
如果我们给 assert_ne! 宏的两个值不相等,它就会通过;如果它们相等,它就会失败。这个宏在以下情况最有用:当我们不确定一个值“会”是什么,但我们知道该值肯定“不应该”是什么。例如,如果我们正在测试一个保证会以某种方式更改其输入的函数,但输入被更改的方式取决于我们运行测试的日期,那么最好的断言方式可能是断言函数的输出不等于其输入。
The assert_ne! macro will pass if the two values we give it are not equal and
will fail if they are equal. This macro is most useful for cases when we’re not
sure what a value will be, but we know what the value definitely shouldn’t
be. For example, if we’re testing a function that is guaranteed to change its
input in some way, but the way in which the input is changed depends on the day
of the week that we run our tests, the best thing to assert might be that the
output of the function is not equal to the input.
在底层,assert_eq! 和 assert_ne! 宏分别使用运算符 == 和 != 。当断言失败时,这些宏会使用调试格式打印它们的参数,这意味着被比较的值必须实现 PartialEq 和 Debug 特征。所有原始类型和大多数标准库类型都实现了这些特征。对于你自己定义的结构体和枚举,你需要实现 PartialEq 才能断言这些类型的相等性。你还需要实现 Debug 才能在断言失败时打印这些值。因为这两个特征都是可派生特征,如第 5 章示例 5-12 中所述,这通常就像在你的结构体或枚举定义中添加 #[derive(PartialEq, Debug)] 标注一样简单。有关这些和其他可派生特征的更多细节,请参阅附录 C “可派生特征”。
Under the surface, the assert_eq! and assert_ne! macros use the operators
== and !=, respectively. When the assertions fail, these macros print their
arguments using debug formatting, which means the values being compared must
implement the PartialEq and Debug traits. All primitive types and most of
the standard library types implement these traits. For structs and enums that
you define yourself, you’ll need to implement PartialEq to assert equality of
those types. You’ll also need to implement Debug to print the values when the
assertion fails. Because both traits are derivable traits, as mentioned in
Listing 5-12 in Chapter 5, this is usually as straightforward as adding the
#[derive(PartialEq, Debug)] annotation to your struct or enum definition. See
Appendix C, “Derivable Traits,” for more
details about these and other derivable traits.
添加自定义失败消息 (Adding Custom Failure Messages)
Adding Custom Failure Messages
你还可以作为可选参数向 assert!、assert_eq! 和 assert_ne! 宏添加要与失败消息一起打印的自定义消息。在必需参数之后指定的任何参数都会被传递给 format! 宏(在第 8 章“使用 + 或 format! 拼接”中讨论),因此你可以传递一个包含 {} 占位符的格式字符串以及要放入这些占位符中的值。自定义消息对于记录断言的含义很有用;当测试失败时,你会更清楚地了解代码出了什么问题。
You can also add a custom message to be printed with the failure message as
optional arguments to the assert!, assert_eq!, and assert_ne! macros. Any
arguments specified after the required arguments are passed along to the
format! macro (discussed in “Concatenating with + or
format!” in Chapter 8), so you can pass a format string that contains {}
placeholders and values to go in those placeholders. Custom messages are useful
for documenting what an assertion means; when a test fails, you’ll have a better
idea of what the problem is with the code.
例如,假设我们有一个通过名称问候他人的函数,并且我们想要测试传入该函数的名称是否出现在输出中:
For example, let’s say we have a function that greets people by name and we want to test that the name we pass into the function appears in the output:
文件名: src/lib.rs
{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-05-greeter/src/lib.rs}}
该程序的要求尚未达成一致,而且我们非常确定问候语开头的 Hello 文本会发生变化。我们决定不希望在要求更改时更新测试,因此与其检查是否与 greeting 函数返回的值完全相等,我们只断言输出包含输入参数的文本。
The requirements for this program haven’t been agreed upon yet, and we’re
pretty sure the Hello text at the beginning of the greeting will change. We
decided we don’t want to have to update the test when the requirements change,
so instead of checking for exact equality to the value returned from the
greeting function, we’ll just assert that the output contains the text of the
input parameter.
现在让我们通过更改 greeting 以排除 name 来在代码中引入一个 bug,看看默认的测试失败是什么样子的:
Now let’s introduce a bug into this code by changing greeting to exclude
name to see what the default test failure looks like:
{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-06-greeter-with-bug/src/lib.rs:here}}
运行此测试会产生如下结果:
Running this test produces the following:
{{#include ../listings/ch11-writing-automated-tests/no-listing-06-greeter-with-bug/output.txt}}
此结果仅指出断言失败以及断言在哪一行。一个更有用的失败消息会打印出 greeting 函数的值。让我们添加一个自定义失败消息,它由一个格式字符串组成,其中的占位符填充了我们从 greeting 函数得到的实际值:
This result just indicates that the assertion failed and which line the
assertion is on. A more useful failure message would print the value from the
greeting function. Let’s add a custom failure message composed of a format
string with a placeholder filled in with the actual value we got from the
greeting function:
{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-07-custom-failure-message/src/lib.rs:here}}
现在当我们运行测试时,我们将得到一个更有用的错误消息:
Now when we run the test, we’ll get a more informative error message:
{{#include ../listings/ch11-writing-automated-tests/no-listing-07-custom-failure-message/output.txt}}
我们可以在测试输出中看到我们实际得到的值,这将帮助我们调试发生了什么,而不是我们期望发生什么。
We can see the value we actually got in the test output, which would help us debug what happened instead of what we were expecting to happen.
使用 should_panic 检查恐慌 (Checking for Panics with should_panic)
Checking for Panics with should_panic
除了检查返回值之外,检查我们的代码是否按预期处理错误条件也很重要。例如,考虑我们在第 9 章示例 9-13 中创建的 Guess 类型。使用 Guess 的其他代码依赖于 Guess 实例仅包含 1 到 100 之间数值的保证。我们可以编写一个测试,确保尝试使用该范围之外的值创建 Guess 实例会引发恐慌。
In addition to checking return values, it’s important to check that our code
handles error conditions as we expect. For example, consider the Guess type
that we created in Chapter 9, Listing 9-13. Other code that uses Guess
depends on the guarantee that Guess instances will contain only values
between 1 and 100. We can write a test that ensures that attempting to create a
Guess instance with a value outside that range panics.
我们通过在测试函数中添加属性 should_panic 来实现这一点。如果函数内的代码引发恐慌,测试就通过;如果函数内的代码不引发恐慌,测试就失败。
We do this by adding the attribute should_panic to our test function. The
test passes if the code inside the function panics; the test fails if the code
inside the function doesn’t panic.
示例 11-8 显示了一个检查 Guess::new 的错误条件是否在我们期望时发生的测试。
Listing 11-8 shows a test that checks that the error conditions of Guess::new
happen when we expect them to.
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-08/src/lib.rs}}
我们将 #[should_panic] 属性放在 #[test] 属性之后,以及它所应用的测试函数之前。让我们看看当这个测试通过时的结果:
We place the #[should_panic] attribute after the #[test] attribute and
before the test function it applies to. Let’s look at the result when this test
passes:
{{#include ../listings/ch11-writing-automated-tests/listing-11-08/output.txt}}
看起来不错!现在让我们通过移除 new 函数在值大于 100 时会引发恐慌的条件,在代码中引入一个 bug:
Looks good! Now let’s introduce a bug in our code by removing the condition
that the new function will panic if the value is greater than 100:
{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-08-guess-with-bug/src/lib.rs:here}}
当我们运行示例 11-8 中的测试时,它将失败:
When we run the test in Listing 11-8, it will fail:
{{#include ../listings/ch11-writing-automated-tests/no-listing-08-guess-with-bug/output.txt}}
在这种情况下我们没有得到非常有用的消息,但当我们查看测试函数时,我们看到它标注了 #[should_panic] 。我们得到的失败意味着测试函数中的代码没有导致恐慌。
We don’t get a very helpful message in this case, but when we look at the test
function, we see that it’s annotated with #[should_panic]. The failure we got
means that the code in the test function did not cause a panic.
使用 should_panic 的测试可能会不够精确。即使测试因为与我们预期不同的原因而引发恐慌,should_panic 测试也会通过。为了使 should_panic 测试更精确,我们可以向 should_panic 属性添加一个可选的 expected 参数。测试工具将确保失败消息包含提供的文本。例如,考虑示例 11-9 中修改后的 Guess 代码,其中 new 函数根据值是太小还是太大引发带有不同消息的恐慌。
Tests that use should_panic can be imprecise. A should_panic test would
pass even if the test panics for a different reason from the one we were
expecting. To make should_panic tests more precise, we can add an optional
expected parameter to the should_panic attribute. The test harness will
make sure that the failure message contains the provided text. For example,
consider the modified code for Guess in Listing 11-9 where the new function
panics with different messages depending on whether the value is too small or
too large.
{{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-09/src/lib.rs:here}}
这个测试会通过,因为我们放入 should_panic 属性的 expected 参数中的值是 Guess::new 函数恐慌消息的一个子字符串。我们可以指定我们期望的完整恐慌消息,在此例中为 Guess value must be less than or equal to 100, got 200 。你选择指定多少取决于恐慌消息中有多少是唯一或动态的,以及你希望测试有多精确。在这种情况下,恐慌消息的子字符串足以确保测试函数中的代码执行了 else if value > 100 的情况。
This test will pass because the value we put in the should_panic attribute’s
expected parameter is a substring of the message that the Guess::new
function panics with. We could have specified the entire panic message that we
expect, which in this case would be Guess value must be less than or equal to 100, got 200. What you choose to specify depends on how much of the panic
message is unique or dynamic and how precise you want your test to be. In this
case, a substring of the panic message is enough to ensure that the code in the
test function executes the else if value > 100 case.
为了看看当带有 expected 消息的 should_panic 测试失败时会发生什么,让我们再次在代码中引入一个 bug,交换 if value < 1 和 else if value > 100 代码块的主体:
To see what happens when a should_panic test with an expected message
fails, let’s again introduce a bug into our code by swapping the bodies of the
if value < 1 and the else if value > 100 blocks:
{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-09-guess-with-panic-msg-bug/src/lib.rs:here}}
这一次当我们运行 should_panic 测试时,它将失败:
This time when we run the should_panic test, it will fail:
{{#include ../listings/ch11-writing-automated-tests/no-listing-09-guess-with-panic-msg-bug/output.txt}}
失败消息指出该测试确实如我们预期的那样引发了恐慌,但恐慌消息不包含预期的字符串 less than or equal to 100 。我们在这种情况下得到的恐慌消息是 Guess value must be greater than or equal to 1, got 200 。现在我们可以开始弄清楚我们的 bug 在哪里了!
The failure message indicates that this test did indeed panic as we expected,
but the panic message did not include the expected string less than or equal to 100. The panic message that we did get in this case was Guess value must be greater than or equal to 1, got 200. Now we can start figuring out where
our bug is!
在测试中使用 Result<T, E> (Using Result<T, E> in Tests)
Using Result<T, E> in Tests
到目前为止,我们所有的测试在失败时都会引发恐慌。我们也可以编写使用 Result<T, E> 的测试!这里是来自示例 11-1 的测试,被重写为使用 Result<T, E> 并在失败时返回 Err 而不是恐慌:
All of our tests so far panic when they fail. We can also write tests that use
Result<T, E>! Here’s the test from Listing 11-1, rewritten to use Result<T, E> and return an Err instead of panicking:
{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-10-result-in-tests/src/lib.rs:here}}
it_works 函数现在的返回类型为 Result<(), String> 。在函数体中,我们不再调用 assert_eq! 宏,而是在测试通过时返回 Ok(()),在测试失败时返回一个包含 String 的 Err。
The it_works function now has the Result<(), String> return type. In the
body of the function, rather than calling the assert_eq! macro, we return
Ok(()) when the test passes and an Err with a String inside when the test
fails.
将测试编写为返回 Result<T, E> 使你能够在测试主体中使用问号运算符,这可以是编写在其中任何操作返回 Err 变体时都应该失败的测试的便捷方式。
Writing tests so that they return a Result<T, E> enables you to use the
question mark operator in the body of tests, which can be a convenient way to
write tests that should fail if any operation within them returns an Err
variant.
你不能在返回 Result<T, E> 的测试上使用 #[should_panic] 标注。要断言一个操作返回 Err 变体,“不要”在 Result<T, E> 值上使用问号运算符。相反,使用 assert!(value.is_err()) 。
You can’t use the #[should_panic] annotation on tests that use Result<T, E>. To assert that an operation returns an Err variant, don’t use the
question mark operator on the Result<T, E> value. Instead, use
assert!(value.is_err()).
既然你已经知道了编写测试的几种方法,让我们看看运行测试时发生了什么,并探索可以与 cargo test 一起使用的不同选项。
Now that you know several ways to write tests, let’s look at what is happening
when we run our tests and explore the different options we can use with cargo test.