单元测试是软件开发过程中的一种测试方法,它通过对软件应用程序的最小可测试部分(通常是函数或方法)进行自动化测试来验证它们是否正确执行预期的行为。单元测试的主要目的是隔离代码库中的每个部分并证明个别部分是正确的,从而提高代码的质量和稳定性。在 <font style="color:rgb(13, 13, 13);">Rust</font> 中,单元测试通常位于与它们测试的代码相同的文件中,并且可以使用 <font style="color:rgb(13, 13, 13);">Rust</font> 提供的测试框架来编写和运行。

第一个单元测试

  1. #[test]
  2. fn basic_test() {
  3. assert!(true);
  4. }

一个单元测试会被构造成一个函数,并使用[test]属性进行标记。前面的 basic_test 函数 中并没有什么复杂的内容。其中有一个基本的断言 assert!,将 true 值作为参数。为了更好 地有序组织代码,你还可以创建一个名为 tests(根据约定)的子模块,并将所有相关的测 试代码放入其中。

运行测试

编译器会忽略带有测试标记的函数 的编译,除非它被告知在测试模式下运行。这可以通过在编译测试代码时将--test 标记参数 传递给 rustc 实现。之后,只需执行编译后的二进制文件即可运行测试。对于之前的测试, 我们将在测试模式下运行以下命令来编译它:

rustc --test first_unit_test.rs

通过--test 标记参数,rustcmain 函数和一些测试工具代码放在一起,并将所有已定 义的测试函数作为线程并行调用。默认情况下,所有测试都是并行运行的,除非将下列环 境变量设置成“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 中,我们定义了一些 测试和函数:

  1. //我们想要测试的函数
  2. fn sum(a: i8, b: i8) -> i8 {
  3. a+b
  4. }
  5. #[cfg(test)]
  6. mod tests {
  7. fn sum_inputs_outputs() -> Vec<((i8, i8), i8)> {
  8. vec![((1, 1), 2), ((0, 0), 0), ((2, -2), 0)]
  9. }
  10. #[test]
  11. fn test_sums() {
  12. for (input, output) in sum_inputs_outputs() {
  13. assert_eq!(crate::sum(input.0, input.1), output);
  14. }
  15. }
  16. }

我们可以通过 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]的属性。下面是一个使用此属性的测试:

  1. #[test]
  2. #[should_panic]
  3. fn this_panics() {
  4. assert_eq!(1, 2);
  5. }
  1. `#[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 命令时,默认情况下会被忽略:

  1. #[cfg(test)]
  2. mod tests {
  3. #[test]
  4. #[ignore]
  5. fn expensive_test() {
  6. // 一些耗时的测试代码
  7. }
  8. }

cargo test -- --ignored

:::info 注意

也可以通过向 Cargo 提供测试函数名称来运行单个测 试,例如 cargo test some_test_func

:::