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

如何编写测试

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

最简单的,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.

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

该文件以一个示例 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(包含使用 2 和 2 调用 add 的结果)等于 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.

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.57s
     Running unittests src/lib.rs (target/debug/deps/adder-01ad14159ff659ab)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

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.

可以将一个测试标记为忽略(ignored),这样它在特定情况下就不会运行;我们将在本章稍后的 “除非特别请求,否则忽略某些测试” 一节中介绍。因为我们在这里没有这样做,所以摘要显示 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 开头)是任何文档测试的结果。我们目前还没有任何文档测试,但 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 Filename: src/lib.rs

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn exploration() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

然后,再次运行 cargo test。现在输出显示 exploration 而不是 it_works

Then, run cargo test again. The output now shows exploration instead of it_works:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.59s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::exploration ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

现在我们将添加另一个测试,但这次我们要编写一个会失败的测试!当测试函数中的某些内容发生 panic 时,测试就会失败。每个测试都在一个新线程中运行,当主线程看到测试线程死亡时,该测试就会被标记为失败。在第 9 章中,我们讨论了引发 panic 最简单的方法是调用 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.

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn exploration() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }

    #[test]
    fn another() {
        panic!("Make this test fail");
    }
}

再次使用 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.

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.72s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok

failures:

---- tests::another stdout ----

thread 'tests::another' panicked at src/lib.rs:17:9:
Make this test fail
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::another

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

test tests::another 这一行显示的不是 ok,而是 FAILED。在单个结果和摘要之间出现了两个新部分:第一部分显示了每个测试失败的具体原因。在这种情况下,我们得到的细节是 tests::another 失败了,因为它在 src/lib.rs 文件的第 17 行发生了 panic,带有消息 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!

由标准库提供的 assert! 宏在你想确保测试中的某个条件评估为 true 时非常有用。我们给 assert! 宏一个评估为布尔值的参数。如果值为 true,则什么也不会发生,测试通过。如果值为 falseassert! 宏会调用 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.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

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.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn larger_can_hold_smaller() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(larger.can_hold(&smaller));
    }
}

注意 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!

$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

running 1 test
test tests::larger_can_hold_smaller ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests rectangle

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

它确实通过了!让我们添加另一个测试,这次断言较小的长方形不能容纳较大的长方形:

It does pass! Let’s add another test, this time asserting that a smaller rectangle cannot hold a larger rectangle:

文件名:src/lib.rs Filename: src/lib.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn larger_can_hold_smaller() {
        // --snip--
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(larger.can_hold(&smaller));
    }

    #[test]
    fn smaller_cannot_hold_larger() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(!smaller.can_hold(&larger));
    }
}

因为在这种情况下 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:

$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

running 2 tests
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests rectangle

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

两个测试都通过了!现在让我们看看当我们在代码中引入一个 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:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

// --snip--
impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width < other.width && self.height > other.height
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn larger_can_hold_smaller() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(larger.can_hold(&smaller));
    }

    #[test]
    fn smaller_cannot_hold_larger() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(!smaller.can_hold(&larger));
    }
}

现在运行测试会产生以下结果:

Running the tests now produces the following:

$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

running 2 tests
test tests::larger_can_hold_smaller ... FAILED
test tests::smaller_cannot_hold_larger ... ok

failures:

---- tests::larger_can_hold_smaller stdout ----

thread 'tests::larger_can_hold_smaller' panicked at src/lib.rs:28:9:
assertion failed: larger.can_hold(&smaller)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::larger_can_hold_smaller

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

我们的测试发现了 bug!因为 larger.width8smaller.width5,现在 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!

验证功能的一种常见方法是测试被测代码的结果与你期望代码返回的值是否相等。你可以通过使用 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.

pub fn add_two(a: u64) -> u64 {
    a + 2
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_adds_two() {
        let result = add_two(2);
        assert_eq!(result, 4);
    }
}

让我们检查它是否通过!

Let’s check that it passes!

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

我们创建了一个名为 result 的变量,用于保存调用 add_two(2) 的结果。然后,我们将 result4 作为参数传递给 assert_eq! 宏。此测试的输出行是 test tests::it_adds_two ... okok 文本表示我们的测试通过了!

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:

pub fn add_two(a: u64) -> u64 {
    a + 3
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_adds_two() {
        let result = add_two(2);
        assert_eq!(result, 4);
    }
}

再次运行测试:

Run the tests again:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_adds_two ... FAILED

failures:

---- tests::it_adds_two stdout ----

thread 'tests::it_adds_two' panicked at src/lib.rs:12:9:
assertion `left == right` failed
  left: 5
 right: 4
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::it_adds_two

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

我们的测试发现了 bug!tests::it_adds_two 测试失败了,消息告诉我们失败的断言是 left == right,以及 leftright 的值是多少。此消息有助于我们开始调试: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! 宏分别使用运算符 ==!=。当断言失败时,这些宏会使用调试格式(debug formatting)打印其参数,这意味着被比较的值必须实现 PartialEqDebug Trait。所有的原始类型和大多数标准库类型都实现了这些 Trait。对于你自己定义的结构体和枚举,你需要实现 PartialEq 才能断言这些类型的相等性。你还需要实现 Debug 才能在断言失败时打印这些值。因为这两个 Trait 都是可派生的 Trait(正如第 5 章示例 5-12 中提到的),这通常就像在你的结构体或枚举定义中添加 #[derive(PartialEq, Debug)] 注解一样简单。有关这些和其他可派生 Trait 的更多详细信息,请参阅附录 C “可派生 Trait”

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

你还可以作为可选参数向 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 Filename: src/lib.rs

pub fn greeting(name: &str) -> String {
    format!("Hello {name}!")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(result.contains("Carol"));
    }
}

这个程序的各种需求尚未达成一致,我们很确定问候语开头的 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:

pub fn greeting(name: &str) -> String {
    String::from("Hello!")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(result.contains("Carol"));
    }
}

运行此测试会产生以下结果:

Running this test produces the following:

$ cargo test
   Compiling greeter v0.1.0 (file:///projects/greeter)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
     Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)

running 1 test
test tests::greeting_contains_name ... FAILED

failures:

---- tests::greeting_contains_name stdout ----

thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
assertion failed: result.contains("Carol")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::greeting_contains_name

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

此结果仅指示断言失败以及断言所在的行号。一个更有用的失败消息应该是打印出来自 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:

pub fn greeting(name: &str) -> String {
    String::from("Hello!")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(
            result.contains("Carol"),
            "Greeting did not contain name, value was `{result}`"
        );
    }
}

现在当我们运行测试时,我们将得到一个更具信息量的错误消息:

Now when we run the test, we’ll get a more informative error message:

$ cargo test
   Compiling greeter v0.1.0 (file:///projects/greeter)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.93s
     Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)

running 1 test
test tests::greeting_contains_name ... FAILED

failures:

---- tests::greeting_contains_name stdout ----

thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
Greeting did not contain name, value was `Hello!`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::greeting_contains_name

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

我们可以在测试输出中看到我们实际得到的值,这有助于我们调试发生了什么,而不是我们期望发生什么。

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 检查 Panic

Checking for Panics with should_panic

除了检查返回值之外,检查我们的代码是否按预期处理错误条件也很重要。例如,考虑我们在第 9 章示例 9-13 中创建的 Guess 类型。其他使用 Guess 的代码依赖于 Guess 实例仅包含 1 到 100 之间的值的保证。我们可以编写一个测试,确保尝试创建一个带有该范围之外值的 Guess 实例会发生 panic。

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 来实现这一点。如果函数内部的代码发生了 panic,则测试通过;如果函数内部的代码没有发生 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.

pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("Guess value must be between 1 and 100, got {value}.");
        }

        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}

我们将 #[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:

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests guessing_game

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

看起来不错!现在让我们在代码中引入一个 bug,移除当值大于 100 时 new 函数会发生 panic 的条件:

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:

pub struct Guess {
    value: i32,
}

// --snip--
impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!("Guess value must be between 1 and 100, got {value}.");
        }

        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}

当我们运行示例 11-8 中的测试时,它将失败:

When we run the test in Listing 11-8, it will fail:

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
     Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... FAILED

failures:

---- tests::greater_than_100 stdout ----
note: test did not panic as expected at src/lib.rs:21:8

failures:
    tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

在这种情况下我们没有得到非常有用的消息,但当我们查看测试函数时,我们看到它被标注了 #[should_panic]。我们得到的失败意味着测试函数中的代码没有引发 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 的测试可能会不够精确。即使测试由于与我们预期的原因不同的原因而发生 panic,should_panic 测试也会通过。为了使 should_panic 测试更精确,我们可以向 should_panic 属性添加一个可选的 expected 参数。测试框架将确保失败消息包含提供的文本。例如,考虑示例 11-9 中修改后的 Guess 代码,其中 new 函数根据值是太小还是太大而引发不同的 panic 消息。

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.

pub struct Guess {
    value: i32,
}

// --snip--

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be greater than or equal to 1, got {value}."
            );
        } else if value > 100 {
            panic!(
                "Guess value must be less than or equal to 100, got {value}."
            );
        }

        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic(expected = "less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

此测试将通过,因为我们在 should_panic 属性的 expected 参数中放入的值是 Guess::new 函数发生 panic 时消息的子字符串。我们可以指定我们期望的完整 panic 消息,在本例中是 Guess value must be less than or equal to 100, got 200。你选择指定什么取决于 panic 消息中有多少是唯一或动态的,以及你希望测试有多精确。在这种情况下,panic 消息的一个子字符串就足以确保测试函数中的代码执行了 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 < 1else 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:

pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be less than or equal to 100, got {value}."
            );
        } else if value > 100 {
            panic!(
                "Guess value must be greater than or equal to 1, got {value}."
            );
        }

        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic(expected = "less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

这一次当我们运行 should_panic 测试时,它将失败:

This time when we run the should_panic test, it will fail:

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... FAILED

failures:

---- tests::greater_than_100 stdout ----

thread 'tests::greater_than_100' panicked at src/lib.rs:12:13:
Guess value must be greater than or equal to 1, got 200.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
      panic message: "Guess value must be greater than or equal to 1, got 200."
 expected substring: "less than or equal to 100"

failures:
    tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

失败消息指出,此测试确实如我们预期那样发生了 panic,但 panic 消息不包含预期的字符串 less than or equal to 100。在这种情况下,我们得到的 panic 消息实际上是 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

到目前为止,我们所有的测试在失败时都会发生 panic。我们还可以编写使用 Result<T, E> 的测试!这是示例 11-1 中的测试,重写为使用 Result<T, E> 并在失败时返回 Err 而不是发生 panic:

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:

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() -> Result<(), String> {
        let result = add(2, 2);

        if result == 4 {
            Ok(())
        } else {
            Err(String::from("two plus two does not equal four"))
        }
    }
}

it_works 函数现在的返回类型为 Result<(), String>。在函数体中,我们不再调用 assert_eq! 宏,而是在测试通过时返回 Ok(()),在测试失败时返回一个包含 StringErr

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.