在开发软件时,我们通常会编写两种测试:单元测试和集成测试。它们用于不同的目 的,并与被测试代码进行不同的交互。单元测试总是轻量级、单个组件的测试,开发人员可以经常运行它们,从而提供更快速的反馈循环;而集成测试比较庞大,并根据环境和规 格模拟真实的应用场景。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>
(突然终止)。
let result = 2 + 2;
assert!(result == 4); // 通过
assert!(result == 5); // 不通过,导致 panic
assert!(true);
assert!(a == b, "{} was not equal to {}", a, b);
assert_eq!
这会接收两个值,如果它们不相等,则会<font style="color:rgb(13, 13, 13);">panic</font>
。它也可以采用自定 义异常信息的格式化字符串:
let a = 23;
let b = 87;
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>
,这是一种用于在开发阶段捕捉错误的手段。
assert_eq!(2 + 2, 4); // 相等,通过
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>**
宏成为在开发过程中检查条件,而不影响生产环境性能的理想选择。