在开发软件时,我们通常会编写两种测试:单元测试和集成测试。它们用于不同的目 的,并与被测试代码进行不同的交互。单元测试总是轻量级、单个组件的测试,开发人员可以经常运行它们,从而提供更快速的反馈循环;而集成测试比较庞大,并根据环境和规 格模拟真实的应用场景。Rust 内置的测试框架为我们提供了编写和组织这些测试的合理默认参数。

  • 单元测试:单元测试通常编写在包含被测试代码的同一模块中。当这些测试的数量 增加时,它们被组织成嵌套模块形式的一个实体。通常在当前模块中创建一个子模 块,对该测试进行命名(例如根据约定将之命名为 tests),并添加相应的注释属性(#[cfg(test)]),然后将所有与测试有关的函数放入其中。该属性只是告知编译器在 测试模块中引用代码,但这只在执行 cargo test 命令时生效。稍后将详细介绍属性 的相关信息。
  • 集成测试:集成测试在程序库根目录下的 tests/目录中单独编写。它们被构造成本 身就像是被测试程序库的使用者。tests/目录中的任何.rs 文件都可以添加一个 use 声明来引入需要测试的任何公共 API

测试原语

Rust内置的测试框架基于一系列主要属性和宏组成的基元。在我们编写任何实际的测试之前,熟悉如何有效地使用它们将非常重要。

属性

Rust代码中的属性是指元素的注释。元素项是软件包crate中的顶层语言结构,例如函数、模块、结构体、枚举和声明的常量,以及在软件包根目录下定义的其他内容。属性通常是编译器内置的,不过也可以由用户通过编译器插件创建。它们指示编译器为其下显示的元素注入额外的代码或含义,如果对应的是模块,那么会对该模块应用上述规则。

  • #[<name>]:这适用于每个元素,通常显示在它们定义的上方。例如,Rust中的测试函数使用#[test]属性进行注释。它表示该函数将被视为测试工具的一部分。
    • 这种形式的属性被称为“外部属性”(<font style="color:rgb(13, 13, 13);">Outer Attribute</font>),它直接应用于紧随其后的项(如函数、结构体、模块等)。这意味着该属性仅影响下一个语法项。
  • #![<name>]:这适用于每个软件包。注意,与#[<name>]相比,其中额外包含一个!。它通常位于用户软件包根目录的最顶端部分。
    • 这种形式的属性被称为“内部属性”(<font style="color:rgb(13, 13, 13);">Inner Attribute</font>),它应用于包含它的项(如模块或整个 <font style="color:rgb(13, 13, 13);">crate</font>)。这意味着该属性会影响当前作用域内的所有项。
    • 如果这个属性位于一个文件的顶部(作为 <font style="color:rgb(13, 13, 13);">crate</font> 的一部分),它会应用于整个 <font style="color:rgb(13, 13, 13);">crate</font>,允许整个 <font style="color:rgb(13, 13, 13);">crate</font> 中的所有代码存在未使用的变量而不警告。如果它位于模块的开头,它只影响该模块内的代码。

:::info 注意

如果要创建程序库项目,那么项目根目录中的文件一 般是 lib.rs 文件,而创建二进制项目时,项目根目录中 的文件将是 main.rs 文件。

:::

断言宏

在测试中,当给定一个测试用例时,我们尝试在给定的输入区间断言程序组件的预期行为。语言通常提供被称为断言函数的函数来执行这些断言。Rust为我们提供了通过宏实现的断言函数,帮助我们实现相同的功能。

<font style="color:rgb(13, 13, 13);">Rust</font> 中,断言宏是一种用于测试代码的执行路径是否符合预期的宏。当你想验证某个条件是否为真时,可以使用断言宏来实现。如果断言的条件评估为 **true**,程序会继续执行;如果条件评估为 **false**,程序会立即终止,并报告错误,这对于调试和确保代码正确性非常有帮助。
assert!
这是最基本的断言宏。它接受一个表达式作为参数,如果表达式的结果是 **<font style="color:rgb(13, 13, 13);">false</font>****<font style="color:rgb(13, 13, 13);">assert!</font>** 宏会导致程序 <font style="color:rgb(13, 13, 13);">panic</font>(突然终止)。
  1. let result = 2 + 2;
  2. assert!(result == 4); // 通过
  3. assert!(result == 5); // 不通过,导致 panic
  4. assert!(true);
  5. assert!(a == b, "{} was not equal to {}", a, b);
assert_eq!

这会接收两个值,如果它们不相等,则会<font style="color:rgb(13, 13, 13);">panic</font>。它也可以采用自定 义异常信息的格式化字符串:

  1. let a = 23;
  2. let b = 87;
  3. assert_eq!(a, b, "{} and {} are not equal", a, b);
assert_ne!

这与assert_eq!类似,因为它需要接收两个值,但只有在两个值互不相等的情况下才进行断言。如果这两个值相等,**<font style="color:rgb(13, 13, 13);">assert_ne!</font>** 会导致程序 <font style="color:rgb(13, 13, 13);">panic</font>,这是一种用于在开发阶段捕捉错误的手段。

  1. assert_eq!(2 + 2, 4); // 相等,通过
  2. assert_ne!(2 + 2, 5); // 不相等,通过
debug_assert!

**<font style="color:rgb(13, 13, 13);">debug_assert!</font>** 宏在 <font style="color:rgb(13, 13, 13);">Rust</font> 中作用类似于 **<font style="color:rgb(13, 13, 13);">assert!</font>** 宏,但有一个关键区别:**<font style="color:rgb(13, 13, 13);">debug_assert!</font>** 宏仅在 **<font style="color:rgb(13, 13, 13);">debug 构建</font>**(即未开启优化的构建)中有效,而在 **<font style="color:rgb(13, 13, 13);">release 构建</font>**(开启优化的构建)时,**<font style="color:rgb(13, 13, 13);">debug_assert!</font>** 的调用会被编译器忽略,不会有任何运行时开销。这使得 **<font style="color:rgb(13, 13, 13);">debug_assert!</font>** 宏成为在开发过程中检查条件,而不影响生产环境性能的理想选择。