简介
欢迎来到「控制测试运行方式」实验。本实验是 《Rust 程序设计语言》 的一部分。你可以在 LabEx 中练习 Rust 技能。
在本实验中,你将学习如何使用 cargo test 的命令行选项以及生成的测试二进制文件来控制 Rust 中测试运行的行为。
欢迎来到「控制测试运行方式」实验。本实验是 《Rust 程序设计语言》 的一部分。你可以在 LabEx 中练习 Rust 技能。
在本实验中,你将学习如何使用 cargo test 的命令行选项以及生成的测试二进制文件来控制 Rust 中测试运行的行为。
正如 cargo run 会编译你的代码然后运行生成的二进制文件一样,cargo test 会在测试模式下编译你的代码并运行生成的测试二进制文件。cargo test 生成的二进制文件的默认行为是并行运行所有测试,并捕获测试运行期间生成的输出,从而防止输出被显示出来,使得读取与测试结果相关的输出更加容易。不过,你可以指定命令行选项来改变这种默认行为。
有些命令行选项会传递给 cargo test,有些则会传递给生成的测试二进制文件。为了区分这两种类型的参数,你要先列出传递给 cargo test 的参数,接着是分隔符 --,然后是传递给测试二进制文件的参数。运行 cargo test --help 会显示你可以与 cargo test 一起使用的选项,运行 cargo test -- --help 会显示在分隔符之后你可以使用的选项。
当你运行多个测试时,默认情况下它们会使用线程并行运行,这意味着它们完成运行的速度更快,你也能更快地得到反馈。由于测试是同时运行的,所以你必须确保你的测试之间不相互依赖,也不依赖于任何共享状态,包括共享环境,比如当前工作目录或环境变量。
例如,假设你的每个测试都会运行一些代码,这些代码会在磁盘上创建一个名为 test-output.txt 的文件,并向该文件写入一些数据。然后每个测试读取该文件中的数据,并断言该文件包含特定的值,每个测试中的值都不同。由于测试是同时运行的,一个测试可能会在另一个测试写入和读取文件的时间段内覆盖该文件。这样第二个测试就会失败,不是因为代码不正确,而是因为测试在并行运行时相互干扰了。一种解决方案是确保每个测试写入不同的文件;另一种解决方案是一次运行一个测试。
如果你不想并行运行测试,或者想要对使用的线程数进行更细粒度的控制,可以将 --test-threads 标志和你想要使用的线程数传递给测试二进制文件。看一下下面的例子:
cargo test -- --test-threads=1
我们将测试线程数设置为 1,这告诉程序不要使用任何并行性。使用一个线程运行测试会比并行运行花费更长的时间,但如果测试共享状态,它们就不会相互干扰。
默认情况下,如果一个测试通过,Rust 的测试库会捕获所有输出到标准输出的内容。例如,如果我们在一个测试中调用 println! 并且测试通过,我们不会在终端中看到 println! 的输出;我们只会看到表明测试通过的那一行。如果一个测试失败,我们会在失败消息的其余部分中看到输出到标准输出的任何内容。
作为一个例子,清单 11-10 中有一个简单的函数,它会打印其参数的值并返回 10,还有一个通过的测试和一个失败的测试。
文件名:src/lib.rs
fn prints_and_returns_10(a: i32) -> i32 {
println!("I got the value {a}");
10
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn this_test_will_pass() {
let value = prints_and_returns_10(4);
assert_eq!(10, value);
}
#[test]
fn this_test_will_fail() {
let value = prints_and_returns_10(8);
assert_eq!(5, value);
}
}
清单 11-10:对调用 println! 的函数进行的测试
当我们使用 cargo test 运行这些测试时,我们会看到以下输出:
running 2 tests
test tests::this_test_will_pass... ok
test tests::this_test_will_fail... FAILED
failures:
---- tests::this_test_will_fail stdout ----
1 I got the value 8
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `5`,
right: `10`', src/lib.rs:19:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace
failures:
tests::this_test_will_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s
请注意,在这个输出中,我们在任何地方都看不到通过的测试运行时打印的 I got the value 4。该输出已被捕获。失败的测试的输出 I got the value 8 [1] 出现在测试总结输出的部分中,该部分还显示了测试失败的原因。
如果我们也想看到通过的测试的打印值,我们可以告诉 Rust 使用 --show-output 来显示成功测试的输出:
cargo test -- --show-output
当我们使用 --show-output 标志再次运行清单 11-10 中的测试时,我们会看到以下输出:
running 2 tests
test tests::this_test_will_pass... ok
test tests::this_test_will_fail... FAILED
successes:
---- tests::this_test_will_pass stdout ----
I got the value 4
successes:
tests::this_test_will_pass
failures:
---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `5`,
right: `10`', src/lib.rs:19:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace
failures:
tests::this_test_will_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s
有时,运行完整的测试套件可能需要很长时间。如果你正在处理特定区域的代码,可能只想运行与该代码相关的测试。你可以通过将你想要运行的测试名称作为参数传递给 cargo test 来选择运行哪些测试。
为了演示如何运行部分测试,我们首先为 add_two 函数创建三个测试,如清单 11-11 所示,然后选择运行哪些测试。
文件名:src/lib.rs
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_two_and_two() {
assert_eq!(4, add_two(2));
}
#[test]
fn add_three_and_two() {
assert_eq!(5, add_two(3));
}
#[test]
fn one_hundred() {
assert_eq!(102, add_two(100));
}
}
清单 11-11:三个具有不同名称的测试
正如我们之前看到的,如果不传递任何参数运行测试,所有测试将并行运行:
running 3 tests
test tests::add_three_and_two... ok
test tests::add_two_and_two... ok
test tests::one_hundred... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s
我们可以将任何测试函数的名称传递给 cargo test,以仅运行该测试:
[object Object]
只有名为 one_hundred 的测试运行了;其他两个测试与该名称不匹配。测试输出在结尾处显示 2 filtered out,让我们知道还有更多测试未运行。
我们不能以这种方式指定多个测试的名称;传递给 cargo test 的第一个值将是唯一被使用的。但是有一种方法可以运行多个测试。
我们可以指定测试名称的一部分,任何名称与该值匹配的测试都将运行。例如,因为我们的两个测试名称中包含 add,所以我们可以通过运行 cargo test add 来运行这两个测试:
[object Object]
此命令运行了所有名称中包含 add 的测试,并筛选掉了名为 one_hundred 的测试。还要注意,测试所在的模块会成为测试名称的一部分,所以我们可以通过按模块名称进行筛选来运行模块中的所有测试。
有时,一些特定的测试执行起来可能非常耗时,所以你可能希望在大多数 cargo test 运行期间排除它们。你可以使用 ignore 属性来注释这些耗时的测试,而不是将所有你想要运行的测试都作为参数列出,如下所示:
文件名:src/lib.rs
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
#[test]
#[ignore]
fn expensive_test() {
// 运行耗时一小时的代码
}
在 #[test] 之后,我们在想要排除的测试上添加 #[ignore] 行。现在当我们运行测试时,it_works 会运行,但 expensive_test 不会:
[object Object]
expensive_test 函数被列为 ignored。如果我们只想运行被忽略的测试,可以使用 cargo test -- --ignored:
[object Object]
通过控制运行哪些测试,你可以确保 cargo test 的结果能快速返回。当你到了检查被忽略测试结果的阶段,并且有时间等待结果时,可以改为运行 cargo test -- --ignored。如果你想运行所有测试,无论它们是否被忽略,可以运行 cargo test -- --include-ignored。
恭喜你!你已经完成了“控制测试运行方式”实验。你可以在 LabEx 中练习更多实验来提升你的技能。