<font style="color:rgb(13, 13, 13);">Rust</font> 中,单元测试通常位于与它们测试的代码相同的文件中,并且可以使用 <font style="color:rgb(13, 13, 13);">Rust</font> 提供的测试框架来编写和运行。
第一个单元测试
#[test]fn basic_test() {assert!(true);}
一个单元测试会被构造成一个函数,并使用[test]属性进行标记。前面的 basic_test 函数 中并没有什么复杂的内容。其中有一个基本的断言 assert!,将 true 值作为参数。为了更好 地有序组织代码,你还可以创建一个名为 tests(根据约定)的子模块,并将所有相关的测 试代码放入其中。
运行测试
编译器会忽略带有测试标记的函数 的编译,除非它被告知在测试模式下运行。这可以通过在编译测试代码时将--test 标记参数 传递给 rustc 实现。之后,只需执行编译后的二进制文件即可运行测试。对于之前的测试, 我们将在测试模式下运行以下命令来编译它:
rustc --test first_unit_test.rs
通过--test 标记参数,rustc 将 main 函数和一些测试工具代码放在一起,并将所有已定 义的测试函数作为线程并行调用。默认情况下,所有测试都是并行运行的,除非将下列环 境变量设置成“RUST_TEST_THREADS=1”。这意味着如果我们希望在单线程模式下运行 之前的测试,那么可以通过“RUST_TEST_THREADS=1”来实现。
:::info
现在 Cargo 已经支持运行测试,所有这些通常都是通过调用 cargo test 命令在内部完成的。此命令为我们编译并运行测试已标记的函数。在接下来的示例中,我们将主要使用 Cargo 来执行测试。
:::
隔离测试代码
当我们的测试变得日益复杂时,可能需要创建其他辅助方法,这些方法只能在测试代 码的上下文中使用。在这种情况下,将相关的测试代码与实际代码隔离是很有益的。我们 可以通过将所有与测试有关的代码封装在模块中,并在其上放置#[cfg(test)]注释标记来达到 此目的。
#[cfg(...)]属性中的 cfg 通常用于条件编译,但不限于测试代码。它可以为不同体系结 构或配置标记引用或排除某些代码。这里的配置标记是 test。这样做的好处是,只有当你运行 cargo test 命令时,测试代码才会被 编译,并包含到已编译的二进制文件中,否则其将会被忽略。
假如你希望以编程方式生成测试数据,但是不必在正式上线的版本中包含这些代码。 让我们通过运行 cargo new unit_test --lib 命令来演示这一点。在 lib.rs 中,我们定义了一些 测试和函数:
//我们想要测试的函数fn sum(a: i8, b: i8) -> i8 {a+b}#[cfg(test)]mod tests {fn sum_inputs_outputs() -> Vec<((i8, i8), i8)> {vec![((1, 1), 2), ((0, 0), 0), ((2, -2), 0)]}#[test]fn test_sums() {for (input, output) in sum_inputs_outputs() {assert_eq!(crate::sum(input.0, input.1), output);}}}
我们可以通过 cargo test 命令来运行这些测试。让我们详细解读一下上述代码。在 sum_ inputs_outputs 函数中会生成已知的输入和输出对。#[test]属性使得 test_sums 函数不会出现 在正式发布的编译版本中。但是,sum_inputs_outputs 并没有使用#[test]进行标记,如果它 是在 tests 模块之外声明的,那么它会被包含到正式发布的编译版本中。通过将#[cfg(test)] 标记和一个 mod tests 子模块搭配使用,并将所有测试代码及其相关函数封装到此模块中, 可以确保代码和生成的二进制文件都是纯粹的测试代码。
我们的 sum 函数在前面没有 pub 关键字修饰的情况下是私有的,这意味着模块中的单 元测试还允许用户测试私有的函数和方法。这样做会非常方便。
#[test]
**#[test]**<font style="color:rgb(13, 13, 13);"> </font>属性用于标记单个测试函数。它告诉<font style="color:rgb(13, 13, 13);">Rust</font>编译器,这个函数是一个测试用例,应当在运行测试时执行。- 当你运行
**cargo test**时,<font style="color:rgb(13, 13, 13);">Rust</font>的测试框架会自动找到所有标记了**#[test]**<font style="color:rgb(13, 13, 13);"> </font>的函数,并执行它们。 **#[test]**<font style="color:rgb(13, 13, 13);"> </font>应用于函数级别。
#[cfg(test)]
**#[cfg(test)]**属性用于条件编译。它标记的代码块只在编译测试代码时被包含,平常的构建过程会忽略这部分代码。- 这允许你在同一个文件中保留测试代码和生产代码,而不会增加最终构建的大小。
**#[cfg(test)]**<font style="color:rgb(13, 13, 13);"> </font>通常用于模块级别,来包围整个测试模块。 - 使用
**#[cfg(test)]**可以避免在非测试构建中包含测试代码和依赖,使得最终的应用更加轻量。
:::info
在上述代码中,#[test] 属性标记了 test_sums 函数作为一个测试函数,这使得 Rust 测试框架能够识别并执行它作为单元测试的一部分。如果你省略了 #[test] 属性,那么即使它位于 #[cfg(test)] 标记的模块内,test_sums 函数也不会被当作测试函数执行。
简而言之,#[test] 不能被省略,如果你希望 cargo test 命令自动发现并运行 test_sums 函数。#[cfg(test)] 标记的模块表明该模块仅在编译测试代码时包含,而 #[test] 属性直接将某个函数指定为测试用例。两者都是必要的,但服务于不同的目的:
#[cfg(test)] 用于条件编译,确保测试代码只在测试构建中被编译。
#[test] 用于标记具体的测试函数,让测试运行器知道哪些函数应当作为测试执行。
:::
故障测试
还有一些测试用例,用户希望API方法基于,某些输入而执行失败,并且希望测试框架断言此失败。Rust为此提供了一个名为#[should_panic]的属性。下面是一个使用此属性的测试:
#[test]#[should_panic]fn this_panics() {assert_eq!(1, 2);}
`#[should_panic]`属性可以和`#[test]`属性搭配使用,以表示运行 `this_panics` 函数应该导致不可恢复的故障,在 `Rust` 中这类异常被称为 `panic`。
**#[should_panic]** 是 <font style="color:rgb(13, 13, 13);">Rust</font> 中的一个测试属性,用于标记预期会导致 <font style="color:rgb(13, 13, 13);">panic</font> 的测试函数。当你在测试中使用这个属性时,你期望被标记的函数在执行过程中触发 <font style="color:rgb(13, 13, 13);">panic</font>。如果该函数确实触发了 <font style="color:rgb(13, 13, 13);">panic</font>,测试会被视为通过;如果没有触发 <font style="color:rgb(13, 13, 13);">panic</font>,则测试失败。
**#[should_panic]** 可以帮助确保你的程序能够在遇到不可恢复的错误时优雅地终止,而不是无声地忽略问题或以不可预测的方式失败。
忽略测试
编写测试时另一个有用的属性是#[ignore]。如果你的测试代码量非常庞大,那么可以 使用#[ignore]属性标记告知测试工具在执行 cargo test 命令时忽略此类测试功能。然后你可以向测试工具或 <font style="background-color:#FBF5CB;">cargo test</font> 命令传递<font style="background-color:#FBF5CB;">--ignored</font> 参数来单独运行这些测试。下面的代码包含一个笨拙的循环操作,当运行 cargo test 命令时,默认情况下会被忽略:
#[cfg(test)]mod tests {#[test]#[ignore]fn expensive_test() {// 一些耗时的测试代码}}
cargo test -- --ignored
:::info 注意
也可以通过向 Cargo 提供测试函数名称来运行单个测 试,例如 cargo test some_test_func。
:::
