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