如何编写测试

测试是 Rust 函数,用于验证非测试代码是否以预期的方式运行。测试函数的主体通常执行以下三个操作

  • 设置任何需要的数据或状态。
  • 运行您想要测试的代码。
  • 断言结果是否符合您的预期。

让我们看看 Rust 专门为编写执行这些操作的测试而提供的功能,其中包括 test 属性、一些宏和 should_panic 属性。

测试函数的解剖

最简单来说,Rust 中的测试是一个用 test 属性注解的函数。属性是关于 Rust 代码片段的元数据;一个例子是我们在第 5 章中与结构体一起使用的 derive 属性。要将一个函数变成一个测试函数,在 fn 前面的行添加 #[test]。当您使用 cargo test 命令运行测试时,Rust 构建一个测试运行器二进制文件,该文件运行带注解的函数并报告每个测试函数是通过还是失败。

每当我们使用 Cargo 创建一个新的库项目时,都会自动为我们生成一个包含测试函数的测试模块。此模块为您提供了一个编写测试的模板,因此您不必每次开始一个新项目时都查找确切的结构和语法。您可以根据需要添加任意数量的附加测试函数和任意数量的测试模块!

在实际测试任何代码之前,我们将通过实验模板测试来探索测试工作原理的某些方面。然后,我们将编写一些真实世界的测试,调用我们编写的一些代码,并断言其行为是正确的。

让我们创建一个名为 adder 的新库项目,它将添加两个数字

$ cargo new adder --lib Created library `adder` project $ cd adder

您的 adder 库中 src/lib.rs 文件的内容应如列表 11-1 所示。

文件名:src/lib.rs
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); } }
列表 11-1:cargo new 自动生成的代码

该文件以示例 add 函数开头,以便我们有一些东西可以测试。

现在,让我们只关注 it_works 函数。请注意 #[test] 注解:此属性表明这是一个测试函数,因此测试运行器知道将此函数视为测试。我们还可能在 tests 模块中包含非测试函数,以帮助设置常见场景或执行常见操作,因此我们始终需要指明哪些函数是测试。

示例函数体使用 assert_eq! 宏来断言 result(包含调用 add 并传入 2 和 2 的结果)等于 4。此断言充当典型测试格式的示例。让我们运行它,看看这个测试是否通过。

cargo test 命令运行我们项目中的所有测试,如列表 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 (file:///projects/adder/target/debug/deps/adder-7acb243c25ffd9dc) 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
列表 11-2:运行自动生成的测试的输出

Cargo 编译并运行了测试。我们看到 running 1 test 这一行。下一行显示生成的测试函数的名称,称为 tests::it_works,以及运行该测试的结果是 ok。总体摘要 test result: ok. 表示所有测试都通过了,而读取 1 passed; 0 failed 的部分汇总了通过或失败的测试数量。

可以将测试标记为忽略,使其在特定实例中不运行;我们将在 “忽略某些测试,除非特别要求”章节后面的部分中介绍。因为我们在这里没有这样做,所以摘要显示 0 ignored

0 measured 统计数据用于衡量性能的基准测试。截至本文撰写时,基准测试仅在 nightly Rust 中可用。请参阅 关于基准测试的文档 以了解更多信息。

我们可以将参数传递给 cargo test 命令,以仅运行名称与字符串匹配的测试;这称为过滤,我们将在 “按名称运行测试子集”章节中介绍。在这里,我们没有过滤正在运行的测试,因此摘要末尾显示 0 filtered out

测试输出的下一部分从 Doc-tests adder 开始,用于任何文档测试的结果。我们还没有任何文档测试,但 Rust 可以编译出现在我们的 API 文档中的任何代码示例。此功能有助于保持您的文档和代码同步!我们将在第 14 章的 “将文档注释作为测试”章节中讨论如何编写文档测试。现在,我们将忽略 Doc-tests 输出。

让我们开始自定义测试以满足我们自己的需求。首先,将 it_works 函数的名称更改为不同的名称,例如 exploration,如下所示

文件名: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

$ 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。

文件名: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); } #[test] fn another() { panic!("Make this test fail"); } }
列表 11-3:添加第二个测试,该测试将失败,因为我们调用了 panic!

使用 cargo test 再次运行测试。输出应如列表 11-4 所示,该列表显示我们的 exploration 测试通过,而 another 测试失败。

$ 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`
列表 11-4:一个测试通过,一个测试失败时的测试结果

test tests::another 行不是显示 ok,而是显示 FAILED。在各个结果和摘要之间出现了两个新部分:第一个部分显示每个测试失败的详细原因。在本例中,我们得到了 another 失败的详细信息,因为它在 src/lib.rs 文件的第 17 行 panicked at 'Make this test fail'。下一部分仅列出所有失败测试的名称,这在有很多测试和很多详细的失败测试输出时很有用。我们可以使用失败测试的名称来仅运行该测试,以便更轻松地对其进行调试;我们将在 “控制测试的运行方式”章节中详细介绍运行测试的方法。

摘要行显示在末尾:总的来说,我们的测试结果是 FAILED。我们有一个测试通过,一个测试失败。

既然您已经了解了不同场景下的测试结果是什么样的,那么让我们看看除 panic! 之外的一些在测试中很有用的宏。

使用 assert! 宏检查结果

标准库提供的 assert! 宏在您想要确保测试中的某些条件评估为 true 时非常有用。我们给 assert! 宏一个评估为布尔值的参数。如果该值为 true,则什么也不会发生,并且测试通过。如果该值为 false,则 assert! 宏调用 panic! 以导致测试失败。使用 assert! 宏有助于我们检查我们的代码是否以我们预期的方式运行。

在第 5 章的列表 5-15 中,我们使用了 Rectangle 结构体和一个 can_hold 方法,它们在列表 11-5 中重复出现。让我们将此代码放入 src/lib.rs 文件中,然后使用 assert! 宏为其编写一些测试。

文件名: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 } }
列表 11-5:第 5 章中的 Rectangle 结构体及其 can_hold 方法

can_hold 方法返回一个布尔值,这意味着它是 assert! 宏的完美用例。在列表 11-6 中,我们编写了一个测试,通过创建一个宽度为 8、高度为 7 的 Rectangle 实例来练习 can_hold 方法,并断言它可以容纳另一个宽度为 5、高度为 1 的 Rectangle 实例。

文件名: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() { let larger = Rectangle { width: 8, height: 7, }; let smaller = Rectangle { width: 5, height: 1, }; assert!(larger.can_hold(&smaller)); } }
列表 11-6:can_hold 的测试,检查较大的矩形是否确实可以容纳较小的矩形

请注意 tests 模块内的 use super::*; 行。tests 模块是一个常规模块,它遵循我们在第 7 章 “模块树中引用项的路径”章节中介绍的常用可见性规则。由于 tests 模块是一个内部模块,我们需要将被测代码从外部模块引入到内部模块的作用域中。我们在这里使用 glob,因此我们在外部模块中定义的任何内容都可用于此 tests 模块。

我们将我们的测试命名为 larger_can_hold_smaller,并且我们创建了我们需要的两个 Rectangle 实例。然后我们调用了 assert! 宏并将调用 larger.can_hold(&smaller) 的结果传递给它。此表达式应该返回 true,因此我们的测试应该通过。让我们来看看!

$ 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

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

文件名: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,我们的测试将通过

$ 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

两个测试都通过了!现在让我们看看当我们在代码中引入错误时,我们的测试结果会发生什么。我们将通过在比较宽度时用小于号替换大于号来更改 can_hold 方法的实现

#[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)); } }

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

$ 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`

我们的测试捕获了错误!因为 larger.width8,而 smaller.width5,所以 can_hold 中宽度的比较现在返回 false:8 不小于 5。

使用 assert_eq!assert_ne! 宏测试相等性

验证功能的一种常见方法是测试被测代码的结果与您期望代码返回的值之间的相等性。您可以使用 assert! 宏并向其传递一个使用 == 运算符的表达式来做到这一点。但是,这是一个非常常见的测试,标准库提供了一对宏——assert_eq!assert_ne!——以更方便地执行此测试。这些宏分别比较两个参数的相等性或不相等性。如果断言失败,它们还会打印这两个值,这使得更容易看到测试为什么失败;相反,assert! 宏仅指示它获得了 == 表达式的 false 值,而没有打印导致 false 值的值。

在列表 11-7 中,我们编写了一个名为 add_two 的函数,该函数将其参数加 2,然后我们使用 assert_eq! 宏测试此函数。

文件名:src/lib.rs
pub fn add_two(a: usize) -> usize { a + 2 } #[cfg(test)] mod tests { use super::*; #[test] fn it_adds_two() { let result = add_two(2); assert_eq!(result, 4); } }
列表 11-7:使用 assert_eq! 宏测试函数 add_two

让我们检查一下它是否通过!

$ 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 ... ok,而 ok 文本表明我们的测试通过了!

让我们在我们的代码中引入一个错误,看看 assert_eq! 在失败时是什么样子。将 add_two 函数的实现更改为改为加 3

pub fn add_two(a: usize) -> usize { a + 3 } #[cfg(test)] mod tests { use super::*; #[test] fn it_adds_two() { let result = add_two(2); assert_eq!(result, 4); } }

再次运行测试

$ 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`

我们的测试捕获了错误!it_adds_two 测试失败,并且消息告诉我们 assertion `left == right` failed 以及 leftright 值是什么。此消息帮助我们开始调试:left 参数(我们调用 add_two(2) 的结果所在的位置)是 5,但 right 参数是 4。您可以想象,当我们进行大量测试时,这将特别有用。

请注意,在某些语言和测试框架中,相等性断言函数的参数称为 expectedactual,并且我们指定参数的顺序很重要。但是,在 Rust 中,它们被称为 leftright,并且我们指定我们期望的值和代码生成的值的顺序无关紧要。我们可以将此测试中的断言写为 assert_eq!(4, result),这将产生相同的失败消息,显示 assertion failed: `(left == right)`

如果 assert_ne! 宏给定的两个值不相等,则会通过,如果它们相等,则会失败。此宏最适用于我们不确定值是什么的情况,但我们知道该值肯定不应该是什么。例如,如果我们正在测试一个保证以某种方式更改其输入的函数,但输入更改的方式取决于我们运行测试的星期几,那么最好的断言可能是函数的输出不等于输入。

在底层,assert_eq!assert_ne! 宏分别使用运算符 ==!=。当断言失败时,这些宏使用调试格式打印它们的参数,这意味着被比较的值必须实现 PartialEqDebug trait。所有原始类型和大多数标准库类型都实现了这些 trait。对于您自己定义的结构体和枚举,您需要实现 PartialEq 才能断言这些类型的相等性。您还需要实现 Debug 才能在断言失败时打印值。由于这两个 trait 都是可派生 trait,如第 5 章的列表 5-12 中所述,因此通常像将 #[derive(PartialEq, Debug)] 注解添加到您的结构体或枚举定义中一样简单。有关这些和其他可派生 trait 的更多详细信息,请参阅附录 C,“可派生 Trait”了解更多详细信息。

添加自定义失败消息

您还可以添加自定义消息,以便与失败消息一起打印,作为 assert!assert_eq!assert_ne! 宏的可选参数。在必需参数之后指定的任何参数都将传递给 format! 宏(在第 8 章 “使用 + 运算符或 format! 宏进行连接”章节中讨论),因此您可以传递一个包含 {} 占位符和值以放入这些占位符的格式字符串。自定义消息对于记录断言的含义很有用;当测试失败时,您将更好地了解代码的问题所在。

例如,假设我们有一个通过名称问候人们的函数,并且我们想测试我们传递给函数的名称是否出现在输出中

文件名: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 函数返回值的完全相等性,而是只断言输出包含输入参数的文本。

现在让我们通过更改 greeting 以排除 name 来在此代码中引入一个错误,看看默认的测试失败是什么样的

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")); } }

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

$ 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 函数获得的实际值填充的占位符

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}`" ); } }

现在,当我们运行测试时,我们将获得更具信息性的错误消息

$ 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`

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

使用 should_panic 检查 Panic

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

我们通过将属性 should_panic 添加到我们的测试函数来做到这一点。如果函数内部的代码 panic,则测试通过;如果函数内部的代码没有 panic,则测试失败。

列表 11-8 显示了一个测试,该测试检查 Guess::new 的错误条件是否在我们期望它们发生时发生。

文件名:src/lib.rs
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); } }
列表 11-8:测试条件是否会导致 panic!

我们将 #[should_panic] 属性放在 #[test] 属性之后,以及它适用的测试函数之前。让我们看看此测试通过时的结果

$ 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

看起来不错!现在让我们通过删除条件来在我们的代码中引入一个错误,即如果值大于 100,new 函数将 panic

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 中的测试时,它将失败

$ 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 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。

使用 should_panic 的测试可能不精确。即使测试由于与我们期望的原因不同的原因而 panic,should_panic 测试也会通过。为了使 should_panic 测试更精确,我们可以向 should_panic 属性添加一个可选的 expected 参数。测试工具将确保失败消息包含提供的文本。例如,考虑列表 11-9 中 Guess 的修改代码,其中 new 函数会根据值是太小还是太大而 panic 并显示不同的消息。

文件名:src/lib.rs
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); } }
列表 11-9:测试 panic!,其中 panic 消息包含指定的子字符串

此测试将通过,因为我们在 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 情况。

要查看当带有 expected 消息的 should_panic 测试失败时会发生什么,让我们再次通过交换 if value < 1else if value > 100 代码块的主体来在我们的代码中引入一个错误

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 测试时,它将失败

$ 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. 现在我们可以开始弄清楚我们的错误在哪里!

在测试中使用 Result<T, E>

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

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

编写测试以便它们返回 Result<T, E> 使您可以在测试主体中使用问号运算符,这可能是编写测试的便捷方法,如果其中的任何操作返回 Err 变体,则测试应失败。

您不能在使用 Result<T, E> 的测试上使用 #[should_panic] 注解。要断言操作返回 Err 变体,不要Result<T, E> 值上使用问号运算符。而是使用 assert!(value.is_err())

现在您已经了解了几种编写测试的方法,让我们看看当我们运行测试时会发生什么,并探索我们可以与 cargo test 一起使用的不同选项。