控制测试的运行方式
正如 cargo run 会编译你的代码然后运行生成的二进制文件一样,cargo test 会以测试模式编译你的代码并运行生成的测试二进制文件。cargo test 生成的二进制文件的默认行为是并行运行所有测试并捕获测试运行期间生成的输出,从而防止输出被显示,使阅读与测试结果相关的输出更容易。不过,你可以指定命令行选项来改变这种默认行为。
有些命令行选项传给 cargo test,有些则传给生成的测试二进制文件。要区分这两种参数,你可以先列出传给 cargo test 的参数,然后加上分隔符 --,再列出传给测试二进制文件的参数。运行 cargo test --help 会显示你可以与 cargo test 一起使用的选项,而运行 cargo test -- --help 会显示分隔符之后可以使用的选项。这些选项也记录在《rustc 之书》的"测试"一节中。
并行或连续运行测试
当你运行多个测试时,默认情况下它们会使用线程并行运行,这意味着它们会更快完成运行,你也能更快得到反馈。由于测试是同时运行的,你必须确保测试之间不相互依赖,也不依赖任何共享状态,包括共享环境,例如当前工作目录或环境变量。
例如,假设你的每个测试都运行一些代码,在磁盘上创建一个名为 test-output.txt 的文件,并向该文件写入一些数据。然后,每个测试读取该文件中的数据并断言文件包含某个特定值,而这个值在每个测试中都不同。由于测试同时运行,一个测试可能会在另一个测试写入和读取该文件之间的时间内覆盖该文件。于是第二个测试会失败,不是因为代码不正确,而是因为测试在并行运行时相互干扰。一种解决方案是确保每个测试写入不同的文件;另一种解决方案是一次运行一个测试。
如果你不想并行运行测试,或者想对使用的线程数进行更细粒度的控制,可以向测试二进制文件发送 --test-threads 标志以及你想使用的线程数。请看下面的例子:
$ cargo test -- --test-threads=1
我们将测试线程数设为 1,告诉程序不要使用任何并行性。使用一个线程运行测试会比并行运行花费更长时间,但如果测试共享状态,它们就不会相互干扰。
显示函数输出
默认情况下,如果测试通过,Rust 的测试库会捕获所有打印到标准输出的内容。例如,如果我们在测试中调用 println! 且测试通过,我们就不会在终端中看到 println! 的输出;我们只会看到表明测试通过的那一行。如果测试失败,我们会在失败消息的其余部分中看到打印到标准输出的内容。
作为示例,列表 11-10 有一个简单的函数,它打印其参数的值并返回 10,还有一个通过的测试和一个失败的测试。
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!(value, 10);
}
#[test]
fn this_test_will_fail() {
let value = prints_and_returns_10(8);
assert_eq!(value, 5);
}
}
println! 的函数的测试当我们用 cargo test 运行这些测试时,会看到以下输出:
$ cargo test
Compiling silly-function v0.1.0 (file:///projects/silly-function)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)
running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok
failures:
---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'tests::this_test_will_fail' (6019863) panicked at src/lib.rs:19:9:
assertion `left == right` failed
left: 10
right: 5
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
error: test failed, to rerun pass `--lib`
注意,在这个输出中我们没有看到 I got the value 4,这是在通过的测试运行时打印的。该输出已被捕获。失败的测试的输出 I got the value 8 出现在测试摘要输出部分,该部分也显示了测试失败的原因。
如果我们也想看到通过测试的打印值,可以用 --show-output 告诉 Rust 同时显示成功测试的输出:
$ cargo test -- --show-output
当我们再次使用 --show-output 标志运行列表 11-10 中的测试时,会看到以下输出:
$ cargo test -- --show-output
Compiling silly-function v0.1.0 (file:///projects/silly-function)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)
running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok
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 'tests::this_test_will_fail' (6022313) panicked at src/lib.rs:19:9:
assertion `left == right` failed
left: 10
right: 5
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
error: test failed, to rerun pass `--lib`
按名称运行测试的子集
运行完整的测试套件有时会花费很长时间。如果你正在处理某个特定领域的代码,可能只想运行与该代码相关的测试。你可以通过向 cargo test 传入要运行的测试名称作为参数来选择运行哪些测试。
为了演示如何运行测试的子集,我们将首先为 add_two 函数创建三个测试,如列表 11-11 所示,并选择运行其中哪些。
pub fn add_two(a: u64) -> u64 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_two_and_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
#[test]
fn add_three_and_two() {
let result = add_two(3);
assert_eq!(result, 5);
}
#[test]
fn one_hundred() {
let result = add_two(100);
assert_eq!(result, 102);
}
}
如果我们运行测试时不传入任何参数,如前所述,所有测试会并行运行:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
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
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
运行单个测试
我们可以将任何测试函数的名称传给 cargo test,只运行该测试:
$ cargo test one_hundred
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.69s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::one_hundred ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
只有名称为 one_hundred 的测试运行了;另外两个测试与该名称不匹配。测试输出在末尾显示 2 filtered out,让我们知道还有更多测试没有运行。
我们无法用这种方式指定多个测试的名称;只有传给 cargo test 的第一个值会被使用。但有一种方法可以运行多个测试。
通过过滤运行多个测试
我们可以指定测试名称的一部分,任何名称与该值匹配的测试都会被运行。例如,因为我们有两个测试的名称包含 add,所以可以通过运行 cargo test add 来运行这两个测试:
$ cargo test add
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
这个命令运行了名称中带有 add 的所有测试,并过滤掉了名为 one_hundred 的测试。另请注意,测试所在的模块会成为测试名称的一部分,因此我们可以通过按模块名称过滤来运行某个模块中的所有测试。
忽略测试,除非特别指定
有时几个特定的测试执行起来非常耗时,因此你可能希望在大多数 cargo test 运行中将它们排除。与其将所有你想运行的测试作为参数列出,不如使用 ignore 属性对耗时的测试进行注解,将它们排除,如下所示:
文件名:src/lib.rs
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[test]
#[ignore]
fn expensive_test() {
// code that takes an hour to run
}
}
在 #[test] 之后,我们在想要排除的测试上添加了 #[ignore] 行。现在当我们运行测试时,it_works 会运行,但 expensive_test 不会:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::expensive_test ... ignored
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
expensive_test 函数被列为 ignored。如果我们只想运行被忽略的测试,可以使用 cargo test -- --ignored:
$ cargo test -- --ignored
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::expensive_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
通过控制运行哪些测试,你可以确保 cargo test 的结果能快速返回。当你到了需要检查 ignored 测试结果且有时间等待结果的时候,可以运行 cargo test -- --ignored。如果你想运行所有测试,无论它们是否被忽略,可以运行 cargo test -- --include-ignored。